UNPKG

zp-figma-converter

Version:

Convert Figma designs to various code formats

529 lines 20.4 kB
"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