fluidcad 0.0.34 → 0.0.36

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.
Files changed (176) hide show
  1. package/README.md +69 -0
  2. package/bin/commands/login.js +148 -0
  3. package/bin/commands/mcp.js +3 -2
  4. package/bin/commands/pack.js +49 -0
  5. package/bin/commands/publish.js +231 -0
  6. package/bin/fluidcad.js +6 -0
  7. package/bin/lib/api-client.js +48 -0
  8. package/bin/lib/browser.js +16 -0
  9. package/bin/lib/config.js +39 -0
  10. package/bin/lib/model-config.js +61 -0
  11. package/bin/lib/prompt.js +97 -0
  12. package/bin/lib/workspace.js +57 -0
  13. package/lib/dist/common/shape-factory.d.ts +2 -1
  14. package/lib/dist/common/shape-factory.js +4 -0
  15. package/lib/dist/common/transformable-primitive.d.ts +6 -5
  16. package/lib/dist/common/transformable-primitive.js +8 -7
  17. package/lib/dist/common/vertex.js +0 -1
  18. package/lib/dist/core/2d/aline.d.ts +4 -3
  19. package/lib/dist/core/2d/aline.js +3 -2
  20. package/lib/dist/core/2d/arc.d.ts +3 -2
  21. package/lib/dist/core/2d/arc.js +4 -3
  22. package/lib/dist/core/2d/bezier.d.ts +8 -6
  23. package/lib/dist/core/2d/circle.d.ts +4 -3
  24. package/lib/dist/core/2d/circle.js +3 -2
  25. package/lib/dist/core/2d/ellipse.d.ts +5 -4
  26. package/lib/dist/core/2d/ellipse.js +5 -4
  27. package/lib/dist/core/2d/hline.d.ts +4 -3
  28. package/lib/dist/core/2d/hline.js +5 -3
  29. package/lib/dist/core/2d/line.js +1 -0
  30. package/lib/dist/core/2d/offset.d.ts +3 -2
  31. package/lib/dist/core/2d/offset.js +6 -5
  32. package/lib/dist/core/2d/polygon.d.ts +5 -4
  33. package/lib/dist/core/2d/polygon.js +10 -9
  34. package/lib/dist/core/2d/rect.d.ts +4 -3
  35. package/lib/dist/core/2d/rect.js +10 -9
  36. package/lib/dist/core/2d/slot.d.ts +14 -6
  37. package/lib/dist/core/2d/slot.js +19 -8
  38. package/lib/dist/core/2d/vline.d.ts +4 -3
  39. package/lib/dist/core/2d/vline.js +5 -3
  40. package/lib/dist/core/chamfer.d.ts +5 -4
  41. package/lib/dist/core/chamfer.js +7 -6
  42. package/lib/dist/core/color.d.ts +3 -2
  43. package/lib/dist/core/color.js +2 -1
  44. package/lib/dist/core/cut.d.ts +4 -3
  45. package/lib/dist/core/cut.js +5 -4
  46. package/lib/dist/core/cylinder.d.ts +2 -1
  47. package/lib/dist/core/cylinder.js +2 -1
  48. package/lib/dist/core/draft.d.ts +3 -2
  49. package/lib/dist/core/draft.js +3 -2
  50. package/lib/dist/core/extrude.d.ts +4 -3
  51. package/lib/dist/core/extrude.js +5 -4
  52. package/lib/dist/core/fillet.d.ts +5 -4
  53. package/lib/dist/core/fillet.js +6 -5
  54. package/lib/dist/core/index.d.ts +1 -0
  55. package/lib/dist/core/index.js +1 -0
  56. package/lib/dist/core/interfaces.d.ts +25 -24
  57. package/lib/dist/core/param.d.ts +74 -0
  58. package/lib/dist/core/param.js +147 -0
  59. package/lib/dist/core/repeat.d.ts +2 -1
  60. package/lib/dist/core/repeat.js +10 -8
  61. package/lib/dist/core/revolve.d.ts +2 -1
  62. package/lib/dist/core/revolve.js +3 -2
  63. package/lib/dist/core/rib.d.ts +3 -2
  64. package/lib/dist/core/rib.js +6 -2
  65. package/lib/dist/core/rotate.d.ts +5 -4
  66. package/lib/dist/core/rotate.js +4 -3
  67. package/lib/dist/core/shell.d.ts +3 -2
  68. package/lib/dist/core/shell.js +3 -2
  69. package/lib/dist/core/sphere.d.ts +3 -2
  70. package/lib/dist/core/sphere.js +2 -1
  71. package/lib/dist/core/translate.d.ts +7 -6
  72. package/lib/dist/core/translate.js +6 -5
  73. package/lib/dist/features/2d/arc.js +5 -5
  74. package/lib/dist/features/2d/bezier.js +16 -16
  75. package/lib/dist/features/2d/circle.js +4 -0
  76. package/lib/dist/features/2d/ellipse.js +4 -0
  77. package/lib/dist/features/2d/hline.d.ts +3 -0
  78. package/lib/dist/features/2d/hline.js +9 -2
  79. package/lib/dist/features/2d/line.d.ts +3 -0
  80. package/lib/dist/features/2d/line.js +11 -3
  81. package/lib/dist/features/2d/sketch.js +5 -1
  82. package/lib/dist/features/2d/slot.d.ts +5 -0
  83. package/lib/dist/features/2d/slot.js +52 -7
  84. package/lib/dist/features/2d/tarc-to-point-tangent.js +3 -0
  85. package/lib/dist/features/2d/tarc-to-point.js +3 -0
  86. package/lib/dist/features/2d/tarc-with-tangent.js +3 -0
  87. package/lib/dist/features/2d/tarc.js +3 -0
  88. package/lib/dist/features/2d/vline.d.ts +3 -0
  89. package/lib/dist/features/2d/vline.js +9 -2
  90. package/lib/dist/features/copy-circular.d.ts +4 -3
  91. package/lib/dist/features/copy-circular.js +16 -9
  92. package/lib/dist/features/copy-circular2d.js +16 -9
  93. package/lib/dist/features/copy-linear.d.ts +4 -3
  94. package/lib/dist/features/copy-linear.js +18 -12
  95. package/lib/dist/features/copy-linear2d.js +18 -12
  96. package/lib/dist/features/extrude-base.d.ts +4 -3
  97. package/lib/dist/features/extrude-base.js +10 -3
  98. package/lib/dist/features/mirror-shape2d.js +2 -2
  99. package/lib/dist/features/repeat-base.d.ts +13 -0
  100. package/lib/dist/features/repeat-base.js +21 -0
  101. package/lib/dist/features/repeat-circular.d.ts +6 -5
  102. package/lib/dist/features/repeat-circular.js +3 -6
  103. package/lib/dist/features/repeat-linear.d.ts +7 -7
  104. package/lib/dist/features/repeat-linear.js +3 -6
  105. package/lib/dist/index.d.ts +5 -0
  106. package/lib/dist/index.js +8 -1
  107. package/lib/dist/io/file-import.d.ts +7 -0
  108. package/lib/dist/io/file-import.js +30 -10
  109. package/lib/dist/math/lazy-matrix.d.ts +5 -0
  110. package/lib/dist/math/lazy-matrix.js +78 -10
  111. package/lib/dist/oc/boolean-ops.d.ts +2 -2
  112. package/lib/dist/param-registry.d.ts +34 -0
  113. package/lib/dist/param-registry.js +60 -0
  114. package/lib/dist/rendering/mesh-builder.js +2 -1
  115. package/lib/dist/tests/features/copy-circular.test.js +1 -1
  116. package/lib/dist/tests/features/copy-linear.test.js +10 -10
  117. package/lib/dist/tests/features/repeat-user-repro-cache.test.d.ts +1 -0
  118. package/lib/dist/tests/features/repeat-user-repro-cache.test.js +97 -0
  119. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  120. package/llm-docs/api/bezier.md +10 -11
  121. package/llm-docs/api/index.json +1 -1
  122. package/llm-docs/api/types/arc-points.md +2 -2
  123. package/llm-docs/api/types/cut.md +10 -10
  124. package/llm-docs/api/types/extrude.md +10 -10
  125. package/llm-docs/api/types/loft.md +6 -6
  126. package/llm-docs/api/types/revolve.md +6 -6
  127. package/llm-docs/api/types/rib.md +2 -2
  128. package/llm-docs/api/types/slot.md +2 -2
  129. package/llm-docs/api/types/sweep.md +10 -10
  130. package/llm-docs/api/types/transformable.md +14 -14
  131. package/llm-docs/index.json +12 -12
  132. package/mcp/dist/client.d.ts +1 -0
  133. package/mcp/dist/client.js +8 -1
  134. package/mcp/dist/server.js +14 -1
  135. package/mcp/dist/tools/engine.d.ts +16 -0
  136. package/mcp/dist/tools/engine.js +45 -0
  137. package/package.json +9 -3
  138. package/server/dist/api.d.ts +37 -0
  139. package/server/dist/api.js +44 -0
  140. package/server/dist/code-editor.d.ts +64 -0
  141. package/server/dist/code-editor.js +520 -2
  142. package/server/dist/fluidcad-server.d.ts +87 -1
  143. package/server/dist/fluidcad-server.js +254 -88
  144. package/server/dist/host/blocked-imports.d.ts +8 -0
  145. package/server/dist/host/blocked-imports.js +30 -0
  146. package/server/dist/{vite-manager.d.ts → host/local-scene-host.d.ts} +3 -1
  147. package/server/dist/{vite-manager.js → host/local-scene-host.js} +6 -26
  148. package/server/dist/host/scene-host.d.ts +19 -0
  149. package/server/dist/host/scene-host.js +1 -0
  150. package/server/dist/index.js +24 -117
  151. package/server/dist/model-package/capture-params.d.ts +19 -0
  152. package/server/dist/model-package/capture-params.js +42 -0
  153. package/server/dist/model-package/pack.d.ts +23 -0
  154. package/server/dist/model-package/pack.js +230 -0
  155. package/server/dist/model-package/types.d.ts +79 -0
  156. package/server/dist/model-package/types.js +17 -0
  157. package/server/dist/routes/hit-test.d.ts +3 -0
  158. package/server/dist/routes/hit-test.js +17 -0
  159. package/server/dist/routes/pack.d.ts +10 -0
  160. package/server/dist/routes/pack.js +47 -0
  161. package/server/dist/routes/params.d.ts +3 -0
  162. package/server/dist/routes/params.js +75 -0
  163. package/server/dist/routes/sketch-edits.d.ts +3 -0
  164. package/server/dist/routes/sketch-edits.js +542 -0
  165. package/server/dist/routes/timeline.d.ts +3 -0
  166. package/server/dist/routes/timeline.js +49 -0
  167. package/server/dist/server-core.d.ts +53 -0
  168. package/server/dist/server-core.js +147 -0
  169. package/server/dist/ws-protocol.d.ts +101 -2
  170. package/ui/dist/assets/index-CDJmUpFI.css +2 -0
  171. package/ui/dist/assets/index-MRqwG9Vh.js +5417 -0
  172. package/ui/dist/index.html +2 -2
  173. package/server/dist/routes/actions.d.ts +0 -3
  174. package/server/dist/routes/actions.js +0 -309
  175. package/ui/dist/assets/index-BdqrMDRu.js +0 -4946
  176. package/ui/dist/assets/index-DR7c2Qk9.css +0 -2
