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.
- package/README.md +69 -0
- package/bin/commands/login.js +148 -0
- package/bin/commands/mcp.js +3 -2
- package/bin/commands/pack.js +49 -0
- package/bin/commands/publish.js +231 -0
- package/bin/fluidcad.js +6 -0
- package/bin/lib/api-client.js +48 -0
- package/bin/lib/browser.js +16 -0
- package/bin/lib/config.js +39 -0
- package/bin/lib/model-config.js +61 -0
- package/bin/lib/prompt.js +97 -0
- package/bin/lib/workspace.js +57 -0
- package/lib/dist/common/shape-factory.d.ts +2 -1
- package/lib/dist/common/shape-factory.js +4 -0
- package/lib/dist/common/transformable-primitive.d.ts +6 -5
- package/lib/dist/common/transformable-primitive.js +8 -7
- package/lib/dist/common/vertex.js +0 -1
- package/lib/dist/core/2d/aline.d.ts +4 -3
- package/lib/dist/core/2d/aline.js +3 -2
- package/lib/dist/core/2d/arc.d.ts +3 -2
- package/lib/dist/core/2d/arc.js +4 -3
- package/lib/dist/core/2d/bezier.d.ts +8 -6
- package/lib/dist/core/2d/circle.d.ts +4 -3
- package/lib/dist/core/2d/circle.js +3 -2
- package/lib/dist/core/2d/ellipse.d.ts +5 -4
- package/lib/dist/core/2d/ellipse.js +5 -4
- package/lib/dist/core/2d/hline.d.ts +4 -3
- package/lib/dist/core/2d/hline.js +5 -3
- package/lib/dist/core/2d/line.js +1 -0
- package/lib/dist/core/2d/offset.d.ts +3 -2
- package/lib/dist/core/2d/offset.js +6 -5
- package/lib/dist/core/2d/polygon.d.ts +5 -4
- package/lib/dist/core/2d/polygon.js +10 -9
- package/lib/dist/core/2d/rect.d.ts +4 -3
- package/lib/dist/core/2d/rect.js +10 -9
- package/lib/dist/core/2d/slot.d.ts +14 -6
- package/lib/dist/core/2d/slot.js +19 -8
- package/lib/dist/core/2d/vline.d.ts +4 -3
- package/lib/dist/core/2d/vline.js +5 -3
- package/lib/dist/core/chamfer.d.ts +5 -4
- package/lib/dist/core/chamfer.js +7 -6
- package/lib/dist/core/color.d.ts +3 -2
- package/lib/dist/core/color.js +2 -1
- package/lib/dist/core/cut.d.ts +4 -3
- package/lib/dist/core/cut.js +5 -4
- package/lib/dist/core/cylinder.d.ts +2 -1
- package/lib/dist/core/cylinder.js +2 -1
- package/lib/dist/core/draft.d.ts +3 -2
- package/lib/dist/core/draft.js +3 -2
- package/lib/dist/core/extrude.d.ts +4 -3
- package/lib/dist/core/extrude.js +5 -4
- package/lib/dist/core/fillet.d.ts +5 -4
- package/lib/dist/core/fillet.js +6 -5
- package/lib/dist/core/index.d.ts +1 -0
- package/lib/dist/core/index.js +1 -0
- package/lib/dist/core/interfaces.d.ts +25 -24
- package/lib/dist/core/param.d.ts +74 -0
- package/lib/dist/core/param.js +147 -0
- package/lib/dist/core/repeat.d.ts +2 -1
- package/lib/dist/core/repeat.js +10 -8
- package/lib/dist/core/revolve.d.ts +2 -1
- package/lib/dist/core/revolve.js +3 -2
- package/lib/dist/core/rib.d.ts +3 -2
- package/lib/dist/core/rib.js +6 -2
- package/lib/dist/core/rotate.d.ts +5 -4
- package/lib/dist/core/rotate.js +4 -3
- package/lib/dist/core/shell.d.ts +3 -2
- package/lib/dist/core/shell.js +3 -2
- package/lib/dist/core/sphere.d.ts +3 -2
- package/lib/dist/core/sphere.js +2 -1
- package/lib/dist/core/translate.d.ts +7 -6
- package/lib/dist/core/translate.js +6 -5
- package/lib/dist/features/2d/arc.js +5 -5
- package/lib/dist/features/2d/bezier.js +16 -16
- package/lib/dist/features/2d/circle.js +4 -0
- package/lib/dist/features/2d/ellipse.js +4 -0
- package/lib/dist/features/2d/hline.d.ts +3 -0
- package/lib/dist/features/2d/hline.js +9 -2
- package/lib/dist/features/2d/line.d.ts +3 -0
- package/lib/dist/features/2d/line.js +11 -3
- package/lib/dist/features/2d/sketch.js +5 -1
- package/lib/dist/features/2d/slot.d.ts +5 -0
- package/lib/dist/features/2d/slot.js +52 -7
- package/lib/dist/features/2d/tarc-to-point-tangent.js +3 -0
- package/lib/dist/features/2d/tarc-to-point.js +3 -0
- package/lib/dist/features/2d/tarc-with-tangent.js +3 -0
- package/lib/dist/features/2d/tarc.js +3 -0
- package/lib/dist/features/2d/vline.d.ts +3 -0
- package/lib/dist/features/2d/vline.js +9 -2
- package/lib/dist/features/copy-circular.d.ts +4 -3
- package/lib/dist/features/copy-circular.js +16 -9
- package/lib/dist/features/copy-circular2d.js +16 -9
- package/lib/dist/features/copy-linear.d.ts +4 -3
- package/lib/dist/features/copy-linear.js +18 -12
- package/lib/dist/features/copy-linear2d.js +18 -12
- package/lib/dist/features/extrude-base.d.ts +4 -3
- package/lib/dist/features/extrude-base.js +10 -3
- package/lib/dist/features/mirror-shape2d.js +2 -2
- package/lib/dist/features/repeat-base.d.ts +13 -0
- package/lib/dist/features/repeat-base.js +21 -0
- package/lib/dist/features/repeat-circular.d.ts +6 -5
- package/lib/dist/features/repeat-circular.js +3 -6
- package/lib/dist/features/repeat-linear.d.ts +7 -7
- package/lib/dist/features/repeat-linear.js +3 -6
- package/lib/dist/index.d.ts +5 -0
- package/lib/dist/index.js +8 -1
- package/lib/dist/io/file-import.d.ts +7 -0
- package/lib/dist/io/file-import.js +30 -10
- package/lib/dist/math/lazy-matrix.d.ts +5 -0
- package/lib/dist/math/lazy-matrix.js +78 -10
- package/lib/dist/oc/boolean-ops.d.ts +2 -2
- package/lib/dist/param-registry.d.ts +34 -0
- package/lib/dist/param-registry.js +60 -0
- package/lib/dist/rendering/mesh-builder.js +2 -1
- package/lib/dist/tests/features/copy-circular.test.js +1 -1
- package/lib/dist/tests/features/copy-linear.test.js +10 -10
- package/lib/dist/tests/features/repeat-user-repro-cache.test.d.ts +1 -0
- package/lib/dist/tests/features/repeat-user-repro-cache.test.js +97 -0
- package/lib/dist/tsconfig.tsbuildinfo +1 -1
- package/llm-docs/api/bezier.md +10 -11
- package/llm-docs/api/index.json +1 -1
- package/llm-docs/api/types/arc-points.md +2 -2
- package/llm-docs/api/types/cut.md +10 -10
- package/llm-docs/api/types/extrude.md +10 -10
- package/llm-docs/api/types/loft.md +6 -6
- package/llm-docs/api/types/revolve.md +6 -6
- package/llm-docs/api/types/rib.md +2 -2
- package/llm-docs/api/types/slot.md +2 -2
- package/llm-docs/api/types/sweep.md +10 -10
- package/llm-docs/api/types/transformable.md +14 -14
- package/llm-docs/index.json +12 -12
- package/mcp/dist/client.d.ts +1 -0
- package/mcp/dist/client.js +8 -1
- package/mcp/dist/server.js +14 -1
- package/mcp/dist/tools/engine.d.ts +16 -0
- package/mcp/dist/tools/engine.js +45 -0
- package/package.json +9 -3
- package/server/dist/api.d.ts +37 -0
- package/server/dist/api.js +44 -0
- package/server/dist/code-editor.d.ts +64 -0
- package/server/dist/code-editor.js +520 -2
- package/server/dist/fluidcad-server.d.ts +87 -1
- package/server/dist/fluidcad-server.js +254 -88
- package/server/dist/host/blocked-imports.d.ts +8 -0
- package/server/dist/host/blocked-imports.js +30 -0
- package/server/dist/{vite-manager.d.ts → host/local-scene-host.d.ts} +3 -1
- package/server/dist/{vite-manager.js → host/local-scene-host.js} +6 -26
- package/server/dist/host/scene-host.d.ts +19 -0
- package/server/dist/host/scene-host.js +1 -0
- package/server/dist/index.js +24 -117
- package/server/dist/model-package/capture-params.d.ts +19 -0
- package/server/dist/model-package/capture-params.js +42 -0
- package/server/dist/model-package/pack.d.ts +23 -0
- package/server/dist/model-package/pack.js +230 -0
- package/server/dist/model-package/types.d.ts +79 -0
- package/server/dist/model-package/types.js +17 -0
- package/server/dist/routes/hit-test.d.ts +3 -0
- package/server/dist/routes/hit-test.js +17 -0
- package/server/dist/routes/pack.d.ts +10 -0
- package/server/dist/routes/pack.js +47 -0
- package/server/dist/routes/params.d.ts +3 -0
- package/server/dist/routes/params.js +75 -0
- package/server/dist/routes/sketch-edits.d.ts +3 -0
- package/server/dist/routes/sketch-edits.js +542 -0
- package/server/dist/routes/timeline.d.ts +3 -0
- package/server/dist/routes/timeline.js +49 -0
- package/server/dist/server-core.d.ts +53 -0
- package/server/dist/server-core.js +147 -0
- package/server/dist/ws-protocol.d.ts +101 -2
- package/ui/dist/assets/index-CDJmUpFI.css +2 -0
- package/ui/dist/assets/index-MRqwG9Vh.js +5417 -0
- package/ui/dist/index.html +2 -2
- package/server/dist/routes/actions.d.ts +0 -3
- package/server/dist/routes/actions.js +0 -309
- package/ui/dist/assets/index-BdqrMDRu.js +0 -4946
- 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
|
-
*
|
|
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
|
|
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
|
|
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 {};
|