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