@@ -114,12 +114,22 @@ function findPickCallInChain(call) {
114
114
  }
115
115
  return null;
116
116
  }
117
+ /**
118
+ * Structural AST check: is this node a `[x, y]` point array?
119
+ * Accepts any two-element array regardless of whether the elements are
120
+ * literals, variables, or expressions — the drag/splice functions only need
121
+ * to *locate* point nodes, not read their old values.
122
+ */
123
+ function isPointArray(node) {
124
+ return node.type === 'array' && node.namedChildren.length === 2;
125
+ }
117
126
  /**
118
127
  * Extract `[x, y]` from an `array` node with exactly two numeric children.
119
- * Handles unary minus (`-5`) because tree-sitter wraps it in a `unary_expression`.
128
+ * Only used where the actual numeric values are needed (e.g. `removePoint`
129
+ * distance computation). Drag/update paths use `isPointArray` instead.
120
130
  */
121
131
  function parsePointLiteral(node) {
122
- if (node.type !== 'array' || node.namedChildren.length !== 2) {
132
+ if (!isPointArray(node)) {
123
133
  return null;
124
134
  }
125
135
  const parts = [];
@@ -132,6 +142,43 @@ function parsePointLiteral(node) {
132
142
  }
133
143
  return [parts[0], parts[1]];
134
144
  }
145
+ function isPointLikeArg(node) {
146
+ if (node.type === 'number')
147
+ return false;
148
+ if (node.type === 'string' || node.type === 'template_string')
149
+ return false;
150
+ if (node.type === 'true' || node.type === 'false')
151
+ return false;
152
+ if (node.type === 'unary_expression' && node.namedChildren[0]?.type === 'number')
153
+ return false;
154
+ return true;
155
+ }
156
+ function collectChainPointArgs(call) {
157
+ const calls = [];
158
+ let current = call;
159
+ while (current && current.type === 'call_expression') {
160
+ calls.push(current);
161
+ const fn = current.childForFieldName('function');
162
+ if (fn && fn.type === 'member_expression') {
163
+ current = fn.childForFieldName('object');
164
+ }
165
+ else {
166
+ break;
167
+ }
168
+ }
169
+ const pointArgs = [];
170
+ for (let i = calls.length - 1; i >= 0; i--) {
171
+ const args = getArgumentsNode(calls[i]);
172
+ if (args) {
173
+ for (const child of args.namedChildren) {
174
+ if (isPointLikeArg(child)) {
175
+ pointArgs.push(child);
176
+ }
177
+ }
178
+ }
179
+ }
180
+ return pointArgs;
181
+ }
135
182
  function spliceCode(code, startIndex, endIndex, replacement) {
136
183
  return code.slice(0, startIndex) + replacement + code.slice(endIndex);
137
184
  }
@@ -553,3 +600,474 @@ export function setPickPoints(code, sourceLine, points) {
553
600
  return spliceCode(code, args.startIndex + 1, args.endIndex - 1, newArgs);
554
601
  });
