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
@@ -0,0 +1,47 @@
1
+ import { Router } from 'express';
2
+ import { packModel } from "../model-package/pack.js";
3
+ /**
4
+ * `POST /api/pack` — produce a `.fluidpkg` (zip) archive of the currently
5
+ * rendered file. Pulls live param overrides and the last-known camera state
6
+ * from the running server so the archive matches what the user is seeing.
7
+ * Returns the binary archive directly (application/zip).
8
+ */
9
+ export function createPackRouter(fluidCadServer, workspacePath, fluidcadVersion, getLastCameraState) {
10
+ const router = Router();
11
+ router.post('/pack', async (req, res) => {
12
+ const currentFile = fluidCadServer.getCurrentFileName();
13
+ if (!currentFile) {
14
+ res.status(404).json({ error: 'No active scene to pack' });
15
+ return;
16
+ }
17
+ const { name, description } = (req.body ?? {});
18
+ const cameraMsg = getLastCameraState();
19
+ const camera = cameraMsg
20
+ ? {
21
+ position: cameraMsg.position,
22
+ target: cameraMsg.target,
23
+ up: cameraMsg.up,
24
+ projection: cameraMsg.projection,
25
+ }
26
+ : undefined;
27
+ try {
28
+ const result = await packModel({
29
+ entryPath: currentFile,
30
+ workspacePath,
31
+ fluidcadVersion,
32
+ name,
33
+ description,
34
+ paramOverrides: fluidCadServer.getParamOverrides(currentFile),
35
+ camera,
36
+ });
37
+ res.setHeader('Content-Type', 'application/zip');
38
+ res.setHeader('X-FluidCAD-Package-Name', result.manifest.name);
39
+ res.setHeader('Content-Disposition', `attachment; filename="${encodeURIComponent(result.manifest.name)}.fluidpkg"`);
40
+ res.send(result.zip);
41
+ }
42
+ catch (err) {
43
+ res.status(500).json({ error: err?.message ?? String(err) });
44
+ }
45
+ });
46
+ return router;
47
+ }
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import type { FluidCadServer } from '../fluidcad-server.ts';
3
+ export declare function createParamsRouter(fluidCadServer: FluidCadServer, sendToExtension: (msg: any) => void, broadcastToUI: (msg: any) => void): Router;
@@ -0,0 +1,75 @@
1
+ import { Router } from 'express';
2
+ export function createParamsRouter(fluidCadServer, sendToExtension, broadcastToUI) {
3
+ const router = Router();
4
+ router.post('/recompute', async (_req, res) => {
5
+ const data = await fluidCadServer.recomputeCurrentFile();
6
+ if (!data) {
7
+ res.status(404).json({ error: 'No active scene' });
8
+ return;
9
+ }
10
+ sendToExtension({
11
+ type: 'scene-rendered',
12
+ absPath: data.absPath,
13
+ result: data.result,
14
+ rollbackStop: data.rollbackStop,
15
+ });
16
+ broadcastToUI({
17
+ type: 'scene-rendered',
18
+ result: data.result,
19
+ absPath: data.absPath,
20
+ breakpointHit: data.breakpointHit,
21
+ params: data.params,
22
+ });
23
+ res.json({ success: true });
24
+ });
25
+ router.post('/set-param', async (req, res) => {
26
+ const { label, value } = req.body;
27
+ if (typeof label !== 'string') {
28
+ res.status(400).json({ error: 'Invalid label' });
29
+ return;
30
+ }
31
+ fluidCadServer.setParam(fluidCadServer.getCurrentFileName(), label, value);
32
+ const data = await fluidCadServer.recomputeCurrentFile();
33
+ if (!data) {
34
+ res.status(404).json({ error: 'No active scene' });
35
+ return;
36
+ }
37
+ sendToExtension({
38
+ type: 'scene-rendered',
39
+ absPath: data.absPath,
40
+ result: data.result,
41
+ rollbackStop: data.rollbackStop,
42
+ });
43
+ broadcastToUI({
44
+ type: 'scene-rendered',
45
+ result: data.result,
46
+ absPath: data.absPath,
47
+ rollbackStop: data.rollbackStop,
48
+ params: data.params,
49
+ });
50
+ res.json({ success: true });
51
+ });
52
+ router.post('/reset-params', async (_req, res) => {
53
+ fluidCadServer.resetParams(fluidCadServer.getCurrentFileName());
54
+ const data = await fluidCadServer.recomputeCurrentFile();
55
+ if (!data) {
56
+ res.status(404).json({ error: 'No active scene' });
57
+ return;
58
+ }
59
+ sendToExtension({
60
+ type: 'scene-rendered',
61
+ absPath: data.absPath,
62
+ result: data.result,
63
+ rollbackStop: data.rollbackStop,
64
+ });
65
+ broadcastToUI({
66
+ type: 'scene-rendered',
67
+ result: data.result,
68
+ absPath: data.absPath,
69
+ rollbackStop: data.rollbackStop,
70
+ params: data.params,
71
+ });
72
+ res.json({ success: true });
73
+ });
74
+ return router;
75
+ }
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import type { FluidCadServer } from '../fluidcad-server.ts';
3
+ export declare function createSketchEditsRouter(fluidCadServer: FluidCadServer, sendToExtension: (msg: any) => void, workspacePath: string): Router;
@@ -0,0 +1,542 @@
1
+ import { Router } from 'express';
2
+ import { addBreakpoint, removeBreakpoint, toggleBreakpoint, clearBreakpoints, insertPoint, removePoint, addPick, removePick, setPickPoints, insertGeometryCallWithVariable, updateGeometryPosition, setLinePosition, setChainPositions, updateDimension, updateDimensionExpressionWithVariable, getDimensionExpression, extractVariablesInScope, setRectDimensions, } from "../code-editor.js";
3
+ const NEW_VAR_NAME_RE = /^[a-zA-Z_$][\w$]*$/;
4
+ function validateNewVariable(input) {
5
+ if (input === undefined || input === null) {
6
+ return null;
7
+ }
8
+ if (typeof input !== 'object') {
9
+ return false;
10
+ }
11
+ const obj = input;
12
+ if (typeof obj.name !== 'string' || !NEW_VAR_NAME_RE.test(obj.name)) {
13
+ return false;
14
+ }
15
+ if (typeof obj.initializer !== 'string' || obj.initializer.trim() === '') {
16
+ return false;
17
+ }
18
+ return { name: obj.name, initializer: obj.initializer };
19
+ }
20
+ export function createSketchEditsRouter(fluidCadServer, sendToExtension, workspacePath) {
21
+ const router = Router();
22
+ // ---------------------------------------------------------------------------
23
+ // /api/import-file — file I/O for STEP/STP imports
24
+ // ---------------------------------------------------------------------------
25
+ router.post('/import-file', async (req, res) => {
26
+ const { fileName, data } = req.body;
27
+ if (typeof fileName !== 'string' || typeof data !== 'string') {
28
+ res.status(400).json({ error: 'Invalid request body' });
29
+ return;
30
+ }
31
+ try {
32
+ await fluidCadServer.importFile(workspacePath, fileName, data);
33
+ }
34
+ catch (err) {
35
+ res.status(500).json({ error: err.message || String(err) });
36
+ return;
37
+ }
38
+ const loadName = fileName.replace(/\.(step|stp)$/i, '');
39
+ res.json({ success: true, fileName: loadName });
40
+ });
41
+ // ---------------------------------------------------------------------------
42
+ // Sketch interactive — IPC pass-through to the extension
43
+ // ---------------------------------------------------------------------------
44
+ router.post('/insert-point', (req, res) => {
45
+ const { point, sourceLocation } = req.body;
46
+ if (!Array.isArray(point) || point.length !== 2 ||
47
+ !sourceLocation || typeof sourceLocation.line !== 'number' || typeof sourceLocation.column !== 'number') {
48
+ res.status(400).json({ error: 'Invalid request body' });
49
+ return;
50
+ }
51
+ sendToExtension({
52
+ type: 'insert-point',
53
+ point: point,
54
+ sourceLocation,
55
+ });
56
+ res.json({ success: true });
57
+ });
58
+ router.post('/remove-point', (req, res) => {
59
+ const { point, sourceLocation } = req.body;
60
+ if (!Array.isArray(point) || point.length !== 2 ||
61
+ !sourceLocation || typeof sourceLocation.line !== 'number' || typeof sourceLocation.column !== 'number') {
62
+ res.status(400).json({ error: 'Invalid request body' });
63
+ return;
64
+ }
65
+ sendToExtension({
66
+ type: 'remove-point',
67
+ point: point,
68
+ sourceLocation,
69
+ });
70
+ res.json({ success: true });
71
+ });
72
+ router.post('/add-pick', (req, res) => {
73
+ const { sourceLocation } = req.body;
74
+ if (!sourceLocation || typeof sourceLocation.line !== 'number' || typeof sourceLocation.column !== 'number') {
75
+ res.status(400).json({ error: 'Invalid request body' });
76
+ return;
77
+ }
78
+ sendToExtension({
79
+ type: 'add-pick',
80
+ sourceLocation,
81
+ });
82
+ res.json({ success: true });
83
+ });
84
+ router.post('/remove-pick', (req, res) => {
85
+ const { sourceLocation } = req.body;
86
+ if (!sourceLocation || typeof sourceLocation.line !== 'number' || typeof sourceLocation.column !== 'number') {
87
+ res.status(400).json({ error: 'Invalid request body' });
88
+ return;
89
+ }
90
+ sendToExtension({
91
+ type: 'remove-pick',
92
+ sourceLocation,
93
+ });
94
+ res.json({ success: true });
95
+ });
96
+ router.post('/set-pick-points', (req, res) => {
97
+ const { points, sourceLocation } = req.body;
98
+ if (!Array.isArray(points) ||
99
+ !sourceLocation || typeof sourceLocation.line !== 'number' || typeof sourceLocation.column !== 'number') {
100
+ res.status(400).json({ error: 'Invalid request body' });
101
+ return;
102
+ }
103
+ sendToExtension({
104
+ type: 'set-pick-points',
105
+ points: points,
106
+ sourceLocation,
107
+ });
108
+ res.json({ success: true });
109
+ });
110
+ router.post('/insert-geometry', (req, res) => {
111
+ const { statement, sketchSourceLocation, newVariable } = req.body;
112
+ if (typeof statement !== 'string' ||
113
+ !sketchSourceLocation || typeof sketchSourceLocation.line !== 'number') {
114
+ res.status(400).json({ error: 'Invalid request body' });
115
+ return;
116
+ }
117
+ const nv = validateNewVariable(newVariable);
118
+ if (nv === false) {
119
+ res.status(400).json({ error: 'Invalid newVariable' });
120
+ return;
121
+ }
122
+ sendToExtension({
123
+ type: 'insert-geometry',
124
+ statement,
125
+ sketchSourceLocation,
126
+ newVariable: nv,
127
+ });
128
+ res.json({ success: true });
129
+ });
130
+ router.post('/update-position', (req, res) => {
131
+ const { newPosition, sourceLocation, pointIndex } = req.body;
132
+ if (!Array.isArray(newPosition) || newPosition.length !== 2 ||
133
+ !sourceLocation || typeof sourceLocation.line !== 'number') {
134
+ res.status(400).json({ error: 'Invalid request body' });
135
+ return;
136
+ }
137
+ sendToExtension({
138
+ type: 'update-position',
139
+ newPosition: newPosition,
140
+ sourceLocation,
141
+ pointIndex: typeof pointIndex === 'number' ? pointIndex : undefined,
142
+ });
143
+ res.json({ success: true });
144
+ });
145
+ router.post('/set-line-position', (req, res) => {
146
+ const { newStart, newEnd, sourceLocation } = req.body;
147
+ if (!Array.isArray(newStart) || newStart.length !== 2 ||
148
+ !Array.isArray(newEnd) || newEnd.length !== 2 ||
149
+ !sourceLocation || typeof sourceLocation.line !== 'number') {
150
+ res.status(400).json({ error: 'Invalid request body' });
151
+ return;
152
+ }
153
+ sendToExtension({
154
+ type: 'set-line-position',
155
+ newStart: newStart,
156
+ newEnd: newEnd,
157
+ sourceLocation,
158
+ });
159
+ res.json({ success: true });
160
+ });
161
+ router.post('/set-chain-positions', (req, res) => {
162
+ const { updates, sourceLocation } = req.body;
163
+ if (!Array.isArray(updates) || updates.length === 0 ||
164
+ !sourceLocation || typeof sourceLocation.line !== 'number') {
165
+ res.status(400).json({ error: 'Invalid request body' });
166
+ return;
167
+ }
168
+ sendToExtension({
169
+ type: 'set-chain-positions',
170
+ updates,
171
+ sourceLocation,
172
+ });
173
+ res.json({ success: true });
174
+ });
175
+ router.post('/update-dimension', (req, res) => {
176
+ const { newValue, sourceLocation } = req.body;
177
+ if (typeof newValue !== 'number' ||
178
+ !sourceLocation || typeof sourceLocation.line !== 'number') {
179
+ res.status(400).json({ error: 'Invalid request body' });
180
+ return;
181
+ }
182
+ sendToExtension({
183
+ type: 'update-dimension',
184
+ newValue,
185
+ sourceLocation,
186
+ });
187
+ res.json({ success: true });
188
+ });
189
+ router.post('/update-dimension-expression', (req, res) => {
190
+ const { expression, sourceLocation, sketchSourceLine, newVariable, dimensionOffset } = req.body;
191
+ if (typeof expression !== 'string' ||
192
+ !sourceLocation || typeof sourceLocation.line !== 'number') {
193
+ res.status(400).json({ error: 'Invalid request body' });
194
+ return;
195
+ }
196
+ const nv = validateNewVariable(newVariable);
197
+ if (nv === false) {
198
+ res.status(400).json({ error: 'Invalid newVariable' });
199
+ return;
200
+ }
201
+ if (nv && typeof sketchSourceLine !== 'number') {
202
+ res.status(400).json({ error: 'sketchSourceLine required when newVariable is provided' });
203
+ return;
204
+ }
205
+ sendToExtension({
206
+ type: 'update-dimension-expression',
207
+ expression,
208
+ sourceLocation,
209
+ sketchSourceLine: typeof sketchSourceLine === 'number' ? sketchSourceLine : null,
210
+ newVariable: nv,
211
+ dimensionOffset: typeof dimensionOffset === 'number' ? dimensionOffset : 0,
212
+ });
213
+ res.json({ success: true });
214
+ });
215
+ router.post('/set-rect-dimensions', (req, res) => {
216
+ const { startPoint, width, height, sourceLocation } = req.body;
217
+ if (typeof width !== 'number' || typeof height !== 'number' ||
218
+ !sourceLocation || typeof sourceLocation.line !== 'number') {
219
+ res.status(400).json({ error: 'Invalid request body' });
220
+ return;
221
+ }
222
+ const sp = Array.isArray(startPoint) && startPoint.length === 2 ? startPoint : null;
223
+ sendToExtension({
224
+ type: 'set-rect-dimensions',
225
+ startPoint: sp,
226
+ width,
227
+ height,
228
+ sourceLocation,
229
+ });
230
+ res.json({ success: true });
231
+ });
232
+ // ---------------------------------------------------------------------------
233
+ // Sketch queries — read current code and answer; no mutation, but only
234
+ // useful to the sketch tooling so categorized here.
235
+ // ---------------------------------------------------------------------------
236
+ router.post('/scope-variables', async (req, res) => {
237
+ const { sketchSourceLine } = req.body;
238
+ if (typeof sketchSourceLine !== 'number') {
239
+ res.status(400).json({ error: 'Invalid request body' });
240
+ return;
241
+ }
242
+ const code = fluidCadServer.getCurrentCode();
243
+ if (!code) {
244
+ res.json({ variables: [] });
245
+ return;
246
+ }
247
+ try {
248
+ const variables = await extractVariablesInScope(code, sketchSourceLine);
249
+ res.json({ variables });
250
+ }
251
+ catch (err) {
252
+ res.status(500).json({ error: err?.message || String(err) });
253
+ }
254
+ });
255
+ router.post('/dimension-expression', async (req, res) => {
256
+ const { sourceLine } = req.body;
257
+ if (typeof sourceLine !== 'number') {
258
+ res.status(400).json({ error: 'Invalid request body' });
259
+ return;
260
+ }
261
+ const code = fluidCadServer.getCurrentCode();
262
+ if (!code) {
263
+ res.json({ expression: null });
264
+ return;
265
+ }
266
+ try {
267
+ const result = await getDimensionExpression(code, sourceLine);
268
+ res.json({ expression: result?.expression ?? null });
269
+ }
270
+ catch (err) {
271
+ res.status(500).json({ error: err?.message || String(err) });
272
+ }
273
+ });
274
+ // ---------------------------------------------------------------------------
275
+ // /api/code/* — extensions send the current buffer text plus operation
276
+ // params; the server returns the fully edited text. All source-text
277
+ // manipulation lives here so VSCode and Neovim share one implementation.
278
+ // ---------------------------------------------------------------------------
279
+ router.post('/code/add-breakpoint', async (req, res) => {
280
+ const { code, referenceRow } = req.body;
281
+ if (typeof code !== 'string' || typeof referenceRow !== 'number') {
282
+ res.status(400).json({ error: 'Invalid request body' });
283
+ return;
284
+ }
285
+ try {
286
+ const result = await addBreakpoint(code, referenceRow);
287
+ res.json(result);
288
+ }
289
+ catch (err) {
290
+ res.status(500).json({ error: err?.message || String(err) });
291
+ }
292
+ });
293
+ router.post('/code/remove-breakpoint', async (req, res) => {
294
+ const { code, line } = req.body;
295
+ if (typeof code !== 'string' || typeof line !== 'number') {
296
+ res.status(400).json({ error: 'Invalid request body' });
297
+ return;
298
+ }
299
+ try {
300
+ const result = await removeBreakpoint(code, line);
301
+ res.json(result);
302
+ }
303
+ catch (err) {
304
+ res.status(500).json({ error: err?.message || String(err) });
305
+ }
306
+ });
307
+ router.post('/code/toggle-breakpoint', async (req, res) => {
308
+ const { code, cursorRow } = req.body;
309
+ if (typeof code !== 'string' || typeof cursorRow !== 'number') {
310
+ res.status(400).json({ error: 'Invalid request body' });
311
+ return;
312
+ }
313
+ try {
314
+ const result = await toggleBreakpoint(code, cursorRow);
315
+ res.json(result);
316
+ }
317
+ catch (err) {
318
+ res.status(500).json({ error: err?.message || String(err) });
319
+ }
320
+ });
321
+ router.post('/code/clear-breakpoints', async (req, res) => {
322
+ const { code } = req.body;
323
+ if (typeof code !== 'string') {
324
+ res.status(400).json({ error: 'Invalid request body' });
325
+ return;
326
+ }
327
+ try {
328
+ const result = await clearBreakpoints(code);
329
+ res.json(result);
330
+ }
331
+ catch (err) {
332
+ res.status(500).json({ error: err?.message || String(err) });
333
+ }
334
+ });
335
+ router.post('/code/insert-point', async (req, res) => {
336
+ const { code, sourceLine, point } = req.body;
337
+ if (typeof code !== 'string' || typeof sourceLine !== 'number' ||
338
+ !Array.isArray(point) || point.length !== 2) {
339
+ res.status(400).json({ error: 'Invalid request body' });
340
+ return;
341
+ }
342
+ try {
343
+ const result = await insertPoint(code, sourceLine, point);
344
+ res.json(result);
345
+ }
346
+ catch (err) {
347
+ res.status(500).json({ error: err?.message || String(err) });
348
+ }
349
+ });
350
+ router.post('/code/remove-point', async (req, res) => {
351
+ const { code, sourceLine, point } = req.body;
352
+ if (typeof code !== 'string' || typeof sourceLine !== 'number' ||
353
+ !Array.isArray(point) || point.length !== 2) {
354
+ res.status(400).json({ error: 'Invalid request body' });
355
+ return;
356
+ }
357
+ try {
358
+ const result = await removePoint(code, sourceLine, point);
359
+ res.json(result);
360
+ }
361
+ catch (err) {
362
+ res.status(500).json({ error: err?.message || String(err) });
363
+ }
364
+ });
365
+ router.post('/code/add-pick', async (req, res) => {
366
+ const { code, sourceLine } = req.body;
367
+ if (typeof code !== 'string' || typeof sourceLine !== 'number') {
368
+ res.status(400).json({ error: 'Invalid request body' });
369
+ return;
370
+ }
371
+ try {
372
+ const result = await addPick(code, sourceLine);
373
+ res.json(result);
374
+ }
375
+ catch (err) {
376
+ res.status(500).json({ error: err?.message || String(err) });
377
+ }
378
+ });
379
+ router.post('/code/remove-pick', async (req, res) => {
380
+ const { code, sourceLine } = req.body;
381
+ if (typeof code !== 'string' || typeof sourceLine !== 'number') {
382
+ res.status(400).json({ error: 'Invalid request body' });
383
+ return;
384
+ }
385
+ try {
386
+ const result = await removePick(code, sourceLine);
387
+ res.json(result);
388
+ }
389
+ catch (err) {
390
+ res.status(500).json({ error: err?.message || String(err) });
391
+ }
392
+ });
393
+ router.post('/code/goto-source', (req, res) => {
394
+ const { filePath, line, column } = req.body;
395
+ if (typeof filePath !== 'string' ||
396
+ typeof line !== 'number' ||
397
+ typeof column !== 'number') {
398
+ res.status(400).json({ error: 'Invalid request body' });
399
+ return;
400
+ }
401
+ sendToExtension({ type: 'goto-source', filePath, line, column });
402
+ res.json({ success: true });
403
+ });
404
+ router.post('/code/set-pick-points', async (req, res) => {
405
+ const { code, sourceLine, points } = req.body;
406
+ if (typeof code !== 'string' || typeof sourceLine !== 'number' ||
407
+ !Array.isArray(points)) {
408
+ res.status(400).json({ error: 'Invalid request body' });
409
+ return;
410
+ }
411
+ try {
412
+ const result = await setPickPoints(code, sourceLine, points);
413
+ res.json(result);
414
+ }
415
+ catch (err) {
416
+ res.status(500).json({ error: err?.message || String(err) });
417
+ }
418
+ });
419
+ router.post('/code/insert-geometry', async (req, res) => {
420
+ const { code, sketchSourceLine, statement, newVariable } = req.body;
421
+ if (typeof code !== 'string' || typeof sketchSourceLine !== 'number' ||
422
+ typeof statement !== 'string') {
423
+ res.status(400).json({ error: 'Invalid request body' });
424
+ return;
425
+ }
426
+ const nv = validateNewVariable(newVariable);
427
+ if (nv === false) {
428
+ res.status(400).json({ error: 'Invalid newVariable' });
429
+ return;
430
+ }
431
+ try {
432
+ const result = await insertGeometryCallWithVariable(code, sketchSourceLine, statement, nv);
433
+ res.json(result);
434
+ }
435
+ catch (err) {
436
+ res.status(500).json({ error: err?.message || String(err) });
437
+ }
438
+ });
439
+ router.post('/code/update-position', async (req, res) => {
440
+ const { code, sourceLine, newPosition, pointIndex } = req.body;
441
+ if (typeof code !== 'string' || typeof sourceLine !== 'number' ||
442
+ !Array.isArray(newPosition) || newPosition.length !== 2) {
443
+ res.status(400).json({ error: 'Invalid request body' });
444
+ return;
445
+ }
446
+ try {
447
+ const result = await updateGeometryPosition(code, sourceLine, newPosition, typeof pointIndex === 'number' ? pointIndex : 0);
448
+ res.json(result);
449
+ }
450
+ catch (err) {
451
+ res.status(500).json({ error: err?.message || String(err) });
452
+ }
453
+ });
454
+ router.post('/code/set-line-position', async (req, res) => {
455
+ const { code, sourceLine, newStart, newEnd } = req.body;
456
+ if (typeof code !== 'string' || typeof sourceLine !== 'number' ||
457
+ !Array.isArray(newStart) || newStart.length !== 2 ||
458
+ !Array.isArray(newEnd) || newEnd.length !== 2) {
459
+ res.status(400).json({ error: 'Invalid request body' });
460
+ return;
461
+ }
462
+ try {
463
+ const result = await setLinePosition(code, sourceLine, newStart, newEnd);
464
+ res.json(result);
465
+ }
466
+ catch (err) {
467
+ res.status(500).json({ error: err?.message || String(err) });
468
+ }
469
+ });
470
+ router.post('/code/set-chain-positions', async (req, res) => {
471
+ const { code, sourceLine, updates } = req.body;
472
+ if (typeof code !== 'string' || typeof sourceLine !== 'number' ||
473
+ !Array.isArray(updates) || updates.length === 0) {
474
+ res.status(400).json({ error: 'Invalid request body' });
475
+ return;
476
+ }
477
+ try {
478
+ const result = await setChainPositions(code, sourceLine, updates);
479
+ res.json(result);
480
+ }
481
+ catch (err) {
482
+ res.status(500).json({ error: err?.message || String(err) });
483
+ }
484
+ });
485
+ router.post('/code/update-dimension', async (req, res) => {
486
+ const { code, sourceLine, newValue } = req.body;
487
+ if (typeof code !== 'string' || typeof sourceLine !== 'number' ||
488
+ typeof newValue !== 'number') {
489
+ res.status(400).json({ error: 'Invalid request body' });
490
+ return;
491
+ }
492
+ try {
493
+ const result = await updateDimension(code, sourceLine, newValue);
494
+ res.json(result);
495
+ }
496
+ catch (err) {
497
+ res.status(500).json({ error: err?.message || String(err) });
498
+ }
499
+ });
500
+ router.post('/code/update-dimension-expression', async (req, res) => {
501
+ const { code, sourceLine, expression, sketchSourceLine, newVariable, dimensionOffset } = req.body;
502
+ if (typeof code !== 'string' || typeof sourceLine !== 'number' ||
503
+ typeof expression !== 'string') {
504
+ res.status(400).json({ error: 'Invalid request body' });
505
+ return;
506
+ }
507
+ const nv = validateNewVariable(newVariable);
508
+ if (nv === false) {
509
+ res.status(400).json({ error: 'Invalid newVariable' });
510
+ return;
511
+ }
512
+ if (nv && typeof sketchSourceLine !== 'number') {
513
+ res.status(400).json({ error: 'sketchSourceLine required when newVariable is provided' });
514
+ return;
515
+ }
516
+ const offset = typeof dimensionOffset === 'number' ? dimensionOffset : 0;
517
+ try {
518
+ const result = await updateDimensionExpressionWithVariable(code, sourceLine, expression, typeof sketchSourceLine === 'number' ? sketchSourceLine : sourceLine, nv, offset);
519
+ res.json(result);
520
+ }
521
+ catch (err) {
522
+ res.status(500).json({ error: err?.message || String(err) });
523
+ }
524
+ });
525
+ router.post('/code/set-rect-dimensions', async (req, res) => {
526
+ const { code, sourceLine, startPoint, width, height } = req.body;
527
+ if (typeof code !== 'string' || typeof sourceLine !== 'number' ||
528
+ typeof width !== 'number' || typeof height !== 'number') {
529
+ res.status(400).json({ error: 'Invalid request body' });
530
+ return;
531
+ }
532
+ const sp = Array.isArray(startPoint) && startPoint.length === 2 ? startPoint : null;
533
+ try {
534
+ const result = await setRectDimensions(code, sourceLine, sp, width, height);
535
+ res.json(result);
536
+ }
537
+ catch (err) {
538
+ res.status(500).json({ error: err?.message || String(err) });
539
+ }
540
+ });
541
+ return router;
542
+ }
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import type { FluidCadServer } from '../fluidcad-server.ts';
3
+ export declare function createTimelineRouter(fluidCadServer: FluidCadServer, sendToExtension: (msg: any) => void, broadcastToUI: (msg: any) => void): Router;