figmatk 0.3.7 → 0.3.8

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.3",
16
+ "version": "0.3.8",
17
17
  "author": {
18
18
  "name": "FigmaTK Contributors"
19
19
  },
@@ -4,10 +4,6 @@
4
4
  * Architecture: dispatcher pattern — each Figma node type maps to a render
5
5
  * function. Unknown types emit a magenta placeholder rect so renders never
6
6
  * crash. Add handlers incrementally as coverage grows.
7
- *
8
- * TODO: Symbol instance resolution (INSTANCE → SYMBOL + apply overrides).
9
- * Until that is implemented, INSTANCE nodes render as placeholders.
10
- * For slides that use direct nodes (not template instances), this works fully.
11
7
  */
12
8
 
13
9
  import { readFileSync } from 'fs';
@@ -511,6 +507,67 @@ function renderPlaceholder(deck, node) {
511
507
  return `<rect x="${x}" y="${y}" width="${w || 40}" height="${h || 40}" fill="none" stroke="#ff00ff" stroke-width="2" stroke-dasharray="6" opacity="0.5"/><!-- ${type} -->`;
512
508
  }
513
509
 
510
+ /**
511
+ * INSTANCE → SYMBOL resolution.
512
+ *
513
+ * Figma templates use INSTANCE nodes that reference a SYMBOL definition.
514
+ * The SYMBOL's children (TEXT, shapes, frames, etc.) define the visual content.
515
+ * The INSTANCE may carry symbolOverrides that modify specific child properties
516
+ * (text content, fills, etc.).
517
+ *
518
+ * Strategy:
519
+ * - Resolve the SYMBOL via symbolData.symbolID
520
+ * - Render the SYMBOL's children tree (they live in the normal node hierarchy)
521
+ * - Apply symbolOverrides: text and fill overrides are temporarily applied
522
+ * to the target nodes, rendered, then restored.
523
+ */
524
+ function renderInstance(deck, node) {
525
+ const { x, y } = pos(node);
526
+ const symbolId = node.symbolData?.symbolID;
527
+ if (!symbolId) return renderPlaceholder(deck, node);
528
+
529
+ const symNid = `${symbolId.sessionID}:${symbolId.localID}`;
530
+ const symbol = deck.getNode(symNid);
531
+ if (!symbol) return renderPlaceholder(deck, node);
532
+
533
+ // Temporarily apply symbolOverrides so rendered content reflects overrides.
534
+ // Only single-level guidPath overrides are handled (covers the common case).
535
+ const overrides = node.symbolData?.symbolOverrides ?? [];
536
+ const restores = [];
537
+
538
+ for (const ov of overrides) {
539
+ const guids = ov.guidPath?.guids;
540
+ if (!guids?.length || guids.length !== 1) continue;
541
+ const targetId = `${guids[0].sessionID}:${guids[0].localID}`;
542
+ const target = deck.getNode(targetId);
543
+ if (!target) continue;
544
+
545
+ // Text override — replace characters but keep derived glyph layout.
546
+ // The glyph positions come from the original text, so this is approximate
547
+ // when character count differs, but visually far better than a placeholder.
548
+ if (ov.textData?.characters != null && target.textData) {
549
+ const origChars = target.textData.characters;
550
+ restores.push(() => { target.textData.characters = origChars; });
551
+ target.textData.characters = ov.textData.characters;
552
+ }
553
+
554
+ // Fill override (image swaps, color changes)
555
+ if (ov.fillPaints) {
556
+ const origFill = target.fillPaints;
557
+ restores.push(() => { target.fillPaints = origFill; });
558
+ target.fillPaints = ov.fillPaints;
559
+ }
560
+ }
561
+
562
+ const inner = childrenSvg(deck, symbol);
563
+
564
+ // Restore mutations
565
+ for (const fn of restores) fn();
566
+
567
+ if (!inner) return '';
568
+ return `<g transform="translate(${x},${y})">\n${inner}\n</g>`;
569
+ }
570
+
514
571
  // ── Dispatcher ────────────────────────────────────────────────────────────────
515
572
 
516
573
  const RENDERERS = {
@@ -523,13 +580,11 @@ const RENDERERS = {
523
580
  GROUP: renderGroup,
524
581
  SECTION: renderGroup,
525
582
  BOOLEAN_OPERATION: renderGroup,
526
- // Stubs — add full implementations over time:
527
583
  VECTOR: renderPlaceholder,
528
584
  LINE: renderLine,
529
585
  STAR: renderPlaceholder,
530
586
  POLYGON: renderPlaceholder,
531
- // TODO: INSTANCE → resolve symbol + apply overrides, then recurse
532
- INSTANCE: renderPlaceholder,
587
+ INSTANCE: renderInstance,
533
588
  };
534
589
 
535
590
  function renderNode(deck, node) {
@@ -298,13 +298,41 @@ function describeLayout(deck, layout, row) {
298
298
  };
299
299
  }
300
300
 
301
+ /**
302
+ * Walk the node tree like deck.walkTree, but follow INSTANCE → SYMBOL links.
303
+ * When an INSTANCE node is encountered, its referenced SYMBOL's children are
304
+ * also walked so that slots inside published template components are discovered.
305
+ */
306
+ function walkTreeThroughInstances(deck, rootId, visitor, depth = 0, visited = new Set()) {
307
+ if (!rootId || visited.has(rootId)) return;
308
+ visited.add(rootId);
309
+
310
+ const node = deck.getNode(rootId);
311
+ if (!node || node.phase === 'REMOVED') return;
312
+ visitor(node, depth);
313
+
314
+ // Follow INSTANCE → SYMBOL: walk the SYMBOL's children
315
+ if (node.type === 'INSTANCE' && node.symbolData?.symbolID) {
316
+ const sid = node.symbolData.symbolID;
317
+ const symNid = `${sid.sessionID}:${sid.localID}`;
318
+ for (const child of deck.getChildren(symNid)) {
319
+ walkTreeThroughInstances(deck, nid(child), visitor, depth + 1, visited);
320
+ }
321
+ }
322
+
323
+ // Walk direct children
324
+ for (const child of deck.getChildren(rootId)) {
325
+ walkTreeThroughInstances(deck, nid(child), visitor, depth + 1, visited);
326
+ }
327
+ }
328
+
301
329
  function discoverSlots(deck, rootId) {
302
330
  const explicitTextSlots = [];
303
331
  const explicitImageSlots = [];
304
332
  const fallbackTextSlots = [];
305
333
  const fallbackImageSlots = [];
306
334
 
307
- deck.walkTree(rootId, node => {
335
+ walkTreeThroughInstances(deck, rootId, node => {
308
336
  const textSlotName = parsePrefixedName(node.name, TEXT_SLOT_PREFIX);
309
337
  if (textSlotName) {
310
338
  const slot = describeTextSlot(node, textSlotName, 'explicit');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "figmatk",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
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.7"
9
+ version: "0.3.8"
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.7"
9
+ version: "0.3.8"
10
10
  ---
11
11
 
12
12
  # Figma Template Builder