figmatk 0.3.9 → 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.
|
@@ -519,27 +519,47 @@ function renderVector(deck, node) {
|
|
|
519
519
|
const blobs = deck.message?.blobs;
|
|
520
520
|
const parts = [];
|
|
521
521
|
|
|
522
|
-
// Fill paths
|
|
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).
|
|
523
524
|
const fillColor = resolveFill(getFillPaints(node));
|
|
524
525
|
if (fillColor && node.fillGeometry?.length && blobs) {
|
|
526
|
+
const segments = [];
|
|
527
|
+
let hasEvenOdd = false;
|
|
525
528
|
for (const geo of node.fillGeometry) {
|
|
526
529
|
const d = decodeCmdBlob(blobs, geo.commandsBlob);
|
|
527
|
-
if (
|
|
528
|
-
|
|
529
|
-
|
|
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}/>`);
|
|
530
537
|
}
|
|
531
538
|
}
|
|
532
539
|
|
|
533
|
-
// Stroke paths
|
|
540
|
+
// Stroke paths — also compound (stroke outlines are pre-expanded fill shapes)
|
|
534
541
|
const strokeColor = resolveFill(node.strokePaints);
|
|
535
542
|
const sw = node.strokeWeight ?? 0;
|
|
536
543
|
if (strokeColor && sw > 0 && node.strokeGeometry?.length && blobs) {
|
|
544
|
+
const segments = [];
|
|
545
|
+
let hasEvenOdd = false;
|
|
537
546
|
for (const geo of node.strokeGeometry) {
|
|
538
547
|
const d = decodeCmdBlob(blobs, geo.commandsBlob);
|
|
539
|
-
if (
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
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"/>`);
|
|
543
563
|
}
|
|
544
564
|
}
|
|
545
565
|
|
|
@@ -594,6 +614,103 @@ function decodeCmdBlob(blobs, blobIdx) {
|
|
|
594
614
|
|
|
595
615
|
function f(v) { return +v.toFixed(2); }
|
|
596
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
|
+
|
|
597
714
|
function renderPlaceholder(deck, node) {
|
|
598
715
|
const { x, y } = pos(node);
|
|
599
716
|
const { w, h } = size(node);
|
|
@@ -684,7 +801,12 @@ const RENDERERS = {
|
|
|
684
801
|
function renderNode(deck, node) {
|
|
685
802
|
if (node.phase === 'REMOVED') return '';
|
|
686
803
|
const fn = RENDERERS[node.type] ?? renderPlaceholder;
|
|
687
|
-
|
|
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;
|
|
688
810
|
}
|
|
689
811
|
|
|
690
812
|
function childrenSvg(deck, node) {
|
package/manifest.json
CHANGED
package/package.json
CHANGED