leafer-x-design-system 2.0.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.
- package/LICENSE +21 -0
- package/README.md +300 -0
- package/assets/README.md +16 -0
- package/assets/assetswechat-qr.png +0 -0
- package/assets/icon.svg +14 -0
- package/assets/wechat-pay.png +0 -0
- package/cli.js +163 -0
- package/examples/basic-usage.js +163 -0
- package/examples/express-server.js +279 -0
- package/index.d.ts +232 -0
- package/index.js +106 -0
- package/leafer-ai-layout-plugin-v2.js +370 -0
- package/leafer-ai-layout-plugin.js +341 -0
- package/leafer-design-system-generator.js +1102 -0
- package/leafer-design-system-pro.js +1194 -0
- package/leafer-renderer-fixed.js +661 -0
- package/leafer-renderer-v2.js +791 -0
- package/leafer-x-design-system.js +140 -0
- package/mcp-adapter.js +142 -0
- package/mcp-server-starter.js +85 -0
- package/mcp-server.js +300 -0
- package/mcp-service-config.js +24 -0
- package/package.json +55 -0
- package/start-mcp-service-v2.js +239 -0
- package/start-mcp-service.js +157 -0
|
@@ -0,0 +1,791 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LeaferJS UI 渲染模块 V2 - 基于深度学习的优化版本
|
|
3
|
+
*
|
|
4
|
+
* 基于 @leafer-ui/node 的高性能 UI 渲染引擎
|
|
5
|
+
* 全面支持 LeaferJS 的所有核心功能
|
|
6
|
+
*
|
|
7
|
+
* 新增功能:
|
|
8
|
+
* - 完整的样式系统(渐变、阴影、内阴影)
|
|
9
|
+
* - 高级定位(origin, around, zIndex)
|
|
10
|
+
* - 描边样式(strokeAlign, strokeCap, strokeJoin, dashPattern)
|
|
11
|
+
* - 遮罩和擦除功能
|
|
12
|
+
* - 混合模式
|
|
13
|
+
* - 更好的错误处理
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// 从 h: 驱动器的 mcp-service 加载依赖
|
|
17
|
+
const {
|
|
18
|
+
Leafer, Rect, Ellipse, Line, Polygon, Star, Path, Text, Image,
|
|
19
|
+
Group, Box, Frame, Pen, useCanvas
|
|
20
|
+
} = require('h:/Yj/mcp-service/node_modules/@leafer-ui/node');
|
|
21
|
+
const skia = require('h:/Yj/mcp-service/node_modules/skia-canvas');
|
|
22
|
+
const crypto = require('crypto');
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const path = require('path');
|
|
25
|
+
const { v4: uuidv4 } = require('uuid');
|
|
26
|
+
|
|
27
|
+
// 初始化 Canvas 环境
|
|
28
|
+
console.log('[LeaferRenderer V2] Initializing canvas environment...');
|
|
29
|
+
try {
|
|
30
|
+
useCanvas('skia', skia);
|
|
31
|
+
console.log('[LeaferRenderer V2] ✅ Canvas environment initialized');
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error('[LeaferRenderer V2] ❌ Failed to initialize canvas:', error);
|
|
34
|
+
throw error;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 加载中文字体
|
|
38
|
+
const { FontLibrary } = skia;
|
|
39
|
+
console.log('[LeaferRenderer V2] Loading Chinese fonts...');
|
|
40
|
+
const loadedFonts = [];
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const systemFonts = [
|
|
44
|
+
{ file: 'C:/Windows/Fonts/simhei.ttf', family: 'SimHei' },
|
|
45
|
+
{ file: 'C:/Windows/Fonts/simsunb.ttf', family: 'SimSun' },
|
|
46
|
+
{ file: 'C:/Windows/Fonts/msyh.ttc', family: 'Microsoft YaHei' },
|
|
47
|
+
{ file: 'C:/Windows/Fonts/msyhbd.ttc', family: 'Microsoft YaHei Bold' },
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
for (const font of systemFonts) {
|
|
51
|
+
if (fs.existsSync(font.file)) {
|
|
52
|
+
try {
|
|
53
|
+
FontLibrary.use(font.family, [font.file]);
|
|
54
|
+
loadedFonts.push(font.family);
|
|
55
|
+
console.log(`[LeaferRenderer V2] ✅ Loaded font: ${font.family}`);
|
|
56
|
+
} catch (fontError) {
|
|
57
|
+
console.warn(`[LeaferRenderer V2] ⚠️ Failed to load ${font.family}:`, fontError.message);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
global.CHINESE_FONT_FAMILIES = loadedFonts.length > 0 ? loadedFonts : ['Arial'];
|
|
63
|
+
} catch (error) {
|
|
64
|
+
console.warn('[LeaferRenderer V2] ⚠️ Font loading failed:', error.message);
|
|
65
|
+
global.CHINESE_FONT_FAMILIES = ['Arial'];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
class LeaferRendererV2 {
|
|
69
|
+
constructor(options = {}) {
|
|
70
|
+
console.log('[LeaferRenderer V2] Initializing...');
|
|
71
|
+
this.options = {
|
|
72
|
+
pixelRatio: options.pixelRatio || 2,
|
|
73
|
+
backgroundColor: options.backgroundColor || '#ffffff',
|
|
74
|
+
maxCacheSize: options.maxCacheSize || 100,
|
|
75
|
+
...options,
|
|
76
|
+
};
|
|
77
|
+
this.renderCache = new Map();
|
|
78
|
+
this.outputDir = options.outputDir || path.join(__dirname, 'output');
|
|
79
|
+
|
|
80
|
+
if (!fs.existsSync(this.outputDir)) {
|
|
81
|
+
fs.mkdirSync(this.outputDir, { recursive: true });
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
this.stats = {
|
|
85
|
+
totalRenders: 0,
|
|
86
|
+
cachedRenders: 0,
|
|
87
|
+
errors: 0
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
console.log('[LeaferRenderer V2] ✅ Initialized successfully');
|
|
91
|
+
console.log(`[LeaferRenderer V2] 📁 Output directory: ${this.outputDir}`);
|
|
92
|
+
console.log(`[LeaferRenderer V2] 🔤 Available fonts: ${global.CHINESE_FONT_FAMILIES.join(', ')}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 渲染 UI 元素 - V2 版本
|
|
97
|
+
*/
|
|
98
|
+
async render(config) {
|
|
99
|
+
const startTime = Date.now();
|
|
100
|
+
console.log('[LeaferRenderer V2] 🎨 Starting render...');
|
|
101
|
+
|
|
102
|
+
let { elements, width, height, options = {} } = config;
|
|
103
|
+
|
|
104
|
+
const pixelRatio = options.pixelRatio || this.options.pixelRatio;
|
|
105
|
+
const backgroundColor = options.backgroundColor || this.options.backgroundColor;
|
|
106
|
+
|
|
107
|
+
// 标准化元素格式
|
|
108
|
+
elements = this.normalizeElements(elements);
|
|
109
|
+
|
|
110
|
+
// 检查缓存
|
|
111
|
+
const cacheKey = this.generateCacheKey({ elements, width, height, options });
|
|
112
|
+
const cached = this.getCache(cacheKey);
|
|
113
|
+
if (cached) {
|
|
114
|
+
console.log('[LeaferRenderer V2] 💾 Cache hit!');
|
|
115
|
+
this.stats.cachedRenders++;
|
|
116
|
+
return {
|
|
117
|
+
url: cached.url,
|
|
118
|
+
base64: cached.base64,
|
|
119
|
+
width,
|
|
120
|
+
height,
|
|
121
|
+
format: options.format || 'png',
|
|
122
|
+
pixelRatio,
|
|
123
|
+
cacheKey,
|
|
124
|
+
cached: true,
|
|
125
|
+
renderTime: Date.now() - startTime
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
let leafer = null;
|
|
130
|
+
try {
|
|
131
|
+
// 创建 Leafer 引擎
|
|
132
|
+
leafer = new Leafer({
|
|
133
|
+
width,
|
|
134
|
+
height,
|
|
135
|
+
pixelRatio,
|
|
136
|
+
fill: backgroundColor,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// 等待引擎准备就绪
|
|
140
|
+
await this.waitReady(leafer);
|
|
141
|
+
|
|
142
|
+
// 渲染所有元素
|
|
143
|
+
console.log(`[LeaferRenderer V2] 📦 Rendering ${elements.length} elements...`);
|
|
144
|
+
for (let i = 0; i < elements.length; i++) {
|
|
145
|
+
const element = elements[i];
|
|
146
|
+
try {
|
|
147
|
+
const leaferElement = this.createElement(element);
|
|
148
|
+
if (leaferElement) {
|
|
149
|
+
leafer.add(leaferElement);
|
|
150
|
+
}
|
|
151
|
+
} catch (elementError) {
|
|
152
|
+
console.error(`[LeaferRenderer V2] ❌ Error creating element ${i}:`, elementError.message);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 等待渲染完成
|
|
157
|
+
await this.waitForRender(leafer);
|
|
158
|
+
|
|
159
|
+
// 导出图片
|
|
160
|
+
const format = options.format || 'png';
|
|
161
|
+
const imageBuffer = await this.exportImage(leafer, format);
|
|
162
|
+
|
|
163
|
+
// 保存图片
|
|
164
|
+
const filename = `ui-${uuidv4()}.${format}`;
|
|
165
|
+
const filePath = path.join(this.outputDir, filename);
|
|
166
|
+
fs.writeFileSync(filePath, imageBuffer);
|
|
167
|
+
|
|
168
|
+
// 生成 base64
|
|
169
|
+
const base64Data = imageBuffer.toString('base64');
|
|
170
|
+
const result = {
|
|
171
|
+
url: `/output/${filename}`,
|
|
172
|
+
base64: `data:image/${format};base64,${base64Data}`,
|
|
173
|
+
width,
|
|
174
|
+
height,
|
|
175
|
+
format,
|
|
176
|
+
pixelRatio,
|
|
177
|
+
cacheKey,
|
|
178
|
+
cached: false,
|
|
179
|
+
renderTime: Date.now() - startTime
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
// 缓存结果
|
|
183
|
+
this.setCache(cacheKey, result);
|
|
184
|
+
this.stats.totalRenders++;
|
|
185
|
+
|
|
186
|
+
console.log(`[LeaferRenderer V2] ✅ Render completed in ${result.renderTime}ms`);
|
|
187
|
+
return result;
|
|
188
|
+
|
|
189
|
+
} catch (error) {
|
|
190
|
+
this.stats.errors++;
|
|
191
|
+
console.error('[LeaferRenderer V2] ❌ Render error:', error);
|
|
192
|
+
throw error;
|
|
193
|
+
} finally {
|
|
194
|
+
if (leafer) {
|
|
195
|
+
try {
|
|
196
|
+
leafer.destroy();
|
|
197
|
+
} catch (e) {
|
|
198
|
+
// Ignore destroy errors
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* 标准化元素格式
|
|
206
|
+
*/
|
|
207
|
+
normalizeElements(elements) {
|
|
208
|
+
return elements.map(el => {
|
|
209
|
+
// 处理 style 对象
|
|
210
|
+
if (el.style && typeof el.style === 'object') {
|
|
211
|
+
const { style, ...rest } = el;
|
|
212
|
+
return { ...rest, ...style };
|
|
213
|
+
}
|
|
214
|
+
return el;
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* 等待引擎准备就绪
|
|
220
|
+
*/
|
|
221
|
+
waitReady(leafer) {
|
|
222
|
+
return new Promise((resolve) => {
|
|
223
|
+
if (leafer.ready) {
|
|
224
|
+
resolve();
|
|
225
|
+
} else {
|
|
226
|
+
leafer.once('ready', resolve);
|
|
227
|
+
setTimeout(resolve, 2000);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 等待渲染完成
|
|
234
|
+
*/
|
|
235
|
+
waitForRender(leafer) {
|
|
236
|
+
return new Promise((resolve) => {
|
|
237
|
+
if (leafer.viewReady) {
|
|
238
|
+
setTimeout(resolve, 100);
|
|
239
|
+
} else {
|
|
240
|
+
leafer.once('view.ready', () => setTimeout(resolve, 100));
|
|
241
|
+
setTimeout(resolve, 3000);
|
|
242
|
+
}
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* 导出图片
|
|
248
|
+
*/
|
|
249
|
+
async exportImage(leafer, format) {
|
|
250
|
+
const leaferCanvas = leafer.canvas;
|
|
251
|
+
if (!leaferCanvas || !leaferCanvas.view) {
|
|
252
|
+
throw new Error('Canvas not available');
|
|
253
|
+
}
|
|
254
|
+
return await leaferCanvas.view.toBuffer(`image/${format}`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* 创建元素 - 支持所有 LeaferJS 元素类型
|
|
259
|
+
*/
|
|
260
|
+
createElement(config) {
|
|
261
|
+
const { type, tag, ...props } = config;
|
|
262
|
+
const elementType = type || tag;
|
|
263
|
+
|
|
264
|
+
if (!elementType) {
|
|
265
|
+
console.warn('[LeaferRenderer V2] ⚠️ Element missing type/tag');
|
|
266
|
+
return null;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
switch (elementType.toLowerCase()) {
|
|
271
|
+
case 'rect':
|
|
272
|
+
return this.createRect(props);
|
|
273
|
+
case 'ellipse':
|
|
274
|
+
case 'circle':
|
|
275
|
+
return this.createEllipse(props);
|
|
276
|
+
case 'line':
|
|
277
|
+
return this.createLine(props);
|
|
278
|
+
case 'polygon':
|
|
279
|
+
return this.createPolygon(props);
|
|
280
|
+
case 'star':
|
|
281
|
+
return this.createStar(props);
|
|
282
|
+
case 'path':
|
|
283
|
+
return this.createPath(props);
|
|
284
|
+
case 'pen':
|
|
285
|
+
return this.createPen(props);
|
|
286
|
+
case 'text':
|
|
287
|
+
return this.createText(props);
|
|
288
|
+
case 'image':
|
|
289
|
+
return this.createImage(props);
|
|
290
|
+
case 'group':
|
|
291
|
+
return this.createGroup(props);
|
|
292
|
+
case 'box':
|
|
293
|
+
return this.createBox(props);
|
|
294
|
+
case 'frame':
|
|
295
|
+
return this.createFrame(props);
|
|
296
|
+
default:
|
|
297
|
+
console.warn(`[LeaferRenderer V2] ⚠️ Unknown element type: ${elementType}`);
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
} catch (error) {
|
|
301
|
+
console.error(`[LeaferRenderer V2] ❌ Error creating ${elementType}:`, error.message);
|
|
302
|
+
return null;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* 创建矩形 - 完整样式支持
|
|
308
|
+
*/
|
|
309
|
+
createRect(props) {
|
|
310
|
+
const config = this.buildBaseConfig(props);
|
|
311
|
+
|
|
312
|
+
// 矩形特有属性
|
|
313
|
+
if (props.cornerRadius !== undefined) {
|
|
314
|
+
config.cornerRadius = props.cornerRadius;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return new Rect(config);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* 创建椭圆/圆形 - 支持扇形、圆环
|
|
322
|
+
*/
|
|
323
|
+
createEllipse(props) {
|
|
324
|
+
const config = this.buildBaseConfig(props);
|
|
325
|
+
|
|
326
|
+
// 椭圆特有属性
|
|
327
|
+
if (props.startAngle !== undefined) config.startAngle = props.startAngle;
|
|
328
|
+
if (props.endAngle !== undefined) config.endAngle = props.endAngle;
|
|
329
|
+
if (props.innerRadius !== undefined) config.innerRadius = props.innerRadius;
|
|
330
|
+
|
|
331
|
+
return new Ellipse(config);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* 创建线条 - 支持虚线、曲线
|
|
336
|
+
*/
|
|
337
|
+
createLine(props) {
|
|
338
|
+
const config = this.buildStrokeConfig(props);
|
|
339
|
+
|
|
340
|
+
// 线条特有属性
|
|
341
|
+
if (props.points !== undefined) config.points = props.points;
|
|
342
|
+
if (props.toPoint !== undefined) config.toPoint = props.toPoint;
|
|
343
|
+
if (props.curve !== undefined) config.curve = props.curve;
|
|
344
|
+
if (props.closed !== undefined) config.closed = props.closed;
|
|
345
|
+
|
|
346
|
+
return new Line(config);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* 创建多边形
|
|
351
|
+
*/
|
|
352
|
+
createPolygon(props) {
|
|
353
|
+
const config = this.buildBaseConfig(props);
|
|
354
|
+
|
|
355
|
+
if (props.sides !== undefined) config.sides = props.sides;
|
|
356
|
+
|
|
357
|
+
return new Polygon(config);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* 创建星形
|
|
362
|
+
*/
|
|
363
|
+
createStar(props) {
|
|
364
|
+
const config = this.buildBaseConfig(props);
|
|
365
|
+
|
|
366
|
+
if (props.corners !== undefined) config.corners = props.corners;
|
|
367
|
+
if (props.innerRadius !== undefined) config.innerRadius = props.innerRadius;
|
|
368
|
+
|
|
369
|
+
return new Star(config);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 创建路径
|
|
374
|
+
*/
|
|
375
|
+
createPath(props) {
|
|
376
|
+
const config = this.buildBaseConfig(props);
|
|
377
|
+
|
|
378
|
+
if (props.path) {
|
|
379
|
+
config.path = props.path;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return new Path(config);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* 创建画笔
|
|
387
|
+
*/
|
|
388
|
+
createPen(props) {
|
|
389
|
+
const pen = new Pen();
|
|
390
|
+
|
|
391
|
+
if (props.commands && Array.isArray(props.commands)) {
|
|
392
|
+
for (const cmd of props.commands) {
|
|
393
|
+
this.executePenCommand(pen, cmd);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return pen;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* 执行画笔命令
|
|
402
|
+
*/
|
|
403
|
+
executePenCommand(pen, cmd) {
|
|
404
|
+
if (!cmd.method) return;
|
|
405
|
+
|
|
406
|
+
switch (cmd.method) {
|
|
407
|
+
case 'setStyle':
|
|
408
|
+
pen.setStyle(cmd.style || {});
|
|
409
|
+
break;
|
|
410
|
+
case 'moveTo':
|
|
411
|
+
pen.moveTo(cmd.x, cmd.y);
|
|
412
|
+
break;
|
|
413
|
+
case 'lineTo':
|
|
414
|
+
pen.lineTo(cmd.x, cmd.y);
|
|
415
|
+
break;
|
|
416
|
+
case 'arc':
|
|
417
|
+
pen.arc(cmd.x, cmd.y, cmd.radius, cmd.startAngle, cmd.endAngle);
|
|
418
|
+
break;
|
|
419
|
+
case 'roundRect':
|
|
420
|
+
pen.roundRect(cmd.x, cmd.y, cmd.width, cmd.height, cmd.radius);
|
|
421
|
+
break;
|
|
422
|
+
case 'ellipse':
|
|
423
|
+
pen.ellipse(cmd.x, cmd.y, cmd.radiusX, cmd.radiusY);
|
|
424
|
+
break;
|
|
425
|
+
case 'closePath':
|
|
426
|
+
pen.closePath();
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* 创建文本 - 完整字体支持
|
|
433
|
+
*/
|
|
434
|
+
createText(props) {
|
|
435
|
+
const config = {
|
|
436
|
+
x: props.x || 0,
|
|
437
|
+
y: props.y || 0,
|
|
438
|
+
text: String(props.text || ''),
|
|
439
|
+
fill: props.fill || '#000000',
|
|
440
|
+
fontSize: props.fontSize || 16,
|
|
441
|
+
fontWeight: props.fontWeight || 'normal',
|
|
442
|
+
textAlign: props.textAlign || 'left',
|
|
443
|
+
opacity: props.opacity !== undefined ? props.opacity : 1,
|
|
444
|
+
};
|
|
445
|
+
|
|
446
|
+
// 字体处理
|
|
447
|
+
const fontFamily = props.fontFamily || global.CHINESE_FONT_FAMILIES[0];
|
|
448
|
+
config.fontFamily = fontFamily;
|
|
449
|
+
|
|
450
|
+
if (props.lineHeight !== undefined) config.lineHeight = props.lineHeight;
|
|
451
|
+
if (props.letterSpacing !== undefined) config.letterSpacing = props.letterSpacing;
|
|
452
|
+
|
|
453
|
+
return new Text(config);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* 创建图片
|
|
458
|
+
*/
|
|
459
|
+
createImage(props) {
|
|
460
|
+
const url = props.url || props.src;
|
|
461
|
+
if (!url) {
|
|
462
|
+
console.warn('[LeaferRenderer V2] ⚠️ Image element missing url/src, skipping');
|
|
463
|
+
return null;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const config = {
|
|
467
|
+
x: props.x || 0,
|
|
468
|
+
y: props.y || 0,
|
|
469
|
+
url: url,
|
|
470
|
+
opacity: props.opacity !== undefined ? props.opacity : 1,
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
if (props.width !== undefined) config.width = props.width;
|
|
474
|
+
if (props.height !== undefined) config.height = props.height;
|
|
475
|
+
if (props.cornerRadius !== undefined) config.cornerRadius = props.cornerRadius;
|
|
476
|
+
|
|
477
|
+
return new Image(config);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* 创建组
|
|
482
|
+
*/
|
|
483
|
+
createGroup(props) {
|
|
484
|
+
const config = {
|
|
485
|
+
x: props.x || 0,
|
|
486
|
+
y: props.y || 0,
|
|
487
|
+
};
|
|
488
|
+
|
|
489
|
+
if (props.opacity !== undefined) config.opacity = props.opacity;
|
|
490
|
+
|
|
491
|
+
const group = new Group(config);
|
|
492
|
+
|
|
493
|
+
// 添加子元素
|
|
494
|
+
if (props.children && Array.isArray(props.children)) {
|
|
495
|
+
for (const childConfig of props.children) {
|
|
496
|
+
const child = this.createElement(childConfig);
|
|
497
|
+
if (child) {
|
|
498
|
+
group.add(child);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
return group;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
/**
|
|
507
|
+
* 创建盒子(带样式的组)
|
|
508
|
+
*/
|
|
509
|
+
createBox(props) {
|
|
510
|
+
const config = this.buildBaseConfig(props);
|
|
511
|
+
|
|
512
|
+
const box = new Box(config);
|
|
513
|
+
|
|
514
|
+
// 添加子元素
|
|
515
|
+
if (props.children && Array.isArray(props.children)) {
|
|
516
|
+
for (const childConfig of props.children) {
|
|
517
|
+
const child = this.createElement(childConfig);
|
|
518
|
+
if (child) {
|
|
519
|
+
box.add(child);
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return box;
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* 创建画板
|
|
529
|
+
*/
|
|
530
|
+
createFrame(props) {
|
|
531
|
+
const config = {
|
|
532
|
+
width: props.width || 100,
|
|
533
|
+
height: props.height || 100,
|
|
534
|
+
};
|
|
535
|
+
|
|
536
|
+
if (props.x !== undefined) config.x = props.x;
|
|
537
|
+
if (props.y !== undefined) config.y = props.y;
|
|
538
|
+
if (props.fill !== undefined) config.fill = props.fill;
|
|
539
|
+
|
|
540
|
+
const frame = new Frame(config);
|
|
541
|
+
|
|
542
|
+
// 添加子元素
|
|
543
|
+
if (props.children && Array.isArray(props.children)) {
|
|
544
|
+
for (const childConfig of props.children) {
|
|
545
|
+
const child = this.createElement(childConfig);
|
|
546
|
+
if (child) {
|
|
547
|
+
frame.add(child);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return frame;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/**
|
|
556
|
+
* 构建基础配置 - 包含所有通用样式
|
|
557
|
+
*/
|
|
558
|
+
buildBaseConfig(props) {
|
|
559
|
+
const config = {
|
|
560
|
+
x: props.x || 0,
|
|
561
|
+
y: props.y || 0,
|
|
562
|
+
width: props.width !== undefined ? props.width : 100,
|
|
563
|
+
height: props.height !== undefined ? props.height : 100,
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
// 填充
|
|
567
|
+
if (props.fill !== undefined) {
|
|
568
|
+
config.fill = this.parseFill(props.fill);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// 描边
|
|
572
|
+
if (props.stroke !== undefined) {
|
|
573
|
+
config.stroke = this.parseFill(props.stroke);
|
|
574
|
+
}
|
|
575
|
+
if (props.strokeWidth !== undefined) config.strokeWidth = props.strokeWidth;
|
|
576
|
+
if (props.strokeAlign !== undefined) config.strokeAlign = props.strokeAlign;
|
|
577
|
+
if (props.strokeCap !== undefined) config.strokeCap = props.strokeCap;
|
|
578
|
+
if (props.strokeJoin !== undefined) config.strokeJoin = props.strokeJoin;
|
|
579
|
+
if (props.dashPattern !== undefined) config.dashPattern = props.dashPattern;
|
|
580
|
+
if (props.dashOffset !== undefined) config.dashOffset = props.dashOffset;
|
|
581
|
+
|
|
582
|
+
// 阴影
|
|
583
|
+
if (props.shadow !== undefined) {
|
|
584
|
+
config.shadow = this.parseShadow(props.shadow);
|
|
585
|
+
}
|
|
586
|
+
if (props.innerShadow !== undefined) {
|
|
587
|
+
config.innerShadow = this.parseShadow(props.innerShadow);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// 变换
|
|
591
|
+
if (props.rotation !== undefined) config.rotation = props.rotation;
|
|
592
|
+
if (props.scaleX !== undefined) config.scaleX = props.scaleX;
|
|
593
|
+
if (props.scaleY !== undefined) config.scaleY = props.scaleY;
|
|
594
|
+
if (props.scale !== undefined) config.scale = props.scale;
|
|
595
|
+
if (props.skewX !== undefined) config.skewX = props.skewX;
|
|
596
|
+
if (props.skewY !== undefined) config.skewY = props.skewY;
|
|
597
|
+
|
|
598
|
+
// 定位
|
|
599
|
+
if (props.origin !== undefined) config.origin = props.origin;
|
|
600
|
+
if (props.around !== undefined) config.around = props.around;
|
|
601
|
+
if (props.zIndex !== undefined) config.zIndex = props.zIndex;
|
|
602
|
+
|
|
603
|
+
// 可见性
|
|
604
|
+
if (props.opacity !== undefined) config.opacity = props.opacity;
|
|
605
|
+
if (props.visible !== undefined) config.visible = props.visible;
|
|
606
|
+
|
|
607
|
+
// 遮罩和擦除
|
|
608
|
+
if (props.mask !== undefined) config.mask = props.mask;
|
|
609
|
+
if (props.eraser !== undefined) config.eraser = props.eraser;
|
|
610
|
+
|
|
611
|
+
// 混合模式
|
|
612
|
+
if (props.blendMode !== undefined) config.blendMode = props.blendMode;
|
|
613
|
+
|
|
614
|
+
return config;
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* 构建描边专用配置
|
|
619
|
+
*/
|
|
620
|
+
buildStrokeConfig(props) {
|
|
621
|
+
const config = {
|
|
622
|
+
x: props.x || 0,
|
|
623
|
+
y: props.y || 0,
|
|
624
|
+
};
|
|
625
|
+
|
|
626
|
+
if (props.width !== undefined) config.width = props.width;
|
|
627
|
+
if (props.height !== undefined) config.height = props.height;
|
|
628
|
+
|
|
629
|
+
// 描边样式
|
|
630
|
+
if (props.stroke !== undefined) config.stroke = this.parseFill(props.stroke);
|
|
631
|
+
if (props.strokeWidth !== undefined) config.strokeWidth = props.strokeWidth;
|
|
632
|
+
if (props.strokeAlign !== undefined) config.strokeAlign = props.strokeAlign;
|
|
633
|
+
if (props.strokeCap !== undefined) config.strokeCap = props.strokeCap;
|
|
634
|
+
if (props.strokeJoin !== undefined) config.strokeJoin = props.strokeJoin;
|
|
635
|
+
if (props.strokeScaleFixed !== undefined) config.strokeScaleFixed = props.strokeScaleFixed;
|
|
636
|
+
if (props.dashPattern !== undefined) config.dashPattern = props.dashPattern;
|
|
637
|
+
if (props.dashOffset !== undefined) config.dashOffset = props.dashOffset;
|
|
638
|
+
|
|
639
|
+
// 变换
|
|
640
|
+
if (props.rotation !== undefined) config.rotation = props.rotation;
|
|
641
|
+
if (props.opacity !== undefined) config.opacity = props.opacity;
|
|
642
|
+
|
|
643
|
+
return config;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* 解析填充样式 - 支持渐变
|
|
648
|
+
*/
|
|
649
|
+
parseFill(fill) {
|
|
650
|
+
if (!fill) return undefined;
|
|
651
|
+
|
|
652
|
+
if (typeof fill === 'string') {
|
|
653
|
+
return fill;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
if (typeof fill === 'object') {
|
|
657
|
+
const { type, ...props } = fill;
|
|
658
|
+
|
|
659
|
+
switch (type) {
|
|
660
|
+
case 'linear':
|
|
661
|
+
return {
|
|
662
|
+
type: 'linear',
|
|
663
|
+
from: props.from || 'top',
|
|
664
|
+
to: props.to || 'bottom',
|
|
665
|
+
stops: props.stops || ['#000000', '#ffffff'],
|
|
666
|
+
};
|
|
667
|
+
case 'radial':
|
|
668
|
+
return {
|
|
669
|
+
type: 'radial',
|
|
670
|
+
from: props.from || 'center',
|
|
671
|
+
to: props.to || 'bottom',
|
|
672
|
+
stops: props.stops || ['#000000', '#ffffff'],
|
|
673
|
+
};
|
|
674
|
+
case 'image':
|
|
675
|
+
case 'pattern': {
|
|
676
|
+
const imageUrl = props.url || props.src;
|
|
677
|
+
if (!imageUrl) {
|
|
678
|
+
console.warn('[LeaferRenderer V2] ⚠️ Image fill missing url/src');
|
|
679
|
+
return undefined;
|
|
680
|
+
}
|
|
681
|
+
return {
|
|
682
|
+
type: 'image',
|
|
683
|
+
url: imageUrl,
|
|
684
|
+
mode: props.mode || 'repeat',
|
|
685
|
+
};
|
|
686
|
+
}
|
|
687
|
+
default:
|
|
688
|
+
return fill;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return fill;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* 解析阴影
|
|
697
|
+
*/
|
|
698
|
+
parseShadow(shadow) {
|
|
699
|
+
if (!shadow) return undefined;
|
|
700
|
+
|
|
701
|
+
if (Array.isArray(shadow)) {
|
|
702
|
+
return shadow.map(s => ({
|
|
703
|
+
x: s.x || 0,
|
|
704
|
+
y: s.y || 0,
|
|
705
|
+
blur: s.blur || 0,
|
|
706
|
+
color: s.color || '#000000',
|
|
707
|
+
}));
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
return {
|
|
711
|
+
x: shadow.x || 0,
|
|
712
|
+
y: shadow.y || 0,
|
|
713
|
+
blur: shadow.blur || 0,
|
|
714
|
+
color: shadow.color || '#000000',
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* 生成缓存键
|
|
720
|
+
*/
|
|
721
|
+
generateCacheKey(config) {
|
|
722
|
+
const str = JSON.stringify(config);
|
|
723
|
+
return crypto.createHash('md5').update(str).digest('hex');
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* 设置缓存
|
|
728
|
+
*/
|
|
729
|
+
setCache(key, value) {
|
|
730
|
+
if (this.renderCache.size >= this.options.maxCacheSize) {
|
|
731
|
+
const firstKey = this.renderCache.keys().next().value;
|
|
732
|
+
this.renderCache.delete(firstKey);
|
|
733
|
+
}
|
|
734
|
+
this.renderCache.set(key, value);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
/**
|
|
738
|
+
* 获取缓存
|
|
739
|
+
*/
|
|
740
|
+
getCache(key) {
|
|
741
|
+
return this.renderCache.get(key) || null;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/**
|
|
745
|
+
* 获取统计信息
|
|
746
|
+
*/
|
|
747
|
+
getStats() {
|
|
748
|
+
return {
|
|
749
|
+
...this.stats,
|
|
750
|
+
cacheSize: this.renderCache.size,
|
|
751
|
+
cacheHitRate: this.stats.totalRenders > 0
|
|
752
|
+
? (this.stats.cachedRenders / this.stats.totalRenders * 100).toFixed(2) + '%'
|
|
753
|
+
: '0%'
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* 批量渲染
|
|
759
|
+
*/
|
|
760
|
+
async batchRender(configs, options = {}) {
|
|
761
|
+
const { concurrency = 3 } = options;
|
|
762
|
+
const results = [];
|
|
763
|
+
|
|
764
|
+
for (let i = 0; i < configs.length; i += concurrency) {
|
|
765
|
+
const batch = configs.slice(i, i + concurrency);
|
|
766
|
+
const promises = batch.map(async (config) => {
|
|
767
|
+
try {
|
|
768
|
+
const result = await this.render(config);
|
|
769
|
+
return { success: true, data: result };
|
|
770
|
+
} catch (error) {
|
|
771
|
+
return { success: false, error: error.message };
|
|
772
|
+
}
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
const batchResults = await Promise.all(promises);
|
|
776
|
+
results.push(...batchResults);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
return results;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* 清空缓存
|
|
784
|
+
*/
|
|
785
|
+
clearCache() {
|
|
786
|
+
this.renderCache.clear();
|
|
787
|
+
console.log('[LeaferRenderer V2] 🗑️ Cache cleared');
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
module.exports = LeaferRendererV2;
|