figmatk 0.3.8 → 0.3.10

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.
@@ -13,7 +13,7 @@
13
13
  {
14
14
  "name": "figmatk",
15
15
  "description": "Swiss Army Knife for Figma Files (.deck)",
16
- "version": "0.3.8",
16
+ "version": "0.3.10",
17
17
  "author": {
18
18
  "name": "FigmaTK Contributors"
19
19
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figmatk",
3
- "version": "0.3.3",
3
+ "version": "0.3.10",
4
4
  "description": "Create and edit Figma Slides .deck files programmatically — no Figma API required",
5
5
  "author": {
6
6
  "name": "FigmaTK Contributors"
@@ -500,6 +500,217 @@ function renderLine(deck, node) {
500
500
  return `<line x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" stroke="${stroke}" stroke-width="${sw}"/>`;
501
501
  }
502
502
 
503
+ /**
504
+ * VECTOR — decode fillGeometry/strokeGeometry commandsBlob binary to SVG paths.
505
+ *
506
+ * Blob format: [cmdByte][float32LE params...]
507
+ * 0x01 = moveTo (x, y)
508
+ * 0x02 = lineTo (x, y)
509
+ * 0x04 = cubicTo (c1x, c1y, c2x, c2y, x, y)
510
+ * 0x00 = close
511
+ *
512
+ * Coordinates are in node-size space. The full affine transform matrix is used
513
+ * to position, scale, and rotate the vector in the slide.
514
+ */
515
+ function renderVector(deck, node) {
516
+ const t = node.transform ?? {};
517
+ const m00 = t.m00 ?? 1, m01 = t.m01 ?? 0, m02 = t.m02 ?? 0;
518
+ const m10 = t.m10 ?? 0, m11 = t.m11 ?? 1, m12 = t.m12 ?? 0;
519
+ const blobs = deck.message?.blobs;
520
+ const parts = [];
521
+
522
+ // Fill paths — combine all fillGeometry entries into one compound path so
523
+ // overlapping sub-paths interact via fill-rule (e.g. QR codes, letter cutouts).
524
+ const fillColor = resolveFill(getFillPaints(node));
525
+ if (fillColor && node.fillGeometry?.length && blobs) {
526
+ const segments = [];
527
+ let hasEvenOdd = false;
528
+ for (const geo of node.fillGeometry) {
529
+ const d = decodeCmdBlob(blobs, geo.commandsBlob);
530
+ if (d) segments.push(d);
531
+ if (geo.windingRule === 'EVENODD') hasEvenOdd = true;
532
+ }
533
+ if (segments.length) {
534
+ // Multiple sub-paths: use evenodd so overlapping regions create holes
535
+ const rule = (segments.length > 1 || hasEvenOdd) ? ' fill-rule="evenodd"' : '';
536
+ parts.push(`<path d="${segments.join('')}" fill="${fillColor}"${rule}/>`);
537
+ }
538
+ }
539
+
540
+ // Stroke paths — also compound (stroke outlines are pre-expanded fill shapes)
541
+ const strokeColor = resolveFill(node.strokePaints);
542
+ const sw = node.strokeWeight ?? 0;
543
+ if (strokeColor && sw > 0 && node.strokeGeometry?.length && blobs) {
544
+ const segments = [];
545
+ let hasEvenOdd = false;
546
+ for (const geo of node.strokeGeometry) {
547
+ const d = decodeCmdBlob(blobs, geo.commandsBlob);
548
+ if (d) segments.push(d);
549
+ if (geo.windingRule === 'EVENODD') hasEvenOdd = true;
550
+ }
551
+ if (segments.length) {
552
+ const rule = (segments.length > 1 || hasEvenOdd) ? ' fill-rule="evenodd"' : '';
553
+ parts.push(`<path d="${segments.join('')}" fill="${strokeColor}"${rule}/>`);
554
+ }
555
+ }
556
+
557
+ // Fallback: decode vectorNetworkBlob when no pre-computed fill/strokeGeometry
558
+ if (!parts.length && node.vectorData?.vectorNetworkBlob != null && blobs) {
559
+ const vnbD = decodeVnb(blobs, node.vectorData.vectorNetworkBlob, node.vectorData.normalizedSize, node.size);
560
+ if (vnbD) {
561
+ const color = fillColor ?? resolveFill(node.strokePaints) ?? '#000000';
562
+ parts.push(`<path d="${vnbD}" fill="${color}" fill-rule="evenodd"/>`);
563
+ }
564
+ }
565
+
566
+ if (!parts.length) return renderPlaceholder(deck, node);
567
+ return `<g transform="matrix(${m00},${m10},${m01},${m11},${m02},${m12})">\n${parts.join('\n')}\n</g>`;
568
+ }
569
+
570
+ /** Decode a commandsBlob index into an SVG path d-string. */
571
+ function decodeCmdBlob(blobs, blobIdx) {
572
+ if (blobIdx == null || !blobs?.[blobIdx]) return null;
573
+ const raw = blobs[blobIdx].bytes ?? blobs[blobIdx];
574
+ if (!raw) return null;
575
+
576
+ // Convert indexed object to Buffer if needed
577
+ let buf;
578
+ if (Buffer.isBuffer(raw) || raw instanceof Uint8Array) {
579
+ buf = Buffer.from(raw);
580
+ } else {
581
+ const len = Object.keys(raw).length;
582
+ buf = Buffer.alloc(len);
583
+ for (let i = 0; i < len; i++) buf[i] = raw[i];
584
+ }
585
+
586
+ const cmds = [];
587
+ let off = 0;
588
+ while (off < buf.length) {
589
+ const cmd = buf[off++];
590
+ if (cmd === 0x01) { // moveTo
591
+ const x = buf.readFloatLE(off); off += 4;
592
+ const y = buf.readFloatLE(off); off += 4;
593
+ cmds.push(`M${f(x)},${f(y)}`);
594
+ } else if (cmd === 0x02) { // lineTo
595
+ const x = buf.readFloatLE(off); off += 4;
596
+ const y = buf.readFloatLE(off); off += 4;
597
+ cmds.push(`L${f(x)},${f(y)}`);
598
+ } else if (cmd === 0x04) { // cubicTo
599
+ const c1x = buf.readFloatLE(off); off += 4;
600
+ const c1y = buf.readFloatLE(off); off += 4;
601
+ const c2x = buf.readFloatLE(off); off += 4;
602
+ const c2y = buf.readFloatLE(off); off += 4;
603
+ const x = buf.readFloatLE(off); off += 4;
604
+ const y = buf.readFloatLE(off); off += 4;
605
+ cmds.push(`C${f(c1x)},${f(c1y)} ${f(c2x)},${f(c2y)} ${f(x)},${f(y)}`);
606
+ } else if (cmd === 0x00) { // close
607
+ cmds.push('Z');
608
+ } else {
609
+ break; // unknown command — stop
610
+ }
611
+ }
612
+ return cmds.length ? cmds.join('') : null;
613
+ }
614
+
615
+ function f(v) { return +v.toFixed(2); }
616
+
617
+ /**
618
+ * Decode vectorNetworkBlob into an SVG path d-string.
619
+ * VNB stores vertices, segments (lines/cubics), and regions (loops of segment indices).
620
+ * Coordinates are in normalizedSize space and must be scaled to nodeSize.
621
+ *
622
+ * Binary layout (all little-endian):
623
+ * Header: numVertices(u32), numSegments(u32), numRegions(u32), numStyles(u32)
624
+ * Vertices: x(f32), y(f32), handleMirroring(u32) — 12 bytes each
625
+ * Segments: startVertex(u32), tangentStartX(f32), tangentStartY(f32),
626
+ * endVertex(u32), tangentEndX(f32), tangentEndY(f32), segType(u32) — 28 bytes each
627
+ * Regions: numLoops(u32), per loop: segCount(u32) + segIndices(u32[segCount]), windingRule(u32)
628
+ */
629
+ function decodeVnb(blobs, blobIdx, normalizedSize, nodeSize) {
630
+ const buf = blobToBuffer(blobs, blobIdx);
631
+ if (!buf || buf.length < 16) return null;
632
+
633
+ const scaleX = (nodeSize?.x ?? 1) / (normalizedSize?.x ?? 1);
634
+ const scaleY = (nodeSize?.y ?? 1) / (normalizedSize?.y ?? 1);
635
+
636
+ let off = 0;
637
+ const numVerts = buf.readUInt32LE(off); off += 4;
638
+ const numSegs = buf.readUInt32LE(off); off += 4;
639
+ const numRegions = buf.readUInt32LE(off); off += 4;
640
+ off += 4; // numStyles
641
+
642
+ // Parse vertices
643
+ const verts = [];
644
+ for (let i = 0; i < numVerts; i++) {
645
+ const x = buf.readFloatLE(off) * scaleX; off += 4;
646
+ const y = buf.readFloatLE(off) * scaleY; off += 4;
647
+ off += 4; // handleMirroring
648
+ verts.push({ x, y });
649
+ }
650
+
651
+ // Parse segments
652
+ const segs = [];
653
+ for (let i = 0; i < numSegs; i++) {
654
+ const sv = buf.readUInt32LE(off); off += 4;
655
+ const tsx = buf.readFloatLE(off) * scaleX; off += 4;
656
+ const tsy = buf.readFloatLE(off) * scaleY; off += 4;
657
+ const ev = buf.readUInt32LE(off); off += 4;
658
+ const tex = buf.readFloatLE(off) * scaleX; off += 4;
659
+ const tey = buf.readFloatLE(off) * scaleY; off += 4;
660
+ const type = buf.readUInt32LE(off); off += 4;
661
+ segs.push({ sv, tsx, tsy, ev, tex, tey, type });
662
+ }
663
+
664
+ // Parse regions → build SVG paths
665
+ const cmds = [];
666
+ for (let r = 0; r < numRegions; r++) {
667
+ if (off + 4 > buf.length) break;
668
+ const numLoops = buf.readUInt32LE(off); off += 4;
669
+ for (let loop = 0; loop < numLoops; loop++) {
670
+ if (off + 4 > buf.length) break;
671
+ const segCount = buf.readUInt32LE(off); off += 4;
672
+ for (let s = 0; s < segCount; s++) {
673
+ if (off + 4 > buf.length) break;
674
+ const segIdx = buf.readUInt32LE(off); off += 4;
675
+ if (segIdx >= segs.length) continue;
676
+ const seg = segs[segIdx];
677
+ const start = verts[seg.sv];
678
+ const end = verts[seg.ev];
679
+ if (!start || !end) continue;
680
+
681
+ if (s === 0) cmds.push(`M${f(start.x)},${f(start.y)}`);
682
+
683
+ if (seg.type === 0) {
684
+ // Line
685
+ cmds.push(`L${f(end.x)},${f(end.y)}`);
686
+ } else {
687
+ // Cubic bezier — tangents are relative to their vertex
688
+ const c1x = start.x + seg.tsx;
689
+ const c1y = start.y + seg.tsy;
690
+ const c2x = end.x + seg.tex;
691
+ const c2y = end.y + seg.tey;
692
+ cmds.push(`C${f(c1x)},${f(c1y)} ${f(c2x)},${f(c2y)} ${f(end.x)},${f(end.y)}`);
693
+ }
694
+ }
695
+ cmds.push('Z');
696
+ }
697
+ off += 4; // windingRule
698
+ }
699
+
700
+ return cmds.length ? cmds.join('') : null;
701
+ }
702
+
703
+ function blobToBuffer(blobs, blobIdx) {
704
+ if (blobIdx == null || !blobs?.[blobIdx]) return null;
705
+ const raw = blobs[blobIdx].bytes ?? blobs[blobIdx];
706
+ if (!raw) return null;
707
+ if (Buffer.isBuffer(raw) || raw instanceof Uint8Array) return Buffer.from(raw);
708
+ const len = Object.keys(raw).length;
709
+ const buf = Buffer.alloc(len);
710
+ for (let i = 0; i < len; i++) buf[i] = raw[i];
711
+ return buf;
712
+ }
713
+
503
714
  function renderPlaceholder(deck, node) {
504
715
  const { x, y } = pos(node);
505
716
  const { w, h } = size(node);
@@ -580,7 +791,7 @@ const RENDERERS = {
580
791
  GROUP: renderGroup,
581
792
  SECTION: renderGroup,
582
793
  BOOLEAN_OPERATION: renderGroup,
583
- VECTOR: renderPlaceholder,
794
+ VECTOR: renderVector,
584
795
  LINE: renderLine,
585
796
  STAR: renderPlaceholder,
586
797
  POLYGON: renderPlaceholder,
@@ -590,7 +801,12 @@ const RENDERERS = {
590
801
  function renderNode(deck, node) {
591
802
  if (node.phase === 'REMOVED') return '';
592
803
  const fn = RENDERERS[node.type] ?? renderPlaceholder;
593
- return fn(deck, node);
804
+ let svg = fn(deck, node);
805
+ const op = node.opacity;
806
+ if (svg && op != null && op < 1) {
807
+ svg = `<g opacity="${op}">${svg}</g>`;
808
+ }
809
+ return svg;
594
810
  }
595
811
 
596
812
  function childrenSvg(deck, node) {
package/manifest.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": "0.2",
3
3
  "name": "figmatk",
4
- "version": "0.3.3",
4
+ "version": "0.3.10",
5
5
  "description": "Create and edit Figma Slides .deck files programmatically - no Figma API required",
6
6
  "author": {
7
7
  "name": "FigmaTK Contributors"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figmatk",
3
- "version": "0.3.8",
3
+ "version": "0.3.10",
4
4
  "description": "Figma Toolkit — Swiss-army knife CLI for Figma .deck and .fig files",
5
5
  "type": "module",
6
6
  "bin": {
@@ -6,7 +6,7 @@ description: >
6
6
  clone or remove slides, or produce a .deck file for Figma Slides.
7
7
  Powered by FigmaTK under the hood.
8
8
  metadata:
9
- version: "0.3.8"
9
+ version: "0.3.10"
10
10
  ---
11
11
 
12
12
  # FigmaTK Skill
@@ -6,7 +6,7 @@ description: >
6
6
  template from an existing deck, define reusable layouts, mark editable
7
7
  text/image slots, or prepare a draft template for later instantiation.
8
8
  metadata:
9
- version: "0.3.8"
9
+ version: "0.3.10"
10
10
  ---
11
11
 
12
12
  # Figma Template Builder