openfig-core 0.3.3 → 0.3.5
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/README.md +1 -0
- package/dist/index.cjs +811 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +242 -20
- package/dist/index.d.ts +242 -20
- package/dist/index.js +793 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -172,13 +172,805 @@ function createEmptyFigDoc() {
|
|
|
172
172
|
const bytes = b64decode(emptyFigTemplate);
|
|
173
173
|
return parseFig(bytes);
|
|
174
174
|
}
|
|
175
|
+
|
|
176
|
+
// src/gradient.ts
|
|
177
|
+
var IDENTITY_TRANSFORM = {
|
|
178
|
+
m00: 1,
|
|
179
|
+
m01: 0,
|
|
180
|
+
m02: 0,
|
|
181
|
+
m10: 0,
|
|
182
|
+
m11: 1,
|
|
183
|
+
m12: 0
|
|
184
|
+
};
|
|
185
|
+
function cloneTransform(transform) {
|
|
186
|
+
if (!transform) return { ...IDENTITY_TRANSFORM };
|
|
187
|
+
return {
|
|
188
|
+
m00: transform.m00,
|
|
189
|
+
m01: transform.m01,
|
|
190
|
+
m02: transform.m02,
|
|
191
|
+
m10: transform.m10,
|
|
192
|
+
m11: transform.m11,
|
|
193
|
+
m12: transform.m12
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
function isRenderableGradientPaint(paint) {
|
|
197
|
+
return !!paint && paint.visible !== false && (paint.type === "GRADIENT_LINEAR" || paint.type === "GRADIENT_RADIAL") && Array.isArray(paint.stops) && paint.stops.length > 0;
|
|
198
|
+
}
|
|
199
|
+
function extractRenderableGradientFill(paints) {
|
|
200
|
+
const paint = paints?.find((entry) => isRenderableGradientPaint(entry));
|
|
201
|
+
if (!paint) return null;
|
|
202
|
+
return {
|
|
203
|
+
type: paint.type === "GRADIENT_LINEAR" ? "linear" : "radial",
|
|
204
|
+
opacity: paint.opacity ?? 1,
|
|
205
|
+
transform: cloneTransform(paint.transform),
|
|
206
|
+
stops: [...paint.stops].sort((a, b) => a.position - b.position).map((stop) => ({
|
|
207
|
+
position: stop.position,
|
|
208
|
+
color: { ...stop.color },
|
|
209
|
+
colorVar: stop.colorVar
|
|
210
|
+
}))
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
function resolveGradientGeometry(fill, width, height) {
|
|
214
|
+
if (width <= 0 || height <= 0) return null;
|
|
215
|
+
const transform = fill.transform ?? IDENTITY_TRANSFORM;
|
|
216
|
+
const { m00, m01, m02, m10, m11, m12 } = transform;
|
|
217
|
+
const det = m00 * m11 - m10 * m01;
|
|
218
|
+
if (Math.abs(det) < 1e-12) return null;
|
|
219
|
+
const ia = m11 / det;
|
|
220
|
+
const ic = -m01 / det;
|
|
221
|
+
const ie = (m01 * m12 - m11 * m02) / det;
|
|
222
|
+
const ib = -m10 / det;
|
|
223
|
+
const iid = m00 / det;
|
|
224
|
+
const iif = (m10 * m02 - m00 * m12) / det;
|
|
225
|
+
const point = (gx, gy) => ({
|
|
226
|
+
x: (ia * gx + ic * gy + ie) * width,
|
|
227
|
+
y: (ib * gx + iid * gy + iif) * height
|
|
228
|
+
});
|
|
229
|
+
if (fill.type === "linear") {
|
|
230
|
+
return {
|
|
231
|
+
type: "linear",
|
|
232
|
+
start: point(0, 0.5),
|
|
233
|
+
end: point(1, 0.5)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
const center = point(0.5, 0.5);
|
|
237
|
+
const xAxisPoint = point(1, 0.5);
|
|
238
|
+
const yAxisPoint = point(0.5, 1);
|
|
239
|
+
return {
|
|
240
|
+
type: "radial",
|
|
241
|
+
center,
|
|
242
|
+
radiusX: Math.hypot(xAxisPoint.x - center.x, xAxisPoint.y - center.y),
|
|
243
|
+
radiusY: Math.hypot(yAxisPoint.x - center.x, yAxisPoint.y - center.y),
|
|
244
|
+
angle: Math.atan2(xAxisPoint.y - center.y, xAxisPoint.x - center.x)
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// src/vector.ts
|
|
249
|
+
var CMD_CLOSE = 0;
|
|
250
|
+
var CMD_MOVE_TO = 1;
|
|
251
|
+
var CMD_LINE_TO = 2;
|
|
252
|
+
var CMD_CUBIC_TO = 4;
|
|
253
|
+
var SEGMENT_LINE = 0;
|
|
254
|
+
var SEGMENT_CUBIC = 4;
|
|
255
|
+
var DEFAULT_HANDLE_MIRRORING = 4;
|
|
256
|
+
function quadraticToCubic(x0, y0, qx, qy, x, y) {
|
|
257
|
+
return {
|
|
258
|
+
type: "C",
|
|
259
|
+
c1x: x0 + 2 / 3 * (qx - x0),
|
|
260
|
+
c1y: y0 + 2 / 3 * (qy - y0),
|
|
261
|
+
c2x: x + 2 / 3 * (qx - x),
|
|
262
|
+
c2y: y + 2 / 3 * (qy - y),
|
|
263
|
+
x,
|
|
264
|
+
y
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function roundPathNumber(n, decimals = 2) {
|
|
268
|
+
const factor = 10 ** decimals;
|
|
269
|
+
return Math.round(n * factor) / factor;
|
|
270
|
+
}
|
|
271
|
+
function getBlobBytes(doc, blobIndex) {
|
|
272
|
+
if (blobIndex == null || blobIndex < 0) return null;
|
|
273
|
+
const blob = doc.message?.blobs?.[blobIndex];
|
|
274
|
+
if (!blob) return null;
|
|
275
|
+
if (blob instanceof Uint8Array) return blob;
|
|
276
|
+
if (blob.bytes instanceof Uint8Array) return blob.bytes;
|
|
277
|
+
if (Array.isArray(blob.bytes)) return Uint8Array.from(blob.bytes);
|
|
278
|
+
if (blob.bytes && typeof blob.bytes === "object") {
|
|
279
|
+
const values = Object.values(blob.bytes);
|
|
280
|
+
if (values.every((value) => typeof value === "number")) {
|
|
281
|
+
return Uint8Array.from(values);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
function geometryBlobToSVGPath(blob) {
|
|
287
|
+
if (!blob.length) return "";
|
|
288
|
+
const view = new DataView(blob.buffer, blob.byteOffset, blob.byteLength);
|
|
289
|
+
let offset = 0;
|
|
290
|
+
const parts = [];
|
|
291
|
+
const canRead = (byteLength) => offset + byteLength <= blob.length;
|
|
292
|
+
while (offset < blob.length) {
|
|
293
|
+
const cmd = blob[offset++];
|
|
294
|
+
switch (cmd) {
|
|
295
|
+
case CMD_CLOSE:
|
|
296
|
+
parts.push("Z");
|
|
297
|
+
break;
|
|
298
|
+
case CMD_MOVE_TO: {
|
|
299
|
+
if (!canRead(8)) return parts.join("");
|
|
300
|
+
const x = roundPathNumber(view.getFloat32(offset, true));
|
|
301
|
+
const y = roundPathNumber(view.getFloat32(offset + 4, true));
|
|
302
|
+
offset += 8;
|
|
303
|
+
parts.push(`M${x} ${y}`);
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
case CMD_LINE_TO: {
|
|
307
|
+
if (!canRead(8)) return parts.join("");
|
|
308
|
+
const x = roundPathNumber(view.getFloat32(offset, true));
|
|
309
|
+
const y = roundPathNumber(view.getFloat32(offset + 4, true));
|
|
310
|
+
offset += 8;
|
|
311
|
+
parts.push(`L${x} ${y}`);
|
|
312
|
+
break;
|
|
313
|
+
}
|
|
314
|
+
case CMD_CUBIC_TO: {
|
|
315
|
+
if (!canRead(24)) return parts.join("");
|
|
316
|
+
const x1 = roundPathNumber(view.getFloat32(offset, true));
|
|
317
|
+
const y1 = roundPathNumber(view.getFloat32(offset + 4, true));
|
|
318
|
+
const x2 = roundPathNumber(view.getFloat32(offset + 8, true));
|
|
319
|
+
const y2 = roundPathNumber(view.getFloat32(offset + 12, true));
|
|
320
|
+
const x = roundPathNumber(view.getFloat32(offset + 16, true));
|
|
321
|
+
const y = roundPathNumber(view.getFloat32(offset + 20, true));
|
|
322
|
+
offset += 24;
|
|
323
|
+
parts.push(`C${x1} ${y1} ${x2} ${y2} ${x} ${y}`);
|
|
324
|
+
break;
|
|
325
|
+
}
|
|
326
|
+
default:
|
|
327
|
+
return parts.join("");
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return parts.join("");
|
|
331
|
+
}
|
|
332
|
+
function parseSVGPathData(svgPath) {
|
|
333
|
+
const tokens = [];
|
|
334
|
+
const re = /([MmLlCcSsQqTtHhVvZz])|([+-]?(?:\d+\.?\d*|\.\d+)(?:[eE][+-]?\d+)?)/g;
|
|
335
|
+
let match;
|
|
336
|
+
while ((match = re.exec(svgPath)) !== null) {
|
|
337
|
+
if (match[1]) tokens.push(match[1]);
|
|
338
|
+
else tokens.push(Number.parseFloat(match[2]));
|
|
339
|
+
}
|
|
340
|
+
const commands = [];
|
|
341
|
+
let i = 0;
|
|
342
|
+
let cx = 0;
|
|
343
|
+
let cy = 0;
|
|
344
|
+
let startX = 0;
|
|
345
|
+
let startY = 0;
|
|
346
|
+
let prevC2x = 0;
|
|
347
|
+
let prevC2y = 0;
|
|
348
|
+
let prevQuadraticX = 0;
|
|
349
|
+
let prevQuadraticY = 0;
|
|
350
|
+
let cmd = "";
|
|
351
|
+
const num = () => {
|
|
352
|
+
const value = tokens[i++];
|
|
353
|
+
if (typeof value !== "number" || Number.isNaN(value)) {
|
|
354
|
+
throw new Error(`Invalid SVG path data near token index ${i - 1}`);
|
|
355
|
+
}
|
|
356
|
+
return value;
|
|
357
|
+
};
|
|
358
|
+
while (i < tokens.length) {
|
|
359
|
+
if (typeof tokens[i] === "string") cmd = tokens[i++];
|
|
360
|
+
switch (cmd) {
|
|
361
|
+
case "M":
|
|
362
|
+
cx = num();
|
|
363
|
+
cy = num();
|
|
364
|
+
startX = cx;
|
|
365
|
+
startY = cy;
|
|
366
|
+
commands.push({ type: "M", x: cx, y: cy });
|
|
367
|
+
prevC2x = cx;
|
|
368
|
+
prevC2y = cy;
|
|
369
|
+
prevQuadraticX = cx;
|
|
370
|
+
prevQuadraticY = cy;
|
|
371
|
+
cmd = "L";
|
|
372
|
+
break;
|
|
373
|
+
case "m":
|
|
374
|
+
cx += num();
|
|
375
|
+
cy += num();
|
|
376
|
+
startX = cx;
|
|
377
|
+
startY = cy;
|
|
378
|
+
commands.push({ type: "M", x: cx, y: cy });
|
|
379
|
+
prevC2x = cx;
|
|
380
|
+
prevC2y = cy;
|
|
381
|
+
prevQuadraticX = cx;
|
|
382
|
+
prevQuadraticY = cy;
|
|
383
|
+
cmd = "l";
|
|
384
|
+
break;
|
|
385
|
+
case "L":
|
|
386
|
+
cx = num();
|
|
387
|
+
cy = num();
|
|
388
|
+
commands.push({ type: "L", x: cx, y: cy });
|
|
389
|
+
prevC2x = cx;
|
|
390
|
+
prevC2y = cy;
|
|
391
|
+
prevQuadraticX = cx;
|
|
392
|
+
prevQuadraticY = cy;
|
|
393
|
+
break;
|
|
394
|
+
case "l":
|
|
395
|
+
cx += num();
|
|
396
|
+
cy += num();
|
|
397
|
+
commands.push({ type: "L", x: cx, y: cy });
|
|
398
|
+
prevC2x = cx;
|
|
399
|
+
prevC2y = cy;
|
|
400
|
+
prevQuadraticX = cx;
|
|
401
|
+
prevQuadraticY = cy;
|
|
402
|
+
break;
|
|
403
|
+
case "H":
|
|
404
|
+
cx = num();
|
|
405
|
+
commands.push({ type: "L", x: cx, y: cy });
|
|
406
|
+
prevC2x = cx;
|
|
407
|
+
prevC2y = cy;
|
|
408
|
+
prevQuadraticX = cx;
|
|
409
|
+
prevQuadraticY = cy;
|
|
410
|
+
break;
|
|
411
|
+
case "h":
|
|
412
|
+
cx += num();
|
|
413
|
+
commands.push({ type: "L", x: cx, y: cy });
|
|
414
|
+
prevC2x = cx;
|
|
415
|
+
prevC2y = cy;
|
|
416
|
+
prevQuadraticX = cx;
|
|
417
|
+
prevQuadraticY = cy;
|
|
418
|
+
break;
|
|
419
|
+
case "V":
|
|
420
|
+
cy = num();
|
|
421
|
+
commands.push({ type: "L", x: cx, y: cy });
|
|
422
|
+
prevC2x = cx;
|
|
423
|
+
prevC2y = cy;
|
|
424
|
+
prevQuadraticX = cx;
|
|
425
|
+
prevQuadraticY = cy;
|
|
426
|
+
break;
|
|
427
|
+
case "v":
|
|
428
|
+
cy += num();
|
|
429
|
+
commands.push({ type: "L", x: cx, y: cy });
|
|
430
|
+
prevC2x = cx;
|
|
431
|
+
prevC2y = cy;
|
|
432
|
+
prevQuadraticX = cx;
|
|
433
|
+
prevQuadraticY = cy;
|
|
434
|
+
break;
|
|
435
|
+
case "C": {
|
|
436
|
+
const c1x = num();
|
|
437
|
+
const c1y = num();
|
|
438
|
+
const c2x = num();
|
|
439
|
+
const c2y = num();
|
|
440
|
+
cx = num();
|
|
441
|
+
cy = num();
|
|
442
|
+
prevC2x = c2x;
|
|
443
|
+
prevC2y = c2y;
|
|
444
|
+
prevQuadraticX = cx;
|
|
445
|
+
prevQuadraticY = cy;
|
|
446
|
+
commands.push({ type: "C", c1x, c1y, c2x, c2y, x: cx, y: cy });
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
case "c": {
|
|
450
|
+
const c1x = cx + num();
|
|
451
|
+
const c1y = cy + num();
|
|
452
|
+
const c2x = cx + num();
|
|
453
|
+
const c2y = cy + num();
|
|
454
|
+
cx += num();
|
|
455
|
+
cy += num();
|
|
456
|
+
prevC2x = c2x;
|
|
457
|
+
prevC2y = c2y;
|
|
458
|
+
prevQuadraticX = cx;
|
|
459
|
+
prevQuadraticY = cy;
|
|
460
|
+
commands.push({ type: "C", c1x, c1y, c2x, c2y, x: cx, y: cy });
|
|
461
|
+
break;
|
|
462
|
+
}
|
|
463
|
+
case "S": {
|
|
464
|
+
const c1x = 2 * cx - prevC2x;
|
|
465
|
+
const c1y = 2 * cy - prevC2y;
|
|
466
|
+
const c2x = num();
|
|
467
|
+
const c2y = num();
|
|
468
|
+
cx = num();
|
|
469
|
+
cy = num();
|
|
470
|
+
prevC2x = c2x;
|
|
471
|
+
prevC2y = c2y;
|
|
472
|
+
prevQuadraticX = cx;
|
|
473
|
+
prevQuadraticY = cy;
|
|
474
|
+
commands.push({ type: "C", c1x, c1y, c2x, c2y, x: cx, y: cy });
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
case "s": {
|
|
478
|
+
const c1x = 2 * cx - prevC2x;
|
|
479
|
+
const c1y = 2 * cy - prevC2y;
|
|
480
|
+
const c2x = cx + num();
|
|
481
|
+
const c2y = cy + num();
|
|
482
|
+
cx += num();
|
|
483
|
+
cy += num();
|
|
484
|
+
prevC2x = c2x;
|
|
485
|
+
prevC2y = c2y;
|
|
486
|
+
prevQuadraticX = cx;
|
|
487
|
+
prevQuadraticY = cy;
|
|
488
|
+
commands.push({ type: "C", c1x, c1y, c2x, c2y, x: cx, y: cy });
|
|
489
|
+
break;
|
|
490
|
+
}
|
|
491
|
+
case "Q": {
|
|
492
|
+
const qx = num();
|
|
493
|
+
const qy = num();
|
|
494
|
+
const x = num();
|
|
495
|
+
const y = num();
|
|
496
|
+
const cubic = quadraticToCubic(cx, cy, qx, qy, x, y);
|
|
497
|
+
commands.push(cubic);
|
|
498
|
+
prevQuadraticX = qx;
|
|
499
|
+
prevQuadraticY = qy;
|
|
500
|
+
prevC2x = cubic.c2x;
|
|
501
|
+
prevC2y = cubic.c2y;
|
|
502
|
+
cx = x;
|
|
503
|
+
cy = y;
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
case "q": {
|
|
507
|
+
const qx = cx + num();
|
|
508
|
+
const qy = cy + num();
|
|
509
|
+
const x = cx + num();
|
|
510
|
+
const y = cy + num();
|
|
511
|
+
const cubic = quadraticToCubic(cx, cy, qx, qy, x, y);
|
|
512
|
+
commands.push(cubic);
|
|
513
|
+
prevQuadraticX = qx;
|
|
514
|
+
prevQuadraticY = qy;
|
|
515
|
+
prevC2x = cubic.c2x;
|
|
516
|
+
prevC2y = cubic.c2y;
|
|
517
|
+
cx = x;
|
|
518
|
+
cy = y;
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
case "T": {
|
|
522
|
+
const qx = 2 * cx - prevQuadraticX;
|
|
523
|
+
const qy = 2 * cy - prevQuadraticY;
|
|
524
|
+
const x = num();
|
|
525
|
+
const y = num();
|
|
526
|
+
const cubic = quadraticToCubic(cx, cy, qx, qy, x, y);
|
|
527
|
+
commands.push(cubic);
|
|
528
|
+
prevQuadraticX = qx;
|
|
529
|
+
prevQuadraticY = qy;
|
|
530
|
+
prevC2x = cubic.c2x;
|
|
531
|
+
prevC2y = cubic.c2y;
|
|
532
|
+
cx = x;
|
|
533
|
+
cy = y;
|
|
534
|
+
break;
|
|
535
|
+
}
|
|
536
|
+
case "t": {
|
|
537
|
+
const qx = 2 * cx - prevQuadraticX;
|
|
538
|
+
const qy = 2 * cy - prevQuadraticY;
|
|
539
|
+
const x = cx + num();
|
|
540
|
+
const y = cy + num();
|
|
541
|
+
const cubic = quadraticToCubic(cx, cy, qx, qy, x, y);
|
|
542
|
+
commands.push(cubic);
|
|
543
|
+
prevQuadraticX = qx;
|
|
544
|
+
prevQuadraticY = qy;
|
|
545
|
+
prevC2x = cubic.c2x;
|
|
546
|
+
prevC2y = cubic.c2y;
|
|
547
|
+
cx = x;
|
|
548
|
+
cy = y;
|
|
549
|
+
break;
|
|
550
|
+
}
|
|
551
|
+
case "Z":
|
|
552
|
+
case "z":
|
|
553
|
+
commands.push({ type: "Z" });
|
|
554
|
+
cx = startX;
|
|
555
|
+
cy = startY;
|
|
556
|
+
prevC2x = cx;
|
|
557
|
+
prevC2y = cy;
|
|
558
|
+
prevQuadraticX = cx;
|
|
559
|
+
prevQuadraticY = cy;
|
|
560
|
+
break;
|
|
561
|
+
case "":
|
|
562
|
+
i++;
|
|
563
|
+
break;
|
|
564
|
+
default:
|
|
565
|
+
throw new Error(`Unsupported SVG path command: ${cmd}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return commands;
|
|
569
|
+
}
|
|
570
|
+
function encodeCommandsBlob(commands, scaleX = 1, scaleY = 1) {
|
|
571
|
+
let byteLength = 0;
|
|
572
|
+
for (const command of commands) {
|
|
573
|
+
byteLength += 1;
|
|
574
|
+
if (command.type === "M" || command.type === "L") byteLength += 8;
|
|
575
|
+
else if (command.type === "C") byteLength += 24;
|
|
576
|
+
}
|
|
577
|
+
const buffer = new ArrayBuffer(byteLength);
|
|
578
|
+
const view = new DataView(buffer);
|
|
579
|
+
let offset = 0;
|
|
580
|
+
for (const command of commands) {
|
|
581
|
+
switch (command.type) {
|
|
582
|
+
case "M":
|
|
583
|
+
view.setUint8(offset++, CMD_MOVE_TO);
|
|
584
|
+
view.setFloat32(offset, command.x * scaleX, true);
|
|
585
|
+
offset += 4;
|
|
586
|
+
view.setFloat32(offset, command.y * scaleY, true);
|
|
587
|
+
offset += 4;
|
|
588
|
+
break;
|
|
589
|
+
case "L":
|
|
590
|
+
view.setUint8(offset++, CMD_LINE_TO);
|
|
591
|
+
view.setFloat32(offset, command.x * scaleX, true);
|
|
592
|
+
offset += 4;
|
|
593
|
+
view.setFloat32(offset, command.y * scaleY, true);
|
|
594
|
+
offset += 4;
|
|
595
|
+
break;
|
|
596
|
+
case "C":
|
|
597
|
+
view.setUint8(offset++, CMD_CUBIC_TO);
|
|
598
|
+
view.setFloat32(offset, command.c1x * scaleX, true);
|
|
599
|
+
offset += 4;
|
|
600
|
+
view.setFloat32(offset, command.c1y * scaleY, true);
|
|
601
|
+
offset += 4;
|
|
602
|
+
view.setFloat32(offset, command.c2x * scaleX, true);
|
|
603
|
+
offset += 4;
|
|
604
|
+
view.setFloat32(offset, command.c2y * scaleY, true);
|
|
605
|
+
offset += 4;
|
|
606
|
+
view.setFloat32(offset, command.x * scaleX, true);
|
|
607
|
+
offset += 4;
|
|
608
|
+
view.setFloat32(offset, command.y * scaleY, true);
|
|
609
|
+
offset += 4;
|
|
610
|
+
break;
|
|
611
|
+
case "Z":
|
|
612
|
+
view.setUint8(offset++, CMD_CLOSE);
|
|
613
|
+
break;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
return new Uint8Array(buffer, 0, offset);
|
|
617
|
+
}
|
|
618
|
+
function encodeVectorNetworkBlob(pathCommandsList) {
|
|
619
|
+
const vertices = [];
|
|
620
|
+
const segments = [];
|
|
621
|
+
const regions = [];
|
|
622
|
+
for (const pathCommands of pathCommandsList) {
|
|
623
|
+
let regionSegments = [];
|
|
624
|
+
let firstVertex = -1;
|
|
625
|
+
let prevVertex = -1;
|
|
626
|
+
let prevX = 0;
|
|
627
|
+
let prevY = 0;
|
|
628
|
+
for (const command of pathCommands) {
|
|
629
|
+
if (command.type === "M") {
|
|
630
|
+
if (regionSegments.length > 0) {
|
|
631
|
+
regions.push(regionSegments);
|
|
632
|
+
regionSegments = [];
|
|
633
|
+
}
|
|
634
|
+
const vertexIndex = vertices.length;
|
|
635
|
+
vertices.push({ x: command.x, y: command.y });
|
|
636
|
+
firstVertex = vertexIndex;
|
|
637
|
+
prevVertex = vertexIndex;
|
|
638
|
+
prevX = command.x;
|
|
639
|
+
prevY = command.y;
|
|
640
|
+
} else if (command.type === "L") {
|
|
641
|
+
const vertexIndex = vertices.length;
|
|
642
|
+
vertices.push({ x: command.x, y: command.y });
|
|
643
|
+
if (prevVertex >= 0) {
|
|
644
|
+
regionSegments.push(segments.length);
|
|
645
|
+
segments.push({ s: prevVertex, tsx: 0, tsy: 0, e: vertexIndex, tex: 0, tey: 0, t: SEGMENT_LINE });
|
|
646
|
+
}
|
|
647
|
+
prevVertex = vertexIndex;
|
|
648
|
+
prevX = command.x;
|
|
649
|
+
prevY = command.y;
|
|
650
|
+
} else if (command.type === "C") {
|
|
651
|
+
const vertexIndex = vertices.length;
|
|
652
|
+
vertices.push({ x: command.x, y: command.y });
|
|
653
|
+
if (prevVertex >= 0) {
|
|
654
|
+
regionSegments.push(segments.length);
|
|
655
|
+
segments.push({
|
|
656
|
+
s: prevVertex,
|
|
657
|
+
tsx: command.c1x - prevX,
|
|
658
|
+
tsy: command.c1y - prevY,
|
|
659
|
+
e: vertexIndex,
|
|
660
|
+
tex: command.c2x - command.x,
|
|
661
|
+
tey: command.c2y - command.y,
|
|
662
|
+
t: SEGMENT_CUBIC
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
prevVertex = vertexIndex;
|
|
666
|
+
prevX = command.x;
|
|
667
|
+
prevY = command.y;
|
|
668
|
+
} else if (command.type === "Z") {
|
|
669
|
+
if (prevVertex >= 0 && prevVertex !== firstVertex) {
|
|
670
|
+
const lastPos = vertices[prevVertex];
|
|
671
|
+
const firstPos = vertices[firstVertex];
|
|
672
|
+
const dx = lastPos.x - firstPos.x;
|
|
673
|
+
const dy = lastPos.y - firstPos.y;
|
|
674
|
+
if (dx * dx + dy * dy < 1e-4) {
|
|
675
|
+
const lastSeg = segments[segments.length - 1];
|
|
676
|
+
if (lastSeg && lastSeg.e === prevVertex) {
|
|
677
|
+
lastSeg.e = firstVertex;
|
|
678
|
+
vertices.pop();
|
|
679
|
+
}
|
|
680
|
+
} else {
|
|
681
|
+
regionSegments.push(segments.length);
|
|
682
|
+
segments.push({ s: prevVertex, tsx: 0, tsy: 0, e: firstVertex, tex: 0, tey: 0, t: SEGMENT_LINE });
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
if (firstVertex >= 0) {
|
|
686
|
+
prevVertex = firstVertex;
|
|
687
|
+
prevX = vertices[firstVertex].x;
|
|
688
|
+
prevY = vertices[firstVertex].y;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
regions.push(regionSegments);
|
|
693
|
+
}
|
|
694
|
+
let regionsByteLength = 0;
|
|
695
|
+
for (const region of regions) regionsByteLength += 4 + 4 + region.length * 4 + 4;
|
|
696
|
+
const totalByteLength = 16 + vertices.length * 12 + segments.length * 28 + regionsByteLength;
|
|
697
|
+
const buffer = new ArrayBuffer(totalByteLength);
|
|
698
|
+
const view = new DataView(buffer);
|
|
699
|
+
let offset = 0;
|
|
700
|
+
view.setUint32(offset, vertices.length, true);
|
|
701
|
+
offset += 4;
|
|
702
|
+
view.setUint32(offset, segments.length, true);
|
|
703
|
+
offset += 4;
|
|
704
|
+
view.setUint32(offset, regions.length, true);
|
|
705
|
+
offset += 4;
|
|
706
|
+
view.setUint32(offset, 1, true);
|
|
707
|
+
offset += 4;
|
|
708
|
+
for (const vertex of vertices) {
|
|
709
|
+
view.setFloat32(offset, vertex.x, true);
|
|
710
|
+
offset += 4;
|
|
711
|
+
view.setFloat32(offset, vertex.y, true);
|
|
712
|
+
offset += 4;
|
|
713
|
+
view.setUint32(offset, DEFAULT_HANDLE_MIRRORING, true);
|
|
714
|
+
offset += 4;
|
|
715
|
+
}
|
|
716
|
+
for (const segment of segments) {
|
|
717
|
+
view.setUint32(offset, segment.s, true);
|
|
718
|
+
offset += 4;
|
|
719
|
+
view.setFloat32(offset, segment.tsx, true);
|
|
720
|
+
offset += 4;
|
|
721
|
+
view.setFloat32(offset, segment.tsy, true);
|
|
722
|
+
offset += 4;
|
|
723
|
+
view.setUint32(offset, segment.e, true);
|
|
724
|
+
offset += 4;
|
|
725
|
+
view.setFloat32(offset, segment.tex, true);
|
|
726
|
+
offset += 4;
|
|
727
|
+
view.setFloat32(offset, segment.tey, true);
|
|
728
|
+
offset += 4;
|
|
729
|
+
view.setUint32(offset, segment.t, true);
|
|
730
|
+
offset += 4;
|
|
731
|
+
}
|
|
732
|
+
for (const region of regions) {
|
|
733
|
+
view.setUint32(offset, 1, true);
|
|
734
|
+
offset += 4;
|
|
735
|
+
view.setUint32(offset, region.length, true);
|
|
736
|
+
offset += 4;
|
|
737
|
+
for (const segmentIndex of region) {
|
|
738
|
+
view.setUint32(offset, segmentIndex, true);
|
|
739
|
+
offset += 4;
|
|
740
|
+
}
|
|
741
|
+
view.setUint32(offset, 1, true);
|
|
742
|
+
offset += 4;
|
|
743
|
+
}
|
|
744
|
+
return new Uint8Array(buffer, 0, offset);
|
|
745
|
+
}
|
|
746
|
+
function cloneStyleOverrides(styleOverrideTable) {
|
|
747
|
+
if (!styleOverrideTable?.length) return void 0;
|
|
748
|
+
return JSON.parse(JSON.stringify(styleOverrideTable));
|
|
749
|
+
}
|
|
750
|
+
function toCommands(input) {
|
|
751
|
+
if (Array.isArray(input.commands) && input.commands.length > 0) {
|
|
752
|
+
return input.commands.map((command) => ({ ...command }));
|
|
753
|
+
}
|
|
754
|
+
if (input.svgPath) return parseSVGPathData(input.svgPath);
|
|
755
|
+
throw new Error("Vector geometry input requires either svgPath or commands");
|
|
756
|
+
}
|
|
757
|
+
function appendVectorPayloadToDocument(doc, input) {
|
|
758
|
+
const blobs = doc.message?.blobs ?? (doc.message.blobs = []);
|
|
759
|
+
const normalizedWidth = input.normalizedWidth ?? input.width;
|
|
760
|
+
const normalizedHeight = input.normalizedHeight ?? input.height;
|
|
761
|
+
const scaleX = normalizedWidth === 0 ? 1 : input.width / normalizedWidth;
|
|
762
|
+
const scaleY = normalizedHeight === 0 ? 1 : input.height / normalizedHeight;
|
|
763
|
+
const fillPaths = (input.fillPaths ?? []).map(toCommands);
|
|
764
|
+
const strokePaths = (input.strokePaths ?? []).map(toCommands);
|
|
765
|
+
if (fillPaths.length === 0 && strokePaths.length === 0) {
|
|
766
|
+
throw new Error("Vector payload requires at least one fill or stroke path");
|
|
767
|
+
}
|
|
768
|
+
const fillGeometry = [];
|
|
769
|
+
for (let i = 0; i < fillPaths.length; i++) {
|
|
770
|
+
const bytes = encodeCommandsBlob(fillPaths[i], scaleX, scaleY);
|
|
771
|
+
blobs.push({ bytes });
|
|
772
|
+
const path = input.fillPaths?.[i];
|
|
773
|
+
fillGeometry.push({
|
|
774
|
+
windingRule: path?.windingRule ?? "NONZERO",
|
|
775
|
+
commandsBlob: blobs.length - 1,
|
|
776
|
+
styleID: path?.styleID ?? 0
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
const strokeGeometry = [];
|
|
780
|
+
for (let i = 0; i < strokePaths.length; i++) {
|
|
781
|
+
const bytes = encodeCommandsBlob(strokePaths[i], scaleX, scaleY);
|
|
782
|
+
blobs.push({ bytes });
|
|
783
|
+
const path = input.strokePaths?.[i];
|
|
784
|
+
strokeGeometry.push({
|
|
785
|
+
windingRule: path?.windingRule ?? "NONZERO",
|
|
786
|
+
commandsBlob: blobs.length - 1,
|
|
787
|
+
styleID: path?.styleID ?? 0
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
const vectorNetworkBlob = encodeVectorNetworkBlob([...fillPaths, ...strokePaths]);
|
|
791
|
+
blobs.push({ bytes: vectorNetworkBlob });
|
|
792
|
+
return {
|
|
793
|
+
fillGeometry,
|
|
794
|
+
strokeGeometry,
|
|
795
|
+
vectorData: {
|
|
796
|
+
vectorNetworkBlob: blobs.length - 1,
|
|
797
|
+
normalizedSize: { x: normalizedWidth, y: normalizedHeight },
|
|
798
|
+
...cloneStyleOverrides(input.styleOverrideTable)?.length ? { styleOverrideTable: cloneStyleOverrides(input.styleOverrideTable) } : {}
|
|
799
|
+
}
|
|
800
|
+
};
|
|
801
|
+
}
|
|
802
|
+
function getStyleOverrideTable(node) {
|
|
803
|
+
const table = node.vectorData?.styleOverrideTable;
|
|
804
|
+
return Array.isArray(table) ? table : [];
|
|
805
|
+
}
|
|
806
|
+
function resolveFillPaints(node, styleID) {
|
|
807
|
+
if (!styleID) return node.fillPaints;
|
|
808
|
+
const override = getStyleOverrideTable(node).find((entry) => entry?.styleID === styleID);
|
|
809
|
+
if (!override || !("fillPaints" in override)) return node.fillPaints;
|
|
810
|
+
return override.fillPaints;
|
|
811
|
+
}
|
|
812
|
+
function resolveGeometry(doc, node, geometry, kind) {
|
|
813
|
+
if (!Array.isArray(geometry) || geometry.length === 0) return [];
|
|
814
|
+
const resolved = geometry.map((entry) => {
|
|
815
|
+
if (typeof entry?.commandsBlob !== "number") return null;
|
|
816
|
+
const bytes = getBlobBytes(doc, entry.commandsBlob);
|
|
817
|
+
if (!bytes) return null;
|
|
818
|
+
const svgPath = geometryBlobToSVGPath(bytes);
|
|
819
|
+
if (!svgPath) return null;
|
|
820
|
+
const path = {
|
|
821
|
+
blobIndex: entry.commandsBlob,
|
|
822
|
+
commandsBlob: bytes,
|
|
823
|
+
svgPath,
|
|
824
|
+
windingRule: entry.windingRule,
|
|
825
|
+
styleID: entry.styleID || 0,
|
|
826
|
+
paints: kind === "fill" ? resolveFillPaints(node, entry.styleID || 0) : node.strokePaints
|
|
827
|
+
};
|
|
828
|
+
return path;
|
|
829
|
+
});
|
|
830
|
+
return resolved.filter((entry) => entry !== null);
|
|
831
|
+
}
|
|
832
|
+
function resolveVectorNodePaths(doc, node) {
|
|
833
|
+
return {
|
|
834
|
+
fill: resolveGeometry(doc, node, node.fillGeometry, "fill"),
|
|
835
|
+
stroke: resolveGeometry(doc, node, node.strokeGeometry, "stroke")
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/color.ts
|
|
840
|
+
function hexToFigColor(hex) {
|
|
841
|
+
if (!hex || hex === "transparent") return { r: 0, g: 0, b: 0, a: 0 };
|
|
842
|
+
const h = hex.replace("#", "");
|
|
843
|
+
const r = parseInt(h.substring(0, 2), 16) / 255;
|
|
844
|
+
const g = parseInt(h.substring(2, 4), 16) / 255;
|
|
845
|
+
const b = parseInt(h.substring(4, 6), 16) / 255;
|
|
846
|
+
const a = h.length >= 8 ? parseInt(h.substring(6, 8), 16) / 255 : 1;
|
|
847
|
+
return { r, g, b, a };
|
|
848
|
+
}
|
|
849
|
+
function parseCssRgbColor(value) {
|
|
850
|
+
const match = value.trim().match(/^rgba?\((.+)\)$/i);
|
|
851
|
+
if (!match) return null;
|
|
852
|
+
const parts = match[1].split(",").map((part) => part.trim());
|
|
853
|
+
if (parts.length < 3) return null;
|
|
854
|
+
const r = Number.parseFloat(parts[0]);
|
|
855
|
+
const g = Number.parseFloat(parts[1]);
|
|
856
|
+
const b = Number.parseFloat(parts[2]);
|
|
857
|
+
const a = parts.length >= 4 ? Number.parseFloat(parts[3]) : 1;
|
|
858
|
+
if ([r, g, b, a].some((n) => Number.isNaN(n))) return null;
|
|
859
|
+
return { r: r / 255, g: g / 255, b: b / 255, a };
|
|
860
|
+
}
|
|
861
|
+
function cssColorToFigColor(value, resolveNamed) {
|
|
862
|
+
const trimmed = value.trim();
|
|
863
|
+
if (trimmed === "transparent" || trimmed === "none") return { r: 0, g: 0, b: 0, a: 0 };
|
|
864
|
+
if (trimmed.startsWith("#")) return hexToFigColor(trimmed);
|
|
865
|
+
const rgba = parseCssRgbColor(trimmed);
|
|
866
|
+
if (rgba) return rgba;
|
|
867
|
+
if (resolveNamed) {
|
|
868
|
+
const resolved = resolveNamed(trimmed);
|
|
869
|
+
if (resolved) return resolved;
|
|
870
|
+
}
|
|
871
|
+
throw new Error(`Unsupported CSS color: ${value}`);
|
|
872
|
+
}
|
|
873
|
+
function makeSolidPaint(fill, resolveNamed) {
|
|
874
|
+
const color = cssColorToFigColor(fill, resolveNamed);
|
|
875
|
+
return {
|
|
876
|
+
type: "SOLID",
|
|
877
|
+
color: { r: color.r, g: color.g, b: color.b, a: 1 },
|
|
878
|
+
opacity: color.a,
|
|
879
|
+
visible: true,
|
|
880
|
+
blendMode: "NORMAL"
|
|
881
|
+
};
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
// src/svgPath.ts
|
|
885
|
+
function serializeSvgPathData(commands) {
|
|
886
|
+
return commands.map((command) => {
|
|
887
|
+
switch (command.type) {
|
|
888
|
+
case "M":
|
|
889
|
+
return `M${command.x} ${command.y}`;
|
|
890
|
+
case "L":
|
|
891
|
+
return `L${command.x} ${command.y}`;
|
|
892
|
+
case "C":
|
|
893
|
+
return `C${command.c1x} ${command.c1y} ${command.c2x} ${command.c2y} ${command.x} ${command.y}`;
|
|
894
|
+
case "Z":
|
|
895
|
+
return "Z";
|
|
896
|
+
}
|
|
897
|
+
}).join(" ");
|
|
898
|
+
}
|
|
899
|
+
function transformSvgPathData(svgPath, {
|
|
900
|
+
scaleX = 1,
|
|
901
|
+
scaleY = 1,
|
|
902
|
+
translateX = 0,
|
|
903
|
+
translateY = 0
|
|
904
|
+
}) {
|
|
905
|
+
const commands = parseSVGPathData(svgPath).map((command) => {
|
|
906
|
+
switch (command.type) {
|
|
907
|
+
case "M":
|
|
908
|
+
case "L":
|
|
909
|
+
return {
|
|
910
|
+
...command,
|
|
911
|
+
x: command.x * scaleX + translateX,
|
|
912
|
+
y: command.y * scaleY + translateY
|
|
913
|
+
};
|
|
914
|
+
case "C":
|
|
915
|
+
return {
|
|
916
|
+
...command,
|
|
917
|
+
c1x: command.c1x * scaleX + translateX,
|
|
918
|
+
c1y: command.c1y * scaleY + translateY,
|
|
919
|
+
c2x: command.c2x * scaleX + translateX,
|
|
920
|
+
c2y: command.c2y * scaleY + translateY,
|
|
921
|
+
x: command.x * scaleX + translateX,
|
|
922
|
+
y: command.y * scaleY + translateY
|
|
923
|
+
};
|
|
924
|
+
case "Z":
|
|
925
|
+
return command;
|
|
926
|
+
}
|
|
927
|
+
});
|
|
928
|
+
return serializeSvgPathData(commands);
|
|
929
|
+
}
|
|
930
|
+
function mapStrokeJoin(value) {
|
|
931
|
+
switch ((value || "").toLowerCase()) {
|
|
932
|
+
case "round":
|
|
933
|
+
return "ROUND";
|
|
934
|
+
case "bevel":
|
|
935
|
+
return "BEVEL";
|
|
936
|
+
default:
|
|
937
|
+
return "MITER";
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
function mapStrokeCap(value) {
|
|
941
|
+
switch ((value || "").toLowerCase()) {
|
|
942
|
+
case "round":
|
|
943
|
+
return "ROUND";
|
|
944
|
+
case "square":
|
|
945
|
+
return "SQUARE";
|
|
946
|
+
default:
|
|
947
|
+
return "NONE";
|
|
948
|
+
}
|
|
949
|
+
}
|
|
175
950
|
export {
|
|
951
|
+
appendVectorPayloadToDocument,
|
|
176
952
|
assembleCanvasFig,
|
|
177
953
|
createEmptyFigDoc,
|
|
178
954
|
createFigZip,
|
|
955
|
+
cssColorToFigColor,
|
|
956
|
+
encodeCommandsBlob,
|
|
179
957
|
encodeFigParts,
|
|
958
|
+
encodeVectorNetworkBlob,
|
|
959
|
+
extractRenderableGradientFill,
|
|
960
|
+
geometryBlobToSVGPath,
|
|
961
|
+
getBlobBytes,
|
|
962
|
+
hexToFigColor,
|
|
963
|
+
makeSolidPaint,
|
|
964
|
+
mapStrokeCap,
|
|
965
|
+
mapStrokeJoin,
|
|
180
966
|
nodeId,
|
|
967
|
+
parseCssRgbColor,
|
|
181
968
|
parseFig,
|
|
182
|
-
parseFigBinary
|
|
969
|
+
parseFigBinary,
|
|
970
|
+
parseSVGPathData,
|
|
971
|
+
resolveGradientGeometry,
|
|
972
|
+
resolveVectorNodePaths,
|
|
973
|
+
serializeSvgPathData,
|
|
974
|
+
transformSvgPathData
|
|
183
975
|
};
|
|
184
976
|
//# sourceMappingURL=index.js.map
|