555
602
  }
603
+ // ---------------------------------------------------------------------------
604
+ // Geometry insertion — insert a new call expression at the end of a sketch body
605
+ // ---------------------------------------------------------------------------
606
+ /**
607
+ * Find the callback body (statement_block) inside a sketch() call.
608
+ * Looks for the last arrow_function or function argument.
609
+ */
610
+ function findSketchBody(call) {
611
+ const args = getArgumentsNode(call);
612
+ if (!args) {
613
+ return null;
614
+ }
615
+ for (let i = args.namedChildren.length - 1; i >= 0; i--) {
616
+ const child = args.namedChildren[i];
617
+ if (child.type === 'arrow_function' || child.type === 'function') {
618
+ const body = child.childForFieldName('body');
619
+ if (body && body.type === 'statement_block') {
620
+ return body;
621
+ }
622
+ }
623
+ }
624
+ return null;
625
+ }
626
+ /**
627
+ * Ensure a symbol is present in the `import { ... } from 'fluidcad'` or
628
+ * `'fluidcad/core'` statement. Returns modified code if the symbol was added.
629
+ */
630
+ async function ensureSymbolImport(code, symbol) {
631
+ const p = await getParser();
632
+ const tree = p.parse(code);
633
+ const importNode = findFluidCadImport(tree);
634
+ if (!importNode) {
635
+ return `import { ${symbol} } from 'fluidcad/core';\n` + code;
636
+ }
637
+ const namedImports = findNamedImports(importNode);
638
+ if (!namedImports) {
639
+ return code;
640
+ }
641
+ for (const spec of namedImports.namedChildren) {
642
+ if (spec.type !== 'import_specifier') {
643
+ continue;
644
+ }
645
+ const name = spec.childForFieldName('name') ?? spec.namedChild(0);
646
+ if (name && name.text === symbol) {
647
+ return code;
648
+ }
649
+ }
650
+ const openBraceOffset = namedImports.startIndex + 1;
651
+ const after = code[openBraceOffset];
652
+ const needsSpace = after !== ' ' && after !== '\t' && after !== '\n';
653
+ const insertText = needsSpace ? ` ${symbol},` : `${symbol},`;
654
+ return code.slice(0, openBraceOffset) + insertText + code.slice(openBraceOffset);
655
+ }
656
+ /**
657
+ * Insert a new geometry call expression at the end of a sketch's callback body.
658
+ *
659
+ * @param code - Full source code
660
+ * @param sketchSourceLine - 1-indexed line where the sketch() call starts
661
+ * @param statement - The call to insert, e.g. "line([5, 10], [20, 30])"
662
+ */
663
+ export async function insertGeometryCall(code, sketchSourceLine, statement) {
664
+ const p = await getParser();
665
+ const tree = p.parse(code);
666
+ const lines = splitLines(code);
667
+ const call = findEditableCallAt(tree, lines, sketchSourceLine);
668
+ if (!call) {
669
+ return { newCode: code };
670
+ }
671
+ const body = findSketchBody(call);
672
+ if (!body) {
673
+ return { newCode: code };
674
+ }
675
+ const bodyChildren = body.namedChildren;
676
+ let insertRow;
677
+ let indent;
678
+ if (bodyChildren.length > 0) {
679
+ const lastStmt = bodyChildren[bodyChildren.length - 1];
680
+ insertRow = lastStmt.endPosition.row + 1;
681
+ indent = indentOf(lines, lastStmt.startPosition.row);
682
+ }
683
+ else {
684
+ insertRow = body.startPosition.row + 1;
685
+ indent = indentOf(lines, body.startPosition.row) + ' ';
686
+ }
687
+ const newLine = statement.split('\n').map(l => `${indent}${l}`).join('\n');
688
+ lines.splice(insertRow, 0, newLine);
689
+ let result = joinLines(lines);
690
+ const funcName = statement.match(/^(\w+)\s*\(/)?.[1];
691
+ if (funcName) {
692
+ result = await ensureSymbolImport(result, funcName);
693
+ }
694
+ return { newCode: result };
695
+ }
696
+ /**
697
+ * Update a point argument of a geometry call.
698
+ *
699
+ * @param code - Full source code
700
+ * @param sourceLine - 1-indexed line of the geometry call
701
+ * @param newPosition - New [x, y] position
702
+ * @param pointIndex - Which point argument to update (0 = first, -1 = last)
703
+ */
704
+ export async function updateGeometryPosition(code, sourceLine, newPosition, pointIndex = 0) {
705
+ return withParsedCode(code, (tree, lines) => {
706
+ const call = findEditableCallAt(tree, lines, sourceLine);
707
+ if (!call) {
708
+ return null;
709
+ }
710
+ const pointText = `[${newPosition[0]}, ${newPosition[1]}]`;
711
+ const pointArgs = collectChainPointArgs(call);
712
+ const targetIdx = pointIndex >= 0 ? pointIndex : pointArgs.length + pointIndex;
713
+ if (targetIdx >= 0 && targetIdx < pointArgs.length) {
714
+ return spliceCode(code, pointArgs[targetIdx].startIndex, pointArgs[targetIdx].endIndex, pointText);
715
+ }
716
+ if (pointIndex === 0 && pointArgs.length === 0) {
717
+ const args = getArgumentsNode(call);
718
+ if (!args) {
719
+ return null;
720
+ }
721
+ const firstArg = args.namedChildren[0];
722
+ if (!firstArg) {
723
+ return spliceCode(code, args.startIndex + 1, args.startIndex + 1, pointText);
724
+ }
725
+ return spliceCode(code, args.startIndex + 1, args.startIndex + 1, pointText + ', ');
726
+ }
727
+ return null;
728
+ });
729
+ }
730
+ /**
731
+ * Update both point arguments of a `line(start, end)` call atomically.
732
+ * Used by body-drag of unconstrained two-point lines, where the whole line
733
+ * is translated and both endpoints change in a single edit.
734
+ */
735
+ export async function setLinePosition(code, sourceLine, newStart, newEnd) {
736
+ return withParsedCode(code, (tree, lines) => {
737
+ const call = findEditableCallAt(tree, lines, sourceLine);
738
+ if (!call) {
739
+ return null;
740
+ }
741
+ const args = getArgumentsNode(call);
742
+ if (!args) {
743
+ return null;
744
+ }
745
+ const pointArgs = [];
746
+ for (const child of args.namedChildren) {
747
+ if (isPointArray(child)) {
748
+ pointArgs.push(child);
749
+ }
750
+ }
751
+ if (pointArgs.length < 2) {
752
+ return null;
753
+ }
754
+ const startNode = pointArgs[0];
755
+ const endNode = pointArgs[pointArgs.length - 1];
756
+ const startText = `[${newStart[0]}, ${newStart[1]}]`;
757
+ const endText = `[${newEnd[0]}, ${newEnd[1]}]`;
758
+ // Splice end first so startNode indices remain valid.
759
+ const afterEnd = spliceCode(code, endNode.startIndex, endNode.endIndex, endText);
760
+ return spliceCode(afterEnd, startNode.startIndex, startNode.endIndex, startText);
761
+ });
762
+ }
763
+ /**
764
+ * Update multiple point arguments of a geometry call chain atomically.
765
+ * Point indices refer to the collected chain points (innermost call first).
766
+ */
767
+ export async function setChainPositions(code, sourceLine, updates) {
768
+ return withParsedCode(code, (tree, lines) => {
769
+ const call = findEditableCallAt(tree, lines, sourceLine);
770
+ if (!call) {
771
+ return null;
772
+ }
773
+ const pointArgs = collectChainPointArgs(call);
774
+ if (pointArgs.length === 0) {
775
+ return null;
776
+ }
777
+ const resolved = updates
778
+ .map(u => {
779
+ const idx = u.pointIndex >= 0 ? u.pointIndex : pointArgs.length + u.pointIndex;
780
+ if (idx < 0 || idx >= pointArgs.length) {
781
+ return null;
782
+ }
783
+ return { node: pointArgs[idx], position: u.position };
784
+ })
785
+ .filter((u) => u !== null)
786
+ .sort((a, b) => b.node.startIndex - a.node.startIndex);
787
+ let result = code;
788
+ for (const { node, position } of resolved) {
789
+ const text = `[${position[0]}, ${position[1]}]`;
790
+ result = spliceCode(result, node.startIndex, node.endIndex, text);
791
+ }
792
+ return result;
793
+ });
794
+ }
795
+ /**
796
+ * Update the last non-array argument of a geometry call (e.g. distance or diameter).
797
+ * Replaces whatever expression is there (literal, variable, binary expression)
798
+ * with the new numeric literal.
799
+ */
800
+ export function updateDimension(code, sourceLine, newValue) {
801
+ return withParsedCode(code, (tree, lines) => {
802
+ const call = findEditableCallAt(tree, lines, sourceLine);
803
+ if (!call) {
804
+ return null;
805
+ }
806
+ const args = getArgumentsNode(call);
807
+ if (!args || args.namedChildren.length === 0) {
808
+ return null;
809
+ }
810
+ const target = findNonArrayArgFromEnd(args);
811
+ if (!target) {
812
+ return null;
813
+ }
814
+ return spliceCode(code, target.startIndex, target.endIndex, String(newValue));
815
+ });
816
+ }
817
+ // ---------------------------------------------------------------------------
818
+ // Expression-aware dimension helpers
819
+ // ---------------------------------------------------------------------------
820
+ function findNonArrayArgFromEnd(args, offset = 0) {
821
+ let skipped = 0;
822
+ for (let i = args.namedChildren.length - 1; i >= 0; i--) {
823
+ const child = args.namedChildren[i];
824
+ if (child.type !== 'array') {
825
+ if (skipped === offset) {
826
+ return child;
827
+ }
828
+ skipped++;
829
+ }
830
+ }
831
+ return null;
832
+ }
833
+ export async function getDimensionExpression(code, sourceLine) {
834
+ const p = await getParser();
835
+ const tree = p.parse(code);
836
+ const lines = splitLines(code);
837
+ let current = findEditableCallAt(tree, lines, sourceLine);
838
+ while (current && current.type === 'call_expression') {
839
+ const args = getArgumentsNode(current);
840
+ if (args) {
841
+ const target = findNonArrayArgFromEnd(args);
842
+ if (target) {
843
+ return { expression: target.text };
844
+ }
845
+ }
846
+ const fn = current.childForFieldName('function');
847
+ current = fn && fn.type === 'member_expression'
848
+ ? fn.childForFieldName('object')
849
+ : null;
850
+ }
851
+ return null;
852
+ }
853
+ export function updateDimensionExpression(code, sourceLine, expression, dimensionOffset = 0) {
854
+ return withParsedCode(code, (tree, lines) => {
855
+ let current = findEditableCallAt(tree, lines, sourceLine);
856
+ while (current && current.type === 'call_expression') {
857
+ const args = getArgumentsNode(current);
858
+ if (args) {
859
+ const target = findNonArrayArgFromEnd(args, dimensionOffset);
860
+ if (target) {
861
+ return spliceCode(code, target.startIndex, target.endIndex, expression);
862
+ }
863
+ }
864
+ const fn = current.childForFieldName('function');
865
+ current = fn && fn.type === 'member_expression'
866
+ ? fn.childForFieldName('object')
867
+ : null;
868
+ }
869
+ return null;
870
+ });
871
+ }
872
+ /**
873
+ * Insert `const name = initializer;` at the top of the sketch arrow-function
874
+ * body. Returns the new code and how many lines were added (for callers that
875
+ * need to re-anchor subsequent sourceLine-based edits).
876
+ */
877
+ export async function declareSketchVariable(code, sketchSourceLine, name, initializer) {
878
+ const p = await getParser();
879
+ const tree = p.parse(code);
880
+ const lines = splitLines(code);
881
+ const call = findEditableCallAt(tree, lines, sketchSourceLine);
882
+ if (!call) {
883
+ return null;
884
+ }
885
+ const body = findSketchBody(call);
886
+ if (!body) {
887
+ return null;
888
+ }
889
+ const bodyChildren = body.namedChildren;
890
+ const insertRow = body.startPosition.row + 1;
891
+ let indent;
892
+ if (bodyChildren.length > 0) {
893
+ indent = indentOf(lines, bodyChildren[0].startPosition.row);
894
+ }
895
+ else {
896
+ indent = indentOf(lines, body.startPosition.row) + ' ';
897
+ }
898
+ const newLine = `${indent}const ${name} = ${initializer};`;
899
+ lines.splice(insertRow, 0, newLine);
900
+ return { newCode: joinLines(lines), linesAdded: 1 };
901
+ }
902
+ /**
903
+ * Run an edit that may be preceded by inserting `const name = init;` at the
904
+ * top of the sketch body. The edit receives the (possibly-mutated) code and
905
+ * the number of lines added by the declaration, so it can re-anchor any
906
+ * sourceLine references inside the body.
907
+ *
908
+ * Adopt this wrapper for any new code-edit endpoint that should support
909
+ * "declare a variable on the same commit."
910
+ */
911
+ async function withOptionalVariableDeclaration(code, sketchSourceLine, newVariable, edit) {
912
+ if (!newVariable) {
913
+ return edit(code, 0);
914
+ }
915
+ const declared = await declareSketchVariable(code, sketchSourceLine, newVariable.name, newVariable.initializer);
916
+ if (!declared) {
917
+ return { newCode: code };
918
+ }
919
+ return edit(declared.newCode, declared.linesAdded);
920
+ }
921
+ export function insertGeometryCallWithVariable(code, sketchSourceLine, statement, newVariable) {
922
+ return withOptionalVariableDeclaration(code, sketchSourceLine, newVariable, (c) => insertGeometryCall(c, sketchSourceLine, statement));
923
+ }
924
+ export function updateDimensionExpressionWithVariable(code, sourceLine, expression, sketchSourceLine, newVariable, dimensionOffset = 0) {
925
+ return withOptionalVariableDeclaration(code, sketchSourceLine, newVariable, (c, shift) => updateDimensionExpression(c, sourceLine + shift, expression, dimensionOffset));
926
+ }
927
+ export async function extractVariablesInScope(code, sketchSourceLine) {
928
+ const p = await getParser();
929
+ const tree = p.parse(code);
930
+ const lines = splitLines(code);
931
+ const sketchRow = resolveSourceRow(lines, sketchSourceLine);
932
+ if (sketchRow < 0) {
933
+ return [];
934
+ }
935
+ const variables = [];
936
+ const seen = new Set();
937
+ function addVar(name, initializer) {
938
+ if (!seen.has(name)) {
939
+ seen.add(name);
940
+ variables.push({ name, initializer });
941
+ }
942
+ }
943
+ function collectDeclarators(node) {
944
+ for (const child of node.namedChildren) {
945
+ if (child.type === 'variable_declarator') {
946
+ const nameNode = child.childForFieldName('name');
947
+ const valueNode = child.childForFieldName('value');
948
+ if (nameNode && nameNode.type === 'identifier') {
949
+ const init = valueNode ? valueNode.text : undefined;
950
+ addVar(nameNode.text, init);
951
+ }
952
+ }
953
+ }
954
+ }
955
+ const FLUIDCAD_SOURCES = ['fluidcad', 'fluidcad/core', "'fluidcad'", "'fluidcad/core'", '"fluidcad"', '"fluidcad/core"'];
956
+ for (const node of tree.rootNode.namedChildren) {
957
+ if (node.startPosition.row > sketchRow) {
958
+ break;
959
+ }
960
+ if (node.type === 'import_statement') {
961
+ const source = node.childForFieldName('source');
962
+ if (source && FLUIDCAD_SOURCES.some(s => source.text.includes(s.replace(/['"]/g, '')))) {
963
+ continue;
964
+ }
965
+ for (const child of node.namedChildren) {
966
+ if (child.type === 'import_clause') {
967
+ for (const spec of child.namedChildren) {
968
+ if (spec.type === 'import_specifier' || spec.type === 'identifier') {
969
+ const nameNode = spec.type === 'import_specifier'
970
+ ? spec.childForFieldName('name') || spec.namedChildren[0]
971
+ : spec;
972
+ if (nameNode) {
973
+ addVar(nameNode.text);
974
+ }
975
+ }
976
+ else if (spec.type === 'named_imports') {
977
+ for (const imp of spec.namedChildren) {
978
+ if (imp.type === 'import_specifier') {
979
+ const alias = imp.childForFieldName('alias');
980
+ const nameN = alias || imp.childForFieldName('name') || imp.namedChildren[0];
981
+ if (nameN) {
982
+ addVar(nameN.text);
983
+ }
984
+ }
985
+ }
986
+ }
987
+ }
988
+ }
989
+ }
990
+ continue;
991
+ }
992
+ if (node.type === 'lexical_declaration' || node.type === 'variable_declaration') {
993
+ collectDeclarators(node);
994
+ continue;
995
+ }
996
+ if (node.type === 'export_statement') {
997
+ for (const child of node.namedChildren) {
998
+ if (child.type === 'lexical_declaration' || child.type === 'variable_declaration') {
999
+ collectDeclarators(child);
1000
+ }
1001
+ }
1002
+ }
1003
+ }
1004
+ const sketchCall = findEditableCallAt(tree, lines, sketchSourceLine);
1005
+ if (sketchCall) {
1006
+ const body = findSketchBody(sketchCall);
1007
+ if (body) {
1008
+ for (const stmt of body.namedChildren) {
1009
+ if (stmt.type === 'lexical_declaration' || stmt.type === 'variable_declaration') {
1010
+ collectDeclarators(stmt);
1011
+ }
1012
+ }
1013
+ }
1014
+ }
1015
+ return variables;
1016
+ }
1017
+ export function setRectDimensions(code, sourceLine, startPoint, width, height) {
1018
+ return withParsedCode(code, (tree, lines) => {
1019
+ const outerCall = findEditableCallAt(tree, lines, sourceLine);
1020
+ if (!outerCall) {
1021
+ return null;
1022
+ }
1023
+ let rectCall = null;
1024
+ let current = outerCall;
1025
+ while (current && current.type === 'call_expression') {
1026
+ const fn = current.childForFieldName('function');
1027
+ if (fn) {
1028
+ if (fn.type === 'identifier' && fn.text === 'rect') {
1029
+ rectCall = current;
1030
+ break;
1031
+ }
1032
+ if (fn.type === 'member_expression') {
1033
+ current = fn.childForFieldName('object');
1034
+ continue;
1035
+ }
1036
+ }
1037
+ break;
1038
+ }
1039
+ if (!rectCall) {
1040
+ return null;
1041
+ }
1042
+ const args = getArgumentsNode(rectCall);
1043
+ if (!args || args.namedChildren.length < 2) {
1044
+ return null;
1045
+ }
1046
+ const pointArgs = [];
1047
+ const numericArgs = [];
1048
+ for (const child of args.namedChildren) {
1049
+ if (isPointArray(child)) {
1050
+ pointArgs.push(child);
1051
+ }
1052
+ else {
1053
+ numericArgs.push(child);
1054
+ }
1055
+ }
1056
+ if (numericArgs.length < 2) {
1057
+ return null;
1058
+ }
1059
+ const edits = [];
1060
+ edits.push({ start: numericArgs[1].startIndex, end: numericArgs[1].endIndex, text: String(height) });
1061
+ edits.push({ start: numericArgs[0].startIndex, end: numericArgs[0].endIndex, text: String(width) });
1062
+ if (startPoint && pointArgs.length > 0) {
1063
+ const pointText = `[${startPoint[0]}, ${startPoint[1]}]`;
1064
+ edits.push({ start: pointArgs[0].startIndex, end: pointArgs[0].endIndex, text: pointText });
1065
+ }
1066
+ edits.sort((a, b) => b.start - a.start);
1067
+ let result = code;
1068
+ for (const edit of edits) {
1069
+ result = spliceCode(result, edit.start, edit.end, edit.text);
1070
+ }
1071
+ return result;
1072
+ });
1073
+ }
@@ -1,9 +1,34 @@
1
+ import type { SceneHost } from './host/scene-host.ts';
2
+ import type { ParamDefinition } from '../../lib/dist/index.js';
1
3
  import type { CompileError } from './ws-protocol.ts';
4
+ type SceneManager = {
5
+ startScene(): any;
6
+ renderScene(scene: any): any;
7
+ rollbackScene(scene: any, rollbackIndex: number): any;
8
+ compare(previousScene: any, currentScene: any): any;
9
+ setCurrentFile(filePath: string): void;
10
+ importFile(workspacePath: string, fileName: string, data: Uint8Array): any;
11
+ getShapeProperties(scene: any, shapeId: string): any;
12
+ getFaceProperties(scene: any, shapeId: string, faceIndex: number): any;
13
+ getEdgeProperties(scene: any, shapeId: string, edgeIndex: number): any;
14
+ hitTest(scene: any, shapeId: string, rayOrigin: [number, number, number], rayDir: [number, number, number], edgeThreshold: number): any;
15
+ exportShapes(scene: any, shapeIds: string[], options: {
16
+ format: 'step' | 'stl';
17
+ includeColors?: boolean;
18
+ resolution?: string;
19
+ customLinearDeflection?: number;
20
+ customAngularDeflectionDeg?: number;
21
+ }): {
22
+ data: string | Uint8Array;
23
+ fileName: string;
24
+ };
25
+ };
2
26
  export type SceneRenderedData = {
3
27
  absPath: string;
4
28
  result: any[];
5
29
  rollbackStop: number;
6
30
  breakpointHit?: boolean;
31
+ params?: ParamDefinition[];
7
32
  };
8
33
  export type SceneSummaryObject = {
9
34
  index: number;
@@ -40,17 +65,47 @@ export type ShapeListEntry = {
40
65
  export type ShapeList = {
41
66
  shapes: ShapeListEntry[];
42
67
  };
68
+ /**
69
+ * `sessionId` is the per-renderer state key. In desktop mode it equals the
70
+ * file path being edited (so per-file state survives switching files). In
71
+ * hub mode it's a WebSocket connection UUID (so concurrent viewers stay
72
+ * isolated). Map keys called `sessionId` accept either flavour.
73
+ */
43
74
  export declare class FluidCadServer {
44
- private viteManager;
75
+ private host;
45
76
  private sceneManager;
46
77
  private previousScenes;
47
78
  private renderingCache;
48
79
  private lastRendered;
80
+ private paramOverrides;
81
+ private sessionFiles;
82
+ private renderMutex;
49
83
  private currentFileName;
50
84
  private currentFilePath;
51
85
  private lastRollbackStop;
52
86
  private compileError;
87
+ constructor(host?: SceneHost);
88
+ getCurrentCode(): string | null;
53
89
  init(workspacePath: string): Promise<void>;
90
+ /**
91
+ * Capture an already-initialized SceneManager. Used by the hub-mode entry
92
+ * after running the packed bundle once to materialize the engine globals.
93
+ */
94
+ setSceneManager(manager: SceneManager): void;
95
+ /**
96
+ * Run `fn` with exclusive access to the OCC engine. The mutex is process-
97
+ * wide: in hub mode concurrent client sessions land here too. Order is
98
+ * first-come, first-served via Promise chain.
99
+ */
100
+ private serialized;
101
+ createSession(sessionId: string, entryFilePath: string): void;
102
+ destroySession(sessionId: string): void;
103
+ /**
104
+ * Re-render the session's entry, ignoring caches. Hub clients call this
105
+ * after editing a param. Returns the fresh render or null if no manager.
106
+ */
107
+ recomputeForSession(sessionId: string): Promise<SceneRenderedData | null>;
108
+ private processFileInternal;
54
109
  processFile(filePath: string, ignoreCache?: boolean): Promise<SceneRenderedData | null>;
55
110
  updateLiveCode(fileName: string, code: string): Promise<SceneRenderedData | null>;
56
111
  rollbackFromUI(index: number): Promise<SceneRenderedData | null>;
@@ -70,9 +125,32 @@ export declare class FluidCadServer {
70
125
  data: string | Uint8Array;
71
126
  fileName: string;
72
127
  } | null;
128
+ /**
129
+ * Export every solid of a hub session's latest render. The session-keyed twin
130
+ * of `exportShapes` (which reads the desktop `currentFileName`): hub mode keys
131
+ * each render's scene by `sessionId`, so exporting/downloading from a hub
132
+ * session must look it up the same way — exactly why `hitTestForSession`
133
+ * exists. Gathers all solids itself ("download the whole model"); returns null
134
+ * when the session has no rendered scene or it holds no solids (the caller maps
135
+ * that to a "nothing to export" response).
136
+ */
137
+ exportShapesForSession(sessionId: string, options: {
138
+ format: 'step' | 'stl';
139
+ includeColors?: boolean;
140
+ resolution?: string;
141
+ customLinearDeflection?: number;
142
+ customAngularDeflectionDeg?: number;
143
+ }): {
144
+ data: string | Uint8Array;
145
+ fileName: string;
146
+ } | null;
73
147
  hitTest(shapeId: string, rayOrigin: [number, number, number], rayDir: [number, number, number], edgeThreshold: number): any;
148
+ hitTestForSession(sessionId: string, shapeId: string, rayOrigin: [number, number, number], rayDir: [number, number, number], edgeThreshold: number): any;
74
149
  setCompileError(err: CompileError | null): void;
75
150
  getCompileError(): CompileError | null;
151
+ setParam(sessionId: string, label: string, value: any): void;
152
+ resetParams(sessionId: string): void;
153
+ getParamOverrides(sessionId: string): Record<string, any>;
76
154
  getCurrentFileName(): string;
77
155
  /**
78
156
  * Test-only seam: stage a scene under the given file name so the inspection
@@ -82,4 +160,12 @@ export declare class FluidCadServer {
82
160
  _setSceneForTesting(fileName: string, scene: any, rollbackStop?: number): void;
83
161
  getSceneSummary(): SceneSummary | null;
84
162
  getShapesList(): ShapeList | null;
163
+ /**
164
+ * Compose a stable cache key over the rendering inputs: the source bytes
165
+ * being rendered plus the param overrides currently in effect for the
166
+ * session. Param changes flip the hash so cached entries don't shadow a
167
+ * recompute, even when the code text is byte-identical.
168
+ */
169
+ private computeParamsHash;
85
170
  }
171
+ export {};