zp-figma-converter
Version:
Convert Figma designs to various code formats
529 lines • 20.4 kB
JavaScript
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.CsdConverter = void 0;
// converters/csd-converter.ts
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const types_1 = require("../../figma/types");
const json2csd_1 = require("../../libs/json2csd");
/**
* Converter for intermediate nodes to CSD format
*/
class CsdConverter {
/**
* Main conversion method - converts intermediate node to CSD
* @param rootNode Root node from Figma
* @param outputPath Output file path for CSD
*/
convert(rootNode, outputPath) {
return __awaiter(this, void 0, void 0, function* () {
// Convert the adjusted node to CSD format
const csdContent = this.nodeToCsd(rootNode);
const adjustedCsdContent = this.adjustRootNodePosition(csdContent);
// Generate JSON structure and save to CSD file
yield this.generateAndSaveCsd(rootNode, adjustedCsdContent, outputPath);
});
}
/**
* Get file extension for this converter
*/
getExtension() {
return '.csd';
}
adjustRootNodePosition(rootNode) {
return Object.assign(Object.assign({}, rootNode), { Position: {
X: 0,
Y: 0
} });
}
/**
* Generate CSD structure and save to file
* @param rootNode Original root node
* @param csdContent Converted CSD content
* @param outputPath Output file path
*/
generateAndSaveCsd(rootNode, csdContent, outputPath) {
return __awaiter(this, void 0, void 0, function* () {
// Create JSON structure for CSD
const jsonData = this.createCsdJsonStructure(rootNode, csdContent);
// Save temporary JSON file
const tempJsonFile = this.saveTempJsonFile(jsonData);
// Convert JSON to CSD and save to output file
try {
(0, json2csd_1.convertFile)(tempJsonFile, outputPath);
// Delete temporary file
fs.unlinkSync(tempJsonFile);
}
catch (error) {
console.error('Error converting to CSD:', error);
throw error;
}
});
}
/**
* Create JSON structure for CSD format
* @param rootNode Original root node
* @param csdContent Converted CSD content
* @returns JSON structure for CSD
*/
createCsdJsonStructure(rootNode, csdContent) {
return {
Name: csdContent.Name,
Type: "Scene",
ID: this.hashCode(rootNode.id).toString(),
Content: {
Content: {
Animation: {
Duration: 0,
Speed: 1
},
ObjectData: {
ctype: 'GameNodeObjectData',
Name: "Scene",
Tag: 0,
Size: csdContent.Size,
Children: [csdContent]
}
}
}
};
}
/**
* Save JSON data to temporary file
* @param jsonData JSON data to save
* @returns Path to temporary file
*/
saveTempJsonFile(jsonData) {
// Create temporary directory if needed
const tempDir = path.join(process.cwd(), 'temp');
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// Generate temporary file path
const tempJsonFile = path.join(tempDir, `csd_temp_${Date.now()}.json`);
// Write JSON to file
fs.writeFileSync(tempJsonFile, JSON.stringify(jsonData, null, 2));
return tempJsonFile;
}
/**
* Convert intermediate node to CSD node
* @param node Intermediate node
* @returns CSD node
*/
nodeToCsd(node) {
// Determine the CSD node type based on the Figma node type
switch (node.type) {
case types_1.FigmaNodeType.RECTANGLE:
return this.convertToImageView(node);
case types_1.FigmaNodeType.TEXT:
return this.convertToText(node);
case types_1.FigmaNodeType.FRAME:
if (node.imageRef) {
return this.convertToImageView(node);
}
return this.convertToPanel(node);
case types_1.FigmaNodeType.GROUP:
case types_1.FigmaNodeType.COMPONENT:
case types_1.FigmaNodeType.COMPONENT_SET:
case types_1.FigmaNodeType.INSTANCE:
return this.convertToPanel(node);
case types_1.FigmaNodeType.VECTOR:
case types_1.FigmaNodeType.ELLIPSE:
case types_1.FigmaNodeType.STAR:
case types_1.FigmaNodeType.LINE:
case types_1.FigmaNodeType.REGULAR_POLYGON:
case types_1.FigmaNodeType.BOOLEAN:
return this.convertToSprite(node);
default:
return this.convertToSingleNode(node);
}
}
/**
* Convert to ImageView node
*/
convertToImageView(node) {
var _a;
const imageViewNode = {
Name: node.name,
ctype: 'ImageViewObjectData',
FileData: {
Type: "Normal",
Path: node.imageRef || "",
Plist: ""
},
Children: []
};
// Add common properties
this.addCommonProperties(imageViewNode, node);
// Handle rounded corners
if ((_a = node.styles) === null || _a === void 0 ? void 0 : _a.cornerRadius) {
imageViewNode.Scale9Enable = true;
imageViewNode.Scale9OriginX = Math.floor(node.styles.cornerRadius);
imageViewNode.Scale9OriginY = Math.floor(node.styles.cornerRadius);
imageViewNode.Scale9Width = Math.floor(node.width);
imageViewNode.Scale9Height = Math.floor(node.height);
}
// Process child nodes
this.processChildNodes(node, imageViewNode);
return imageViewNode;
}
/**
* Convert to Text node
*/
convertToText(node) {
var _a, _b, _c;
const textNode = {
Name: node.name,
ctype: 'TextObjectData',
LabelText: ((_a = node.styles) === null || _a === void 0 ? void 0 : _a.textContent) || ''
};
// Add common properties
this.addCommonProperties(textNode, node);
// Handle font and style
if ((_b = node.styles) === null || _b === void 0 ? void 0 : _b.font) {
textNode.FontSize = node.styles.font.size;
// Handle text alignment
if (node.styles.font.alignHorizontal) {
switch (node.styles.font.alignHorizontal.toUpperCase()) {
case 'LEFT':
textNode.HorizontalAlignmentType = "HT_Left";
break;
case 'CENTER':
textNode.HorizontalAlignmentType = "HT_Center";
break;
case 'RIGHT':
textNode.HorizontalAlignmentType = "HT_Right";
break;
}
}
}
// Handle text color
if ((_c = node.styles) === null || _c === void 0 ? void 0 : _c.fill) {
textNode.CColor = this.convertStringColorToRgb(node.styles.fill);
}
return textNode;
}
/**
* Convert to Panel node
*/
convertToPanel(node) {
var _a;
const panelNode = {
Name: node.name,
ctype: 'PanelObjectData',
Children: [] // Initialize with empty array
};
// Add common properties
this.addCommonProperties(panelNode, node);
// Handle background color
if ((_a = node.styles) === null || _a === void 0 ? void 0 : _a.fill) {
const color = this.convertStringColorToRgb(node.styles.fill);
if (color) {
panelNode.SingleColor = color;
panelNode.BackColorAlpha = color.A || 255;
panelNode.ComboBoxIndex = 1; // Use background color
}
}
// Process child nodes
this.processChildNodes(node, panelNode);
return panelNode;
}
/**
* Convert to Sprite node
*/
convertToSprite(node) {
const spriteNode = {
Name: node.name,
ctype: 'SpriteObjectData',
FileData: {
Type: "Normal",
Path: node.imageRef || "",
Plist: ""
}
};
// Add common properties
this.addCommonProperties(spriteNode, node);
return spriteNode;
}
/**
* Convert to SingleNode
*/
convertToSingleNode(node) {
const singleNode = {
Name: node.name,
ctype: 'SingleNodeObjectData'
};
// Add common properties
this.addCommonProperties(singleNode, node);
return singleNode;
}
/**
* Add common properties to all node types
*/
addCommonProperties(csdNode, node) {
// Position
csdNode.Position = {
X: Math.floor(node.x),
Y: Math.floor(node.y)
};
// Size
csdNode.Size = {
X: Math.floor(node.width),
Y: Math.floor(node.height)
};
// Scale
csdNode.Scale = {
ScaleX: 1,
ScaleY: 1
};
// AnchorPoint
csdNode.AnchorPoint = this.getAnchorPoint(node);
// Opacity
if (node.opacity !== undefined) {
csdNode.Alpha = Math.round(node.opacity * 255);
}
// Rotation
if (node.rotation !== undefined) {
csdNode.RotationSkewX = node.rotation;
csdNode.RotationSkewY = node.rotation;
}
// Tag
const idParts = node.id.split(':');
if (idParts.length > 1) {
csdNode.Tag = parseInt(idParts[1], 10) || 0;
}
else {
csdNode.Tag = 0;
}
// ActionTag
csdNode.ActionTag = this.hashCode(node.id);
// Visibility
csdNode.IconVisible = true;
csdNode.Children = [];
}
/**
* Determine the anchor point based on the node type
*/
getAnchorPoint(node) {
var _a, _b, _c;
const cocosNodeType = this.getCocosCsdTypeFromNode(node);
switch (cocosNodeType) {
// Panel and other containers (0, 0)
case 'PanelObjectData':
case 'SingleNodeObjectData':
case 'GameNodeObjectData':
case 'ScrollViewObjectData':
case 'PageViewObjectData':
case 'ListViewObjectData':
return { ScaleX: 0, ScaleY: 0 };
// Widget and UI element (0.5, 0.5)
case 'ButtonObjectData':
case 'ImageViewObjectData':
case 'SpriteObjectData':
case 'CheckBoxObjectData':
case 'TextAtlasObjectData':
case 'TextBMFontObjectData':
case 'LoadingBarObjectData':
case 'SliderObjectData':
case 'TextFieldObjectData':
return { ScaleX: 0.5, ScaleY: 0.5 };
// Text node has special handling
case 'TextObjectData':
// For text, the default is (0.5, 0.5) but can be adjusted based on alignment
if ((_b = (_a = node.styles) === null || _a === void 0 ? void 0 : _a.font) === null || _b === void 0 ? void 0 : _b.alignHorizontal) {
const hAlign = node.styles.font.alignHorizontal.toUpperCase();
const vAlign = ((_c = node.styles.font.alignVertical) === null || _c === void 0 ? void 0 : _c.toUpperCase()) || 'TOP';
let scaleX = 0.5; // Default is center
let scaleY = 0.5; // Default is center
// Determine horizontal alignment
if (hAlign === 'LEFT')
scaleX = 0;
else if (hAlign === 'RIGHT')
scaleX = 1;
// Determine vertical alignment
if (vAlign === 'TOP')
scaleY = 0;
else if (vAlign === 'BOTTOM')
scaleY = 1;
return { ScaleX: scaleX, ScaleY: scaleY };
}
return { ScaleX: 0.5, ScaleY: 0.5 };
default:
// Default is (0, 0) for cases that cannot be determined
return { ScaleX: 0, ScaleY: 0 };
}
}
/**
* Get Cocos CSD node type from intermediate node
*/
getCocosCsdTypeFromNode(node) {
switch (node.type) {
case types_1.FigmaNodeType.RECTANGLE:
return 'ImageViewObjectData';
case types_1.FigmaNodeType.TEXT:
return 'TextObjectData';
case types_1.FigmaNodeType.FRAME:
case types_1.FigmaNodeType.GROUP:
case types_1.FigmaNodeType.COMPONENT:
case types_1.FigmaNodeType.COMPONENT_SET:
case types_1.FigmaNodeType.INSTANCE:
return 'PanelObjectData';
case types_1.FigmaNodeType.VECTOR:
case types_1.FigmaNodeType.ELLIPSE:
case types_1.FigmaNodeType.STAR:
case types_1.FigmaNodeType.LINE:
case types_1.FigmaNodeType.REGULAR_POLYGON:
case types_1.FigmaNodeType.BOOLEAN:
return 'SpriteObjectData';
default:
return 'SingleNodeObjectData';
}
}
/**
* Convert hex or rgba color string to RGB object
*/
convertStringColorToRgb(colorStr) {
// Handle hex color
if (colorStr.startsWith('#')) {
const r = parseInt(colorStr.slice(1, 3), 16);
const g = parseInt(colorStr.slice(3, 5), 16);
const b = parseInt(colorStr.slice(5, 7), 16);
return { R: r, G: g, B: b };
}
// Handle rgba color
if (colorStr.startsWith('rgba')) {
const rgba = colorStr.match(/rgba\((\d+),\s*(\d+),\s*(\d+),\s*([\d.]+)\)/);
if (rgba) {
const r = parseInt(rgba[1], 10);
const g = parseInt(rgba[2], 10);
const b = parseInt(rgba[3], 10);
const a = parseFloat(rgba[4]);
return { R: r, G: g, B: b, A: Math.round(a * 255) };
}
}
// Default
return { R: 255, G: 255, B: 255 };
}
/**
* Create a hash code from a string
*/
hashCode(str) {
let hash = 0;
for (let i = 0; i < str.length; i++) {
hash = ((hash << 5) - hash) + str.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}
return Math.abs(hash);
}
/**
* Process child nodes for a panel
* @param node Parent intermediate node
* @param panelNode Parent panel node
*/
processChildNodes(node, panelNode) {
// Store original position for relative positioning of children
const nodeX = node.x;
const nodeY = node.y;
// Handle child nodes
if (node.children && node.children.length > 0) {
node.children.forEach(childNode => {
// Convert child node
const childCsdNode = this.nodeToCsd(childNode);
// Calculate and adjust child position
this.adjustChildNodePosition(childNode, childCsdNode, nodeX, nodeY, node.width, node.height);
// Ensure Children is initialized and add child
if (panelNode.Children) {
panelNode.Children.push(childCsdNode);
}
});
}
}
/**
* Calculate and adjust child node position relative to parent
*/
adjustChildNodePosition(childNode, childCsdNode, parentX, parentY, parentWidth, parentHeight) {
if (!childCsdNode.Position)
return;
// Calculate relative position to parent node
childCsdNode.Position = {
X: childNode.x - parentX,
Y: childNode.y - parentY
};
// Adjust position according to Cocos Studio coordinate system
this.adjustChildPositionBasedOnParentAnchor(childNode, childCsdNode, { x: parentX, y: parentY, width: parentWidth, height: parentHeight });
}
/**
* Adjust the position of the child node based on the AnchorPoint of the parent node
*/
adjustChildPositionBasedOnParentAnchor(childNode, childCsdNode, parentBoundingBox) {
if (!childCsdNode.Position || !childCsdNode.AnchorPoint)
return;
// Get the anchor point of the child
const childAnchorX = childCsdNode.AnchorPoint.ScaleX || 0;
const childAnchorY = childCsdNode.AnchorPoint.ScaleY || 0;
// --- NOTE ABOUT COORDINATES ---
// Figma: (0,0) at the top left, Y increases when going down
// Cocos: (0,0) at the bottom left, Y increases when going up
// 1. Get the relative position in the Figma coordinate system
let relX = childCsdNode.Position.X;
let relY = childCsdNode.Position.Y;
// 2. Adjust based on the anchor point of the child node
// In Cocos, position is relative to the anchor point of the child
if (childCsdNode.Size) {
relX += childCsdNode.Size.X * childAnchorX;
relY += childCsdNode.Size.Y * (1 - childAnchorY);
}
// 3. Convert to Cocos coordinates (reverse the Y axis)
// In Cocos, Y=0 is at the bottom, Y increases when going up
relY = parentBoundingBox.height - relY;
// 4. Apply any constraints that might affect positioning
// this.applyConstraintsIfNeeded(childNode, childCsdNode, relX, relY, parentBoundingBox.width, parentBoundingBox.height);
// 5. Update the position of the child node (round to avoid precision issues)
childCsdNode.Position.X = Math.round(relX);
childCsdNode.Position.Y = Math.round(relY);
}
}
exports.CsdConverter = CsdConverter;
//# sourceMappingURL=csd-converter.js.map