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.
|
@@ -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:
|
|
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
|
-
|
|
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
package/package.json
CHANGED