cocos2d-cli 1.1.1 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,46 +1,470 @@
1
1
  /**
2
- * prefab-create 命令 - 创建新预制体文件
2
+ * prefab-create 命令 - 从 JSON 结构创建预制体文件
3
+ *
4
+ * JSON 格式示例:
5
+ * {
6
+ * "name": "Panel",
7
+ * "width": 400,
8
+ * "height": 300,
9
+ * "color": "#ffffff",
10
+ * "components": [
11
+ * { "type": "sprite", "sizeMode": 1 },
12
+ * { "type": "widget", "top": 0, "bottom": 0 }
13
+ * ],
14
+ * "children": [...]
15
+ * }
16
+ *
17
+ * 节点属性:name, width, height, x, y, color, opacity, anchorX, anchorY, rotation, scaleX, scaleY, active
18
+ * 组件属性:type + 各组件特有属性
3
19
  */
4
20
 
5
- const { saveScene, createPrefab } = require('../lib/fire-utils');
6
21
  const fs = require('fs');
7
22
  const path = require('path');
23
+ const { Components, generateId } = require('../lib/components');
24
+ const { createPrefab, generateFileId } = require('../lib/fire-utils');
8
25
 
9
- function run(args) {
10
- if (args.length < 2) {
11
- console.log(JSON.stringify({ error: '用法: cocos2.4 prefab-create <预制体路径> <根节点名称>' }));
12
- return;
26
+ // 支持的组件类型
27
+ const COMPONENT_TYPES = {
28
+ 'sprite': 'sprite',
29
+ 'label': 'label',
30
+ 'button': 'button',
31
+ 'layout': 'layout',
32
+ 'widget': 'widget',
33
+ 'camera': 'camera',
34
+ 'canvas': 'canvas',
35
+ 'particle': 'particleSystem',
36
+ 'particlesystem': 'particleSystem'
37
+ };
38
+
39
+ /**
40
+ * 解析颜色字符串 #RRGGBB 或 #RRGGBBAA
41
+ */
42
+ function parseColor(colorStr) {
43
+ if (!colorStr || typeof colorStr !== 'string') return null;
44
+
45
+ let hex = colorStr.replace('#', '');
46
+ if (hex.length === 6) {
47
+ return {
48
+ r: parseInt(hex.substring(0, 2), 16),
49
+ g: parseInt(hex.substring(2, 4), 16),
50
+ b: parseInt(hex.substring(4, 6), 16),
51
+ a: 255
52
+ };
53
+ } else if (hex.length === 8) {
54
+ return {
55
+ r: parseInt(hex.substring(0, 2), 16),
56
+ g: parseInt(hex.substring(2, 4), 16),
57
+ b: parseInt(hex.substring(4, 6), 16),
58
+ a: parseInt(hex.substring(6, 8), 16)
59
+ };
60
+ }
61
+ return null;
62
+ }
63
+
64
+ /**
65
+ * 生成 UUID
66
+ */
67
+ function generateUUID() {
68
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
69
+ const r = Math.random() * 16 | 0;
70
+ const v = c === 'x' ? r : (r & 0x3 | 0x8);
71
+ return v.toString(16);
72
+ });
73
+ }
74
+
75
+ /**
76
+ * 解析组件定义
77
+ * 支持两种格式:
78
+ * 1. 字符串: "sprite"
79
+ * 2. 对象: { "type": "sprite", "sizeMode": 1 }
80
+ */
81
+ function parseComponent(compDef) {
82
+ if (typeof compDef === 'string') {
83
+ const type = COMPONENT_TYPES[compDef.toLowerCase()];
84
+ return type ? { type, props: {} } : null;
13
85
  }
14
86
 
15
- const prefabPath = args[0];
16
- const rootName = args[1] || 'RootNode';
87
+ if (typeof compDef === 'object' && compDef.type) {
88
+ const type = COMPONENT_TYPES[compDef.type.toLowerCase()];
89
+ if (!type) return null;
90
+
91
+ const props = { ...compDef };
92
+ delete props.type;
93
+ return { type, props };
94
+ }
17
95
 
18
- try {
19
- // 检查文件是否已存在
20
- if (fs.existsSync(prefabPath)) {
21
- console.log(JSON.stringify({ error: `文件已存在: ${prefabPath}` }));
22
- return;
96
+ return null;
97
+ }
98
+
99
+ /**
100
+ * 应用组件属性
101
+ * @param comp 组件对象
102
+ * @param props 属性对象
103
+ * @param node 节点对象(可选,用于 label 的 color 设置到节点)
104
+ */
105
+ function applyComponentProps(comp, props, node) {
106
+ if (!props || !comp) return;
107
+
108
+ const type = comp.__type__;
109
+
110
+ // Label 的 color 属性应该设置到节点上
111
+ if (type === 'cc.Label' && props.color && node) {
112
+ const parsed = parseColor(props.color);
113
+ if (parsed) {
114
+ node._color = { "__type__": "cc.Color", ...parsed };
115
+ }
116
+ delete props.color; // 不再在组件中处理
117
+ }
118
+
119
+ // Widget 特殊处理:根据设置的方向计算 alignFlags
120
+ // 位掩码定义 (来自 Cocos Creator 源码)
121
+ // TOP=1, MID(verticalCenter)=2, BOT=4, LEFT=8, CENTER(horizontalCenter)=16, RIGHT=32
122
+ if (type === 'cc.Widget') {
123
+ const ALIGN = {
124
+ top: 1,
125
+ verticalCenter: 2,
126
+ bottom: 4,
127
+ left: 8,
128
+ horizontalCenter: 16,
129
+ right: 32
130
+ };
131
+ let alignFlags = 0;
132
+
133
+ for (const dir of ['top', 'bottom', 'left', 'right', 'horizontalCenter', 'verticalCenter']) {
134
+ if (props[dir] !== undefined && props[dir] !== null) {
135
+ alignFlags |= ALIGN[dir];
136
+ }
23
137
  }
24
138
 
25
- // 确保目录存在
26
- const dir = path.dirname(prefabPath);
27
- if (!fs.existsSync(dir)) {
28
- fs.mkdirSync(dir, { recursive: true });
139
+ if (alignFlags > 0) {
140
+ comp._alignFlags = alignFlags;
29
141
  }
142
+ }
143
+
144
+ // 通用属性映射
145
+ const propMap = {
146
+ // Sprite
147
+ 'sizeMode': '_sizeMode',
148
+ 'fillType': '_fillType',
149
+ 'fillCenter': '_fillCenter',
150
+ 'fillStart': '_fillStart',
151
+ 'fillRange': '_fillRange',
152
+ 'trim': '_isTrimmedMode',
153
+
154
+ // Label
155
+ 'string': '_string',
156
+ 'fontSize': '_fontSize',
157
+ 'lineHeight': '_lineHeight',
158
+ 'horizontalAlign': '_N$horizontalAlign',
159
+ 'verticalAlign': '_N$verticalAlign',
160
+ 'overflow': '_N$overflow',
161
+ 'fontFamily': '_N$fontFamily',
162
+ 'wrap': '_enableWrapText',
30
163
 
31
- // 创建预制体数据
32
- const data = createPrefab(rootName);
164
+ // Widget
165
+ 'alignFlags': '_alignFlags',
166
+ 'left': '_left',
167
+ 'right': '_right',
168
+ 'top': '_top',
169
+ 'bottom': '_bottom',
170
+ 'horizontalCenter': '_horizontalCenter',
171
+ 'verticalCenter': '_verticalCenter',
172
+ 'isAbsLeft': '_isAbsLeft',
173
+ 'isAbsRight': '_isAbsRight',
174
+ 'isAbsTop': '_isAbsTop',
175
+ 'isAbsBottom': '_isAbsBottom',
33
176
 
34
- // 保存文件
35
- saveScene(prefabPath, data);
177
+ // Button
178
+ 'interactable': '_N$interactable',
179
+ 'transition': '_N$transition',
180
+ 'zoomScale': 'zoomScale',
181
+ 'duration': 'duration',
36
182
 
183
+ // Layout
184
+ 'layoutType': '_N$layoutType',
185
+ 'cellSize': '_N$cellSize',
186
+ 'startAxis': '_N$startAxis',
187
+ 'paddingLeft': '_N$paddingLeft',
188
+ 'paddingRight': '_N$paddingRight',
189
+ 'paddingTop': '_N$paddingTop',
190
+ 'paddingBottom': '_N$paddingBottom',
191
+ 'spacingX': '_N$spacingX',
192
+ 'spacingY': '_N$spacingY',
193
+ 'resize': '_resize',
194
+
195
+ // Canvas
196
+ 'designResolution': '_designResolution',
197
+ 'fitWidth': '_fitWidth',
198
+ 'fitHeight': '_fitHeight',
199
+
200
+ // Camera
201
+ 'orthoSize': '_orthoSize',
202
+ 'backgroundColor': '_backgroundColor',
203
+ 'cullingMask': '_cullingMask',
204
+ 'zoomRatio': '_zoomRatio'
205
+ };
206
+
207
+ // 特殊处理:label 的 string 属性需要同时设置 _N$string
208
+ if (type === 'cc.Label' && props.string !== undefined) {
209
+ comp._N$string = props.string;
210
+ }
211
+
212
+ // 应用属性
213
+ for (const [key, value] of Object.entries(props)) {
214
+ const compKey = propMap[key];
215
+ if (compKey) {
216
+ // 处理特殊类型
217
+ if (key === 'fillCenter' && Array.isArray(value)) {
218
+ comp[compKey] = { "__type__": "cc.Vec2", "x": value[0], "y": value[1] };
219
+ } else if (key === 'cellSize' && Array.isArray(value)) {
220
+ comp[compKey] = { "__type__": "cc.Size", "width": value[0], "height": value[1] };
221
+ } else if (key === 'designResolution' && Array.isArray(value)) {
222
+ comp[compKey] = { "__type__": "cc.Size", "width": value[0], "height": value[1] };
223
+ } else if ((key === 'backgroundColor' || key === 'color') && typeof value === 'string') {
224
+ const color = parseColor(value);
225
+ if (color) {
226
+ comp[compKey] = { "__type__": "cc.Color", ...color };
227
+ }
228
+ } else {
229
+ comp[compKey] = value;
230
+ }
231
+ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * 从 JSON 定义创建预制体数据
237
+ */
238
+ function createPrefabData(nodeDef) {
239
+ const data = [];
240
+
241
+ // 索引 0: cc.Prefab
242
+ data.push({
243
+ "__type__": "cc.Prefab",
244
+ "_name": "",
245
+ "_objFlags": 0,
246
+ "_native": "",
247
+ "data": { "__id__": 1 },
248
+ "optimizationPolicy": 0,
249
+ "asyncLoadAssets": false,
250
+ "readonly": false
251
+ });
252
+
253
+ const prefabInfoList = [];
254
+
255
+ function addNode(def, parentIndex) {
256
+ const nodeIndex = data.length;
257
+ const uuid = generateUUID();
258
+ const fileId = generateFileId();
259
+
260
+ // 解析组件
261
+ const compList = (def.components || [])
262
+ .map(parseComponent)
263
+ .filter(Boolean);
264
+
265
+ // 解析颜色
266
+ let color = { "__type__": "cc.Color", "r": 255, "g": 255, "b": 255, "a": 255 };
267
+ if (def.color) {
268
+ const parsed = parseColor(def.color);
269
+ if (parsed) {
270
+ color = { "__type__": "cc.Color", ...parsed };
271
+ }
272
+ }
273
+
274
+ // 解析尺寸
275
+ const width = def.width || 0;
276
+ const height = def.height || 0;
277
+
278
+ // 解析锚点
279
+ const anchorX = def.anchorX !== undefined ? def.anchorX : 0.5;
280
+ const anchorY = def.anchorY !== undefined ? def.anchorY : 0.5;
281
+
282
+ // 解析旋转和缩放
283
+ const rotation = def.rotation || 0;
284
+ const scaleX = def.scaleX !== undefined ? def.scaleX : 1;
285
+ const scaleY = def.scaleY !== undefined ? def.scaleY : 1;
286
+
287
+ // 创建节点
288
+ const node = {
289
+ "__type__": "cc.Node",
290
+ "_name": def.name || 'Node',
291
+ "_objFlags": 0,
292
+ "_parent": parentIndex === null ? null : { "__id__": parentIndex },
293
+ "_children": [],
294
+ "_active": def.active !== false,
295
+ "_components": [],
296
+ "_prefab": null,
297
+ "_opacity": def.opacity !== undefined ? def.opacity : 255,
298
+ "_color": color,
299
+ "_contentSize": {
300
+ "__type__": "cc.Size",
301
+ width,
302
+ height
303
+ },
304
+ "_anchorPoint": { "__type__": "cc.Vec2", "x": anchorX, "y": anchorY },
305
+ "_trs": {
306
+ "__type__": "TypedArray",
307
+ "ctor": "Float64Array",
308
+ "array": [
309
+ def.x || 0,
310
+ def.y || 0,
311
+ 0, // z
312
+ 0, // quat x
313
+ 0, // quat y
314
+ rotation * Math.PI / 180, // quat z (rotation in radians)
315
+ 1, // quat w
316
+ scaleX,
317
+ scaleY,
318
+ 1 // scale z
319
+ ]
320
+ },
321
+ "_eulerAngles": { "__type__": "cc.Vec3", "x": 0, "y": 0, "z": rotation },
322
+ "_skewX": 0,
323
+ "_skewY": 0,
324
+ "_is3DNode": false,
325
+ "_groupIndex": 0,
326
+ "groupIndex": 0,
327
+ "_id": uuid
328
+ };
329
+
330
+ data.push(node);
331
+
332
+ // 记录 PrefabInfo
333
+ prefabInfoList.push({
334
+ nodeIndex,
335
+ prefabInfo: {
336
+ "__type__": "cc.PrefabInfo",
337
+ "root": { "__id__": 1 },
338
+ "asset": { "__id__": 0 },
339
+ "fileId": fileId,
340
+ "sync": false
341
+ }
342
+ });
343
+
344
+ // 添加组件
345
+ for (const { type, props } of compList) {
346
+ if (Components[type]) {
347
+ const comp = Components[type](nodeIndex);
348
+ // 应用组件属性
349
+ applyComponentProps(comp, props, node);
350
+ const compIndex = data.length;
351
+ data.push(comp);
352
+ node._components.push({ "__id__": compIndex });
353
+ }
354
+ }
355
+
356
+ // 更新父节点
357
+ if (parentIndex !== null) {
358
+ data[parentIndex]._children.push({ "__id__": nodeIndex });
359
+ }
360
+
361
+ // 递归处理子节点
362
+ if (def.children && def.children.length > 0) {
363
+ for (const child of def.children) {
364
+ addNode(child, nodeIndex);
365
+ }
366
+ }
367
+
368
+ return nodeIndex;
369
+ }
370
+
371
+ // 添加根节点
372
+ addNode(nodeDef, null);
373
+
374
+ // 添加所有 PrefabInfo
375
+ for (const { nodeIndex, prefabInfo } of prefabInfoList) {
376
+ data.push(prefabInfo);
377
+ data[nodeIndex]._prefab = { "__id__": data.length - 1 };
378
+ }
379
+
380
+ return data;
381
+ }
382
+
383
+ function run(args) {
384
+ if (args.length < 1) {
37
385
  console.log(JSON.stringify({
38
- success: true,
39
- path: prefabPath,
40
- rootName: rootName,
41
- message: `预制体创建成功: ${rootName}`
386
+ error: '用法: cocos2d-cli create-prefab <输出路径.prefab>',
387
+ hint: '从 stdin 读取 JSON 结构生成预制体',
388
+ example: 'type panel.json | cocos2d-cli create-prefab assets/panel.prefab'
42
389
  }));
390
+ return;
391
+ }
392
+
393
+ const outputPath = args[0];
394
+
395
+ // 从 stdin 读取 JSON
396
+ let input = '';
397
+
398
+ if (!process.stdin.isTTY) {
399
+ input = fs.readFileSync(0, 'utf8');
400
+ }
401
+
402
+ // 没有 stdin 输入时创建空预制体
403
+ if (!input.trim()) {
404
+ const prefabName = path.basename(outputPath, '.prefab');
43
405
 
406
+ try {
407
+ if (fs.existsSync(outputPath)) {
408
+ console.log(JSON.stringify({ error: `文件已存在: ${outputPath}` }));
409
+ return;
410
+ }
411
+
412
+ const dir = path.dirname(outputPath);
413
+ if (!fs.existsSync(dir)) {
414
+ fs.mkdirSync(dir, { recursive: true });
415
+ }
416
+
417
+ const data = createPrefab(prefabName);
418
+ fs.writeFileSync(outputPath, JSON.stringify(data, null, 2), 'utf8');
419
+
420
+ console.log(JSON.stringify({
421
+ success: true,
422
+ path: outputPath,
423
+ rootName: prefabName
424
+ }));
425
+ return;
426
+ } catch (err) {
427
+ console.log(JSON.stringify({ error: err.message }));
428
+ return;
429
+ }
430
+ }
431
+
432
+ try {
433
+ // 移除 BOM 并解析 JSON
434
+ input = input.replace(/^\uFEFF/, '').trim();
435
+ const nodeDef = JSON.parse(input);
436
+
437
+ // 支持数组格式(第一个作为根节点)
438
+ const rootNode = Array.isArray(nodeDef) ? nodeDef[0] : nodeDef;
439
+
440
+ // 生成预制体
441
+ const prefabData = createPrefabData(rootNode);
442
+
443
+ // 确保目录存在
444
+ const outputDir = path.dirname(outputPath);
445
+ if (!fs.existsSync(outputDir)) {
446
+ fs.mkdirSync(outputDir, { recursive: true });
447
+ }
448
+
449
+ // 保存
450
+ fs.writeFileSync(outputPath, JSON.stringify(prefabData, null, 2), 'utf8');
451
+
452
+ // 统计
453
+ let nodeCount = 0, compCount = 0;
454
+ for (const item of prefabData) {
455
+ if (item.__type__ === 'cc.Node') nodeCount++;
456
+ else if (item.__type__?.startsWith('cc.') && !['cc.Prefab', 'cc.PrefabInfo'].includes(item.__type__)) {
457
+ compCount++;
458
+ }
459
+ }
460
+
461
+ console.log(JSON.stringify({
462
+ success: true,
463
+ path: outputPath,
464
+ nodes: nodeCount,
465
+ components: compCount
466
+ }));
467
+
44
468
  } catch (err) {
45
469
  console.log(JSON.stringify({ error: err.message }));
46
470
  }