lanhu-layer-tree 1.0.0
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.
- package/.env.example +11 -0
- package/README.md +317 -0
- package/dist/assets.d.ts +38 -0
- package/dist/assets.d.ts.map +1 -0
- package/dist/assets.js +247 -0
- package/dist/assets.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +322 -0
- package/dist/cli.js.map +1 -0
- package/dist/client.d.ts +44 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +441 -0
- package/dist/client.js.map +1 -0
- package/dist/cookie.d.ts +32 -0
- package/dist/cookie.d.ts.map +1 -0
- package/dist/cookie.js +177 -0
- package/dist/cookie.js.map +1 -0
- package/dist/formatter.d.ts +6 -0
- package/dist/formatter.d.ts.map +1 -0
- package/dist/formatter.js +225 -0
- package/dist/formatter.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/merger.d.ts +27 -0
- package/dist/merger.d.ts.map +1 -0
- package/dist/merger.js +141 -0
- package/dist/merger.js.map +1 -0
- package/dist/resolver.d.ts +27 -0
- package/dist/resolver.d.ts.map +1 -0
- package/dist/resolver.js +196 -0
- package/dist/resolver.js.map +1 -0
- package/dist/types.d.ts +85 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/utils.d.ts +23 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +106 -0
- package/dist/utils.js.map +1 -0
- package/package.json +38 -0
- package/src/assets.ts +221 -0
- package/src/cli.ts +333 -0
- package/src/client.ts +490 -0
- package/src/cookie.ts +156 -0
- package/src/formatter.ts +251 -0
- package/src/index.ts +4 -0
- package/src/merger.ts +154 -0
- package/src/resolver.ts +195 -0
- package/src/types.ts +94 -0
- package/src/utils.ts +120 -0
- package/tsconfig.json +20 -0
package/src/formatter.ts
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { Layer, TreeFormatOptions } from './types';
|
|
2
|
+
import { convertToEnginePos } from './utils';
|
|
3
|
+
import {
|
|
4
|
+
resolveLayerAssetsUnity,
|
|
5
|
+
resolveLayerAssetsCocos,
|
|
6
|
+
getAssetInfoForLayer,
|
|
7
|
+
AssetInfo
|
|
8
|
+
} from './resolver';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 翻转同级节点的顺序(保持父子关系不变)
|
|
12
|
+
* 游戏引擎中节点展示逻辑跟 PS 中是相反的,位于树下方的在展示时靠前
|
|
13
|
+
*/
|
|
14
|
+
function reverseSiblingOrder(layers: Layer[]): Layer[] {
|
|
15
|
+
if (!layers || layers.length === 0) return [];
|
|
16
|
+
|
|
17
|
+
// 分组:按父路径分组同级节点
|
|
18
|
+
const siblingsMap: Record<string, Layer[]> = {};
|
|
19
|
+
layers.forEach(layer => {
|
|
20
|
+
const path = layer.path;
|
|
21
|
+
const parentPath = path.includes('/') ? path.split('/').slice(0, -1).join('/') : '';
|
|
22
|
+
if (!siblingsMap[parentPath]) {
|
|
23
|
+
siblingsMap[parentPath] = [];
|
|
24
|
+
}
|
|
25
|
+
siblingsMap[parentPath].push(layer);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// 翻转每组同级节点
|
|
29
|
+
Object.keys(siblingsMap).forEach(parentPath => {
|
|
30
|
+
siblingsMap[parentPath].reverse();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// 重新构建列表(深度优先遍历)
|
|
34
|
+
const result: Layer[] = [];
|
|
35
|
+
const visited = new Set<string>();
|
|
36
|
+
|
|
37
|
+
function dfs(parentPath: string) {
|
|
38
|
+
if (!siblingsMap[parentPath]) return;
|
|
39
|
+
siblingsMap[parentPath].forEach(layer => {
|
|
40
|
+
const path = layer.path;
|
|
41
|
+
if (visited.has(path)) return;
|
|
42
|
+
visited.add(path);
|
|
43
|
+
result.push(layer);
|
|
44
|
+
dfs(path);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
dfs('');
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const GROUP_TYPES = new Set(['group', 'layerGroup', 'artboard', 'layerSection']);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 将图层列表格式化为树形结构
|
|
56
|
+
*/
|
|
57
|
+
export function formatLayersTree(
|
|
58
|
+
layers: Layer[],
|
|
59
|
+
designName: string = 'Root',
|
|
60
|
+
canvasWidth: number = 0,
|
|
61
|
+
canvasHeight: number = 0,
|
|
62
|
+
options: TreeFormatOptions = {}
|
|
63
|
+
): string {
|
|
64
|
+
if (!layers || layers.length === 0) return '';
|
|
65
|
+
|
|
66
|
+
// 解析资源 UUID(如果指定了引擎和资源目录)
|
|
67
|
+
let uuidMap: Record<string, AssetInfo> = {};
|
|
68
|
+
if (options.engine && options.assetsDir) {
|
|
69
|
+
console.log(`\n🔍 开始解析 ${options.engine.toUpperCase()} 资源 UUID...`);
|
|
70
|
+
if (options.engine === 'unity') {
|
|
71
|
+
uuidMap = resolveLayerAssetsUnity(layers, options.assetsDir);
|
|
72
|
+
} else if (options.engine === 'cocos') {
|
|
73
|
+
uuidMap = resolveLayerAssetsCocos(layers, options.assetsDir);
|
|
74
|
+
}
|
|
75
|
+
console.log(`✅ 解析完成,共找到 ${Object.keys(uuidMap).length} 个资源\n`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const lines: string[] = [];
|
|
79
|
+
let textLayerIndex = 1;
|
|
80
|
+
|
|
81
|
+
// 过滤掉不需要导出的图层
|
|
82
|
+
const filteredLayers = layers.filter(layer => {
|
|
83
|
+
const name = layer.name.trim();
|
|
84
|
+
return !name.startsWith('参考图') && !name.startsWith('fe');
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// 翻转同级节点顺序
|
|
88
|
+
const reversedLayers = reverseSiblingOrder(filteredLayers);
|
|
89
|
+
|
|
90
|
+
// 添加树根
|
|
91
|
+
lines.push(designName);
|
|
92
|
+
|
|
93
|
+
// 父节点栈:[(depth, abs_x, abs_y, width, height), ...]
|
|
94
|
+
const parentStack: Array<[number, number, number, number, number]> = [];
|
|
95
|
+
|
|
96
|
+
reversedLayers.forEach((layer, idx) => {
|
|
97
|
+
const depth = layer.depth;
|
|
98
|
+
let name = layer.name;
|
|
99
|
+
|
|
100
|
+
// 过滤掉名称中的"拷贝"及其后面的数字部分
|
|
101
|
+
name = name.replace(/\s*拷贝\s*\d*/g, '').trim();
|
|
102
|
+
|
|
103
|
+
// 提取九宫格参数(如 " s9-3-3" → [3,3])
|
|
104
|
+
let s9Params: [number, number] | null = null;
|
|
105
|
+
const s9Match = name.match(/\s+s9-(\d+)-(\d+)/);
|
|
106
|
+
if (s9Match) {
|
|
107
|
+
s9Params = [parseInt(s9Match[1]), parseInt(s9Match[2])];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 过滤掉九宫格标记
|
|
111
|
+
name = name.replace(/\s+s9-\d+-\d+/g, '').trim();
|
|
112
|
+
|
|
113
|
+
const ltype = layer.type;
|
|
114
|
+
const width = layer.width;
|
|
115
|
+
const height = layer.height;
|
|
116
|
+
const absX = layer.x;
|
|
117
|
+
const absY = layer.y;
|
|
118
|
+
|
|
119
|
+
// 维护父节点栈
|
|
120
|
+
while (parentStack.length > 0 && parentStack[parentStack.length - 1][0] >= depth) {
|
|
121
|
+
parentStack.pop();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// 计算相对于父节点的坐标
|
|
125
|
+
let relX: number, relY: number;
|
|
126
|
+
let parentW: number, parentH: number;
|
|
127
|
+
|
|
128
|
+
if (parentStack.length > 0) {
|
|
129
|
+
const [, parentAbsX, parentAbsY, pW, pH] = parentStack[parentStack.length - 1];
|
|
130
|
+
relX = absX - parentAbsX;
|
|
131
|
+
relY = absY - parentAbsY;
|
|
132
|
+
parentW = pW;
|
|
133
|
+
parentH = pH;
|
|
134
|
+
} else {
|
|
135
|
+
parentW = canvasWidth;
|
|
136
|
+
parentH = canvasHeight;
|
|
137
|
+
relX = absX;
|
|
138
|
+
relY = absY;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 坐标系转换
|
|
142
|
+
let x: number, y: number;
|
|
143
|
+
if (options.engine && canvasWidth && canvasHeight) {
|
|
144
|
+
[x, y] = convertToEnginePos(relX, relY, width, height, parentW, parentH);
|
|
145
|
+
} else {
|
|
146
|
+
x = relX;
|
|
147
|
+
y = relY;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// 将当前节点加入父节点栈
|
|
151
|
+
parentStack.push([depth, absX, absY, width, height]);
|
|
152
|
+
|
|
153
|
+
// 查找资源 UUID(仅对非文本、非组图层)
|
|
154
|
+
let assetInfo: AssetInfo | null = null;
|
|
155
|
+
if (ltype !== 'textLayer' && !GROUP_TYPES.has(ltype)) {
|
|
156
|
+
assetInfo = getAssetInfoForLayer(layer.name, uuidMap);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 构建图层描述
|
|
160
|
+
let desc: string;
|
|
161
|
+
|
|
162
|
+
if (ltype === 'textLayer') {
|
|
163
|
+
// 文本图层
|
|
164
|
+
const textStyle = layer.text_style || {};
|
|
165
|
+
const styleParts: string[] = [];
|
|
166
|
+
if (textStyle.fill_color) styleParts.push(textStyle.fill_color);
|
|
167
|
+
if (textStyle.font_size) styleParts.push(String(Math.round(textStyle.font_size)));
|
|
168
|
+
if (textStyle.font_name) styleParts.push(textStyle.font_name);
|
|
169
|
+
if (textStyle.stroke_color) styleParts.push(textStyle.stroke_color);
|
|
170
|
+
if (textStyle.stroke_width) styleParts.push(String(Math.round(textStyle.stroke_width)));
|
|
171
|
+
|
|
172
|
+
desc = `txt T${textLayerIndex} ${name} pos[${x},${y}] size[${width},${height}]`;
|
|
173
|
+
if (styleParts.length > 0) {
|
|
174
|
+
desc += ` style[${styleParts.join(',')}]`;
|
|
175
|
+
}
|
|
176
|
+
textLayerIndex++;
|
|
177
|
+
} else if (GROUP_TYPES.has(ltype)) {
|
|
178
|
+
// 组图层
|
|
179
|
+
desc = `gp ${name} pos[${x},${y}] size[${width},${height}]`;
|
|
180
|
+
} else {
|
|
181
|
+
// 普通图层
|
|
182
|
+
desc = `sp ${name}`;
|
|
183
|
+
if (assetInfo?.guid) {
|
|
184
|
+
desc += ` uuid[${assetInfo.guid}]`;
|
|
185
|
+
} else if (assetInfo?.uuid) {
|
|
186
|
+
desc += ` uuid[${assetInfo.uuid}]`;
|
|
187
|
+
}
|
|
188
|
+
if (assetInfo?.sliced) {
|
|
189
|
+
// 优先使用图层名称中的 s9 参数
|
|
190
|
+
if (s9Params) {
|
|
191
|
+
desc += ` s9[${s9Params[0]},${s9Params[1]}]`;
|
|
192
|
+
} else if (assetInfo.border) {
|
|
193
|
+
// border 格式: [left, bottom, right, top]
|
|
194
|
+
const [left, bottom] = assetInfo.border;
|
|
195
|
+
desc += ` s9[${left},${bottom}]`;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
desc += ` pos[${x},${y}] size[${width},${height}]`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 检查是否是最后一个子节点
|
|
202
|
+
let isLastChild = true;
|
|
203
|
+
for (let nextIdx = idx + 1; nextIdx < reversedLayers.length; nextIdx++) {
|
|
204
|
+
const nextDepth = reversedLayers[nextIdx].depth;
|
|
205
|
+
if (nextDepth <= depth) {
|
|
206
|
+
if (nextDepth === depth) {
|
|
207
|
+
isLastChild = false;
|
|
208
|
+
}
|
|
209
|
+
break;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// 构建前缀
|
|
214
|
+
const prefixParts: string[] = [];
|
|
215
|
+
|
|
216
|
+
for (let d = 0; d <= depth; d++) {
|
|
217
|
+
if (d === depth) {
|
|
218
|
+
prefixParts.push(isLastChild ? '└── ' : '├── ');
|
|
219
|
+
} else {
|
|
220
|
+
// 找到深度为 d 的最近祖先节点
|
|
221
|
+
let ancestorIdx: number | null = null;
|
|
222
|
+
for (let prevIdx = idx - 1; prevIdx >= 0; prevIdx--) {
|
|
223
|
+
if (reversedLayers[prevIdx].depth === d) {
|
|
224
|
+
ancestorIdx = prevIdx;
|
|
225
|
+
break;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 检查该祖先节点是否还有后续同级兄弟
|
|
230
|
+
let hasSibling = false;
|
|
231
|
+
if (ancestorIdx !== null) {
|
|
232
|
+
for (let checkIdx = ancestorIdx + 1; checkIdx < reversedLayers.length; checkIdx++) {
|
|
233
|
+
const checkDepth = reversedLayers[checkIdx].depth;
|
|
234
|
+
if (checkDepth <= d) {
|
|
235
|
+
if (checkDepth === d) {
|
|
236
|
+
hasSibling = true;
|
|
237
|
+
}
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
prefixParts.push(hasSibling ? '│ ' : ' ');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
lines.push(prefixParts.join('') + desc);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
return lines.join('\n');
|
|
251
|
+
}
|
package/src/index.ts
ADDED
package/src/merger.ts
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 多 URL 图层合并 - 相似度计算 + 合并
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Layer } from './types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 标准化路径:移除 #ID 后缀
|
|
9
|
+
* 用于支持同名图层匹配
|
|
10
|
+
*/
|
|
11
|
+
function normalizePath(path: string): string {
|
|
12
|
+
return path
|
|
13
|
+
.split('/')
|
|
14
|
+
.map(part => (part.includes('#') ? part.split('#')[0] : part))
|
|
15
|
+
.join('/');
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 计算两个图层列表的相似度(0-100)
|
|
20
|
+
*
|
|
21
|
+
* 计算依据:
|
|
22
|
+
* 1. 路径匹配(去除 #ID 后缀的标准化路径)
|
|
23
|
+
* 2. 类型一致性(权重 40%)
|
|
24
|
+
* 3. 名称相似度(权重 30%,完全相等或字符重合度)
|
|
25
|
+
* 4. 尺寸接近度(权重 30%)
|
|
26
|
+
*/
|
|
27
|
+
export function calculateLayerSimilarity(layers1: Layer[], layers2: Layer[]): number {
|
|
28
|
+
if (!layers1 || !layers2 || layers1.length === 0 || layers2.length === 0) {
|
|
29
|
+
return 0;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const map1 = new Map<string, Layer[]>();
|
|
33
|
+
for (const layer of layers1) {
|
|
34
|
+
const key = normalizePath(layer.path || '');
|
|
35
|
+
if (!map1.has(key)) map1.set(key, []);
|
|
36
|
+
map1.get(key)!.push(layer);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const map2 = new Map<string, Layer[]>();
|
|
40
|
+
for (const layer of layers2) {
|
|
41
|
+
const key = normalizePath(layer.path || '');
|
|
42
|
+
if (!map2.has(key)) map2.set(key, []);
|
|
43
|
+
map2.get(key)!.push(layer);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
let matchedCount = 0;
|
|
47
|
+
let totalScore = 0;
|
|
48
|
+
|
|
49
|
+
for (const [normPath, list1] of map1) {
|
|
50
|
+
const list2 = map2.get(normPath);
|
|
51
|
+
if (!list2) continue;
|
|
52
|
+
|
|
53
|
+
for (const layer1 of list1) {
|
|
54
|
+
let bestScore = 0;
|
|
55
|
+
for (const layer2 of list2) {
|
|
56
|
+
let score = 0;
|
|
57
|
+
|
|
58
|
+
// 类型匹配(权重 40%)
|
|
59
|
+
if ((layer1.type || '') === (layer2.type || '')) {
|
|
60
|
+
score += 0.4;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 名称匹配(权重 30%)
|
|
64
|
+
const name1 = layer1.name || '';
|
|
65
|
+
const name2 = layer2.name || '';
|
|
66
|
+
if (name1 && name1 === name2) {
|
|
67
|
+
score += 0.3;
|
|
68
|
+
} else if (name1 && name2) {
|
|
69
|
+
let common = 0;
|
|
70
|
+
for (const c of name1) {
|
|
71
|
+
if (name2.includes(c)) common++;
|
|
72
|
+
}
|
|
73
|
+
score += 0.3 * (common / Math.max(name1.length, name2.length));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 尺寸接近度(权重 30%)
|
|
77
|
+
const w1 = layer1.width || 0;
|
|
78
|
+
const h1 = layer1.height || 0;
|
|
79
|
+
const w2 = layer2.width || 0;
|
|
80
|
+
const h2 = layer2.height || 0;
|
|
81
|
+
if (w1 > 0 && h1 > 0 && w2 > 0 && h2 > 0) {
|
|
82
|
+
const wRatio = Math.min(w1, w2) / Math.max(w1, w2);
|
|
83
|
+
const hRatio = Math.min(h1, h2) / Math.max(h1, h2);
|
|
84
|
+
score += 0.3 * ((wRatio + hRatio) / 2);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (score > bestScore) bestScore = score;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (bestScore > 0.5) {
|
|
91
|
+
matchedCount++;
|
|
92
|
+
totalScore += bestScore;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const totalLayers = Math.max(layers1.length, layers2.length);
|
|
98
|
+
if (totalLayers === 0) return 0;
|
|
99
|
+
|
|
100
|
+
const matchRate = matchedCount / totalLayers;
|
|
101
|
+
const avgQuality = matchedCount > 0 ? totalScore / matchedCount : 0;
|
|
102
|
+
|
|
103
|
+
return Math.round(matchRate * avgQuality * 100 * 100) / 100;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* 合并多个图层列表
|
|
108
|
+
*
|
|
109
|
+
* 策略:
|
|
110
|
+
* 1. 以第一个图层列表为基准
|
|
111
|
+
* 2. 后续列表中已存在路径的图层跳过(保留第一个版本)
|
|
112
|
+
* 3. 后续列表中独有的图层附加,并在名称后追加 (from <设计名>) 标记来源
|
|
113
|
+
*/
|
|
114
|
+
export function mergeLayers(
|
|
115
|
+
layersList: Layer[][],
|
|
116
|
+
designNames: string[]
|
|
117
|
+
): { layers: Layer[]; name: string } {
|
|
118
|
+
if (!layersList || layersList.length === 0) {
|
|
119
|
+
return { layers: [], name: '' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (layersList.length === 1) {
|
|
123
|
+
return {
|
|
124
|
+
layers: layersList[0],
|
|
125
|
+
name: designNames[0] || 'Root'
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const baseLayers = layersList[0];
|
|
130
|
+
const merged: Layer[] = [...baseLayers];
|
|
131
|
+
|
|
132
|
+
const baseMap = new Set<string>();
|
|
133
|
+
for (const layer of baseLayers) {
|
|
134
|
+
baseMap.add(normalizePath(layer.path || ''));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (let idx = 1; idx < layersList.length; idx++) {
|
|
138
|
+
const layers = layersList[idx];
|
|
139
|
+
const sourceName = designNames[idx] || `Source${idx + 1}`;
|
|
140
|
+
for (const layer of layers) {
|
|
141
|
+
const key = normalizePath(layer.path || '');
|
|
142
|
+
if (baseMap.has(key)) continue;
|
|
143
|
+
const newLayer: Layer = {
|
|
144
|
+
...layer,
|
|
145
|
+
name: `${layer.name || ''} (from ${sourceName})`
|
|
146
|
+
};
|
|
147
|
+
merged.push(newLayer);
|
|
148
|
+
baseMap.add(key);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const mergedName = designNames.length > 0 ? designNames.join(' + ') : 'Merged';
|
|
153
|
+
return { layers: merged, name: mergedName };
|
|
154
|
+
}
|
package/src/resolver.ts
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 资源 UUID 解析:Unity/Cocos
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as fs from 'fs';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
import { Layer } from './types';
|
|
8
|
+
import {
|
|
9
|
+
findLocalAsset,
|
|
10
|
+
readUnityMeta,
|
|
11
|
+
readCocosSpriteUuid,
|
|
12
|
+
makeCocosMeta,
|
|
13
|
+
UnityMetaInfo
|
|
14
|
+
} from './assets';
|
|
15
|
+
|
|
16
|
+
export interface AssetInfo {
|
|
17
|
+
uuid?: string;
|
|
18
|
+
guid?: string;
|
|
19
|
+
sliced: boolean;
|
|
20
|
+
border?: [number, number, number, number] | null;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const SKIP_LAYER_TYPES = new Set([
|
|
24
|
+
'textLayer',
|
|
25
|
+
'group',
|
|
26
|
+
'layerGroup',
|
|
27
|
+
'artboard',
|
|
28
|
+
'layerSection'
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
const CHINESE_REGEX = /[一-鿿]/;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* 从图层名称提取资源文件名
|
|
35
|
+
* 规则:
|
|
36
|
+
* - 去除 "Img_" / "Img" 前缀
|
|
37
|
+
* - 去除 "拷贝" 及后续数字
|
|
38
|
+
* - 去除九宫格标记 " s9-x-y"
|
|
39
|
+
* - 中文命名 / "background" 跳过
|
|
40
|
+
*/
|
|
41
|
+
function extractResourceFilename(name: string): string | null {
|
|
42
|
+
let resourceName = name;
|
|
43
|
+
if (resourceName.startsWith('Img_')) {
|
|
44
|
+
resourceName = resourceName.slice(4);
|
|
45
|
+
} else if (resourceName.startsWith('Img')) {
|
|
46
|
+
resourceName = resourceName.slice(3);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
resourceName = resourceName.replace(/\s*拷贝\s*\d*/g, '').trim();
|
|
50
|
+
resourceName = resourceName.replace(/\s+s9-\d+-\d+/g, '').trim();
|
|
51
|
+
|
|
52
|
+
if (!resourceName) return null;
|
|
53
|
+
if (CHINESE_REGEX.test(resourceName)) return null;
|
|
54
|
+
if (resourceName.toLowerCase() === 'background') return null;
|
|
55
|
+
|
|
56
|
+
return `${resourceName}.png`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 收集图层中所有需要解析的资源文件名
|
|
61
|
+
*/
|
|
62
|
+
function collectImageRefs(layers: Layer[]): Set<string> {
|
|
63
|
+
const refs = new Set<string>();
|
|
64
|
+
for (const layer of layers) {
|
|
65
|
+
const name = layer.name || '';
|
|
66
|
+
const ltype = layer.type || '';
|
|
67
|
+
|
|
68
|
+
if (SKIP_LAYER_TYPES.has(ltype)) continue;
|
|
69
|
+
if (name.startsWith('参考图') || name.startsWith('fe')) continue;
|
|
70
|
+
|
|
71
|
+
const filename = extractResourceFilename(name);
|
|
72
|
+
if (filename) refs.add(filename);
|
|
73
|
+
}
|
|
74
|
+
return refs;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 批量解析图层中的图片资源 GUID(Unity)
|
|
79
|
+
*/
|
|
80
|
+
export function resolveLayerAssetsUnity(
|
|
81
|
+
layers: Layer[],
|
|
82
|
+
assetsDir: string
|
|
83
|
+
): Record<string, AssetInfo> {
|
|
84
|
+
if (!assetsDir || !fs.existsSync(assetsDir)) return {};
|
|
85
|
+
|
|
86
|
+
const imageRefs = collectImageRefs(layers);
|
|
87
|
+
const uuidMap: Record<string, AssetInfo> = {};
|
|
88
|
+
const missingFiles: string[] = [];
|
|
89
|
+
|
|
90
|
+
for (const filename of imageRefs) {
|
|
91
|
+
const local = findLocalAsset(filename, assetsDir);
|
|
92
|
+
if (!local) {
|
|
93
|
+
missingFiles.push(filename);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const metaPath = `${local}.meta`;
|
|
98
|
+
if (fs.existsSync(metaPath)) {
|
|
99
|
+
const info = readUnityMeta(metaPath);
|
|
100
|
+
if (info) {
|
|
101
|
+
uuidMap[filename] = {
|
|
102
|
+
guid: info.guid,
|
|
103
|
+
sliced: info.sliced,
|
|
104
|
+
border: info.border
|
|
105
|
+
};
|
|
106
|
+
const slicedTag = info.sliced ? '九宫格' : '普通';
|
|
107
|
+
console.log(` [复用] ${filename} guid=${info.guid.slice(0, 8)}... ${slicedTag}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (missingFiles.length > 0) {
|
|
113
|
+
console.error(`\n❌ 错误:以下 ${missingFiles.length} 个资源在本地未找到:`);
|
|
114
|
+
for (const f of missingFiles.sort()) {
|
|
115
|
+
console.error(` - ${f}`);
|
|
116
|
+
}
|
|
117
|
+
console.error(`\n请确保资源文件存在于目录:${assetsDir}`);
|
|
118
|
+
throw new Error(`缺失 ${missingFiles.length} 个资源文件`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return uuidMap;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 批量解析图层中的图片资源 UUID(Cocos)
|
|
126
|
+
*
|
|
127
|
+
* 仅处理本地已存在的资源:
|
|
128
|
+
* - 有 .meta 文件 → 读取 UUID
|
|
129
|
+
* - 无 .meta 文件 → 生成 .meta 并分配新 UUID
|
|
130
|
+
*/
|
|
131
|
+
export function resolveLayerAssetsCocos(
|
|
132
|
+
layers: Layer[],
|
|
133
|
+
assetsDir: string
|
|
134
|
+
): Record<string, AssetInfo> {
|
|
135
|
+
if (!assetsDir || !fs.existsSync(assetsDir)) return {};
|
|
136
|
+
|
|
137
|
+
const imageRefs = collectImageRefs(layers);
|
|
138
|
+
const uuidMap: Record<string, AssetInfo> = {};
|
|
139
|
+
const missingFiles: string[] = [];
|
|
140
|
+
|
|
141
|
+
for (const filename of imageRefs) {
|
|
142
|
+
const local = findLocalAsset(filename, assetsDir);
|
|
143
|
+
if (!local) {
|
|
144
|
+
missingFiles.push(filename);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const metaPath = `${local}.meta`;
|
|
149
|
+
|
|
150
|
+
// 已有 .meta 文件
|
|
151
|
+
if (fs.existsSync(metaPath)) {
|
|
152
|
+
const uid = readCocosSpriteUuid(metaPath);
|
|
153
|
+
if (uid) {
|
|
154
|
+
uuidMap[filename] = { uuid: uid, sliced: false };
|
|
155
|
+
console.log(` [复用] ${filename} uuid=${uid.slice(0, 8)}...`);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 无 .meta 文件 → 生成新的 .meta
|
|
161
|
+
try {
|
|
162
|
+
const meta = makeCocosMeta(local);
|
|
163
|
+
fs.writeFileSync(metaPath, JSON.stringify(meta, null, 2), 'utf-8');
|
|
164
|
+
const subKeys = Object.keys(meta.subMetas);
|
|
165
|
+
const uid = meta.subMetas[subKeys[0]].uuid;
|
|
166
|
+
uuidMap[filename] = { uuid: uid, sliced: false };
|
|
167
|
+
console.log(` [生成meta] ${filename} uuid=${uid.slice(0, 8)}...`);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
console.warn(` [警告] 无法为 ${filename} 生成 meta:`, e);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (missingFiles.length > 0) {
|
|
174
|
+
console.error(`\n❌ 错误:以下 ${missingFiles.length} 个资源在本地未找到:`);
|
|
175
|
+
for (const f of missingFiles.sort()) {
|
|
176
|
+
console.error(` - ${f}`);
|
|
177
|
+
}
|
|
178
|
+
console.error(`\n请确保资源文件存在于目录:${assetsDir}`);
|
|
179
|
+
throw new Error(`缺失 ${missingFiles.length} 个资源文件`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return uuidMap;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 根据图层名称取出对应的资源信息
|
|
187
|
+
*/
|
|
188
|
+
export function getAssetInfoForLayer(
|
|
189
|
+
layerName: string,
|
|
190
|
+
uuidMap: Record<string, AssetInfo>
|
|
191
|
+
): AssetInfo | null {
|
|
192
|
+
const filename = extractResourceFilename(layerName);
|
|
193
|
+
if (!filename) return null;
|
|
194
|
+
return uuidMap[filename] || null;
|
|
195
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 蓝湖 API 类型定义
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface LanhuConfig {
|
|
6
|
+
cookie?: string;
|
|
7
|
+
baseUrl?: string;
|
|
8
|
+
timeout?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ParsedUrl {
|
|
12
|
+
team_id: string;
|
|
13
|
+
project_id: string;
|
|
14
|
+
doc_id?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Design {
|
|
18
|
+
index: number;
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
url: string;
|
|
24
|
+
update_time: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DesignsResponse {
|
|
28
|
+
status: string;
|
|
29
|
+
message?: string;
|
|
30
|
+
project_name?: string;
|
|
31
|
+
total_designs?: number;
|
|
32
|
+
designs?: Design[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface Layer {
|
|
36
|
+
name: string;
|
|
37
|
+
type: string;
|
|
38
|
+
width: number;
|
|
39
|
+
height: number;
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
path: string;
|
|
43
|
+
depth: number;
|
|
44
|
+
smart_object?: string;
|
|
45
|
+
text?: string;
|
|
46
|
+
text_style?: TextStyle;
|
|
47
|
+
opacity?: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TextStyle {
|
|
51
|
+
fill_color?: string;
|
|
52
|
+
font_size?: number;
|
|
53
|
+
font_name?: string;
|
|
54
|
+
stroke_color?: string;
|
|
55
|
+
stroke_width?: number;
|
|
56
|
+
shadow_color?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface LayersResponse {
|
|
60
|
+
design_id: string;
|
|
61
|
+
design_name: string;
|
|
62
|
+
version: string;
|
|
63
|
+
canvas_size: {
|
|
64
|
+
width: number;
|
|
65
|
+
height: number;
|
|
66
|
+
};
|
|
67
|
+
total_layers: number;
|
|
68
|
+
layers: Layer[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface Slice {
|
|
72
|
+
name: string;
|
|
73
|
+
download_url: string;
|
|
74
|
+
size: string;
|
|
75
|
+
format: string;
|
|
76
|
+
layer_path: string;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface SlicesResponse {
|
|
80
|
+
design_id: string;
|
|
81
|
+
design_name: string;
|
|
82
|
+
version: string;
|
|
83
|
+
canvas_size: {
|
|
84
|
+
width: number;
|
|
85
|
+
height: number;
|
|
86
|
+
};
|
|
87
|
+
total_slices: number;
|
|
88
|
+
slices: Slice[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface TreeFormatOptions {
|
|
92
|
+
engine?: 'unity' | 'cocos';
|
|
93
|
+
assetsDir?: string;
|
|
94
|
+
}
|