fluidcad 0.0.32 → 0.0.34

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 (279) hide show
  1. package/README.md +3 -2
  2. package/bin/commands/init.js +55 -0
  3. package/bin/commands/mcp.js +33 -0
  4. package/bin/commands/serve.js +77 -0
  5. package/bin/fluidcad.js +15 -107
  6. package/lib/dist/common/scene-object.d.ts +4 -1
  7. package/lib/dist/common/scene-object.js +9 -2
  8. package/lib/dist/common/solid.d.ts +4 -1
  9. package/lib/dist/common/solid.js +13 -0
  10. package/lib/dist/core/2d/tarc.d.ts +20 -2
  11. package/lib/dist/core/2d/tarc.js +24 -0
  12. package/lib/dist/core/index.d.ts +2 -1
  13. package/lib/dist/core/index.js +1 -0
  14. package/lib/dist/core/interfaces.d.ts +107 -2
  15. package/lib/dist/core/load.d.ts +2 -2
  16. package/lib/dist/core/repeat.js +62 -46
  17. package/lib/dist/core/rib.d.ts +18 -0
  18. package/lib/dist/core/rib.js +37 -0
  19. package/lib/dist/features/2d/arc.d.ts +8 -2
  20. package/lib/dist/features/2d/arc.js +94 -17
  21. package/lib/dist/features/2d/back.js +3 -2
  22. package/lib/dist/features/2d/sketch.d.ts +4 -0
  23. package/lib/dist/features/2d/sketch.js +21 -0
  24. package/lib/dist/features/2d/tarc-constrained.d.ts +2 -0
  25. package/lib/dist/features/2d/tarc-constrained.js +8 -0
  26. package/lib/dist/features/2d/tarc-radius-to-object.d.ts +16 -0
  27. package/lib/dist/features/2d/tarc-radius-to-object.js +58 -0
  28. package/lib/dist/features/2d/tarc-to-object.d.ts +18 -0
  29. package/lib/dist/features/2d/tarc-to-object.js +66 -0
  30. package/lib/dist/features/2d/tarc-to-point-tangent.d.ts +2 -0
  31. package/lib/dist/features/2d/tarc-to-point-tangent.js +3 -0
  32. package/lib/dist/features/2d/tarc-to-point.d.ts +2 -0
  33. package/lib/dist/features/2d/tarc-to-point.js +3 -0
  34. package/lib/dist/features/2d/tarc-with-tangent.d.ts +2 -0
  35. package/lib/dist/features/2d/tarc-with-tangent.js +3 -0
  36. package/lib/dist/features/2d/tarc.d.ts +2 -0
  37. package/lib/dist/features/2d/tarc.js +3 -0
  38. package/lib/dist/features/extrude-base.d.ts +9 -0
  39. package/lib/dist/features/extrude-base.js +22 -0
  40. package/lib/dist/features/extrude-to-face.js +1 -5
  41. package/lib/dist/features/extrude-two-distances.js +1 -2
  42. package/lib/dist/features/extrude.js +1 -2
  43. package/lib/dist/features/load.d.ts +6 -0
  44. package/lib/dist/features/load.js +53 -1
  45. package/lib/dist/features/mirror-feature.d.ts +3 -2
  46. package/lib/dist/features/mirror-feature.js +1 -1
  47. package/lib/dist/features/repeat-circular.d.ts +3 -3
  48. package/lib/dist/features/repeat-circular.js +8 -1
  49. package/lib/dist/features/repeat-linear.d.ts +4 -2
  50. package/lib/dist/features/repeat-linear.js +10 -1
  51. package/lib/dist/features/repeat-matrix.d.ts +3 -1
  52. package/lib/dist/features/repeat-matrix.js +7 -2
  53. package/lib/dist/features/rib.d.ts +31 -0
  54. package/lib/dist/features/rib.js +321 -0
  55. package/lib/dist/features/select.d.ts +1 -0
  56. package/lib/dist/features/select.js +81 -10
  57. package/lib/dist/features/shell.d.ts +4 -1
  58. package/lib/dist/features/shell.js +14 -3
  59. package/lib/dist/filters/edge/belongs-to-face.d.ts +12 -9
  60. package/lib/dist/filters/edge/belongs-to-face.js +64 -15
  61. package/lib/dist/filters/filter-builder-base.d.ts +25 -0
  62. package/lib/dist/filters/filter-builder-base.js +47 -0
  63. package/lib/dist/filters/filter.js +39 -14
  64. package/lib/dist/filters/from-object.d.ts +4 -0
  65. package/lib/dist/filters/from-object.js +10 -0
  66. package/lib/dist/helpers/clone-transform.d.ts +2 -1
  67. package/lib/dist/helpers/scene-helpers.d.ts +1 -1
  68. package/lib/dist/helpers/scene-helpers.js +146 -12
  69. package/lib/dist/index.d.ts +7 -1
  70. package/lib/dist/index.js +3 -3
  71. package/lib/dist/io/file-import.d.ts +5 -1
  72. package/lib/dist/io/file-import.js +29 -18
  73. package/lib/dist/math/lazy-matrix.d.ts +31 -0
  74. package/lib/dist/math/lazy-matrix.js +66 -0
  75. package/lib/dist/oc/color-transfer.d.ts +19 -8
  76. package/lib/dist/oc/color-transfer.js +70 -12
  77. package/lib/dist/oc/constraints/constraint-solver-adaptor.d.ts +5 -0
  78. package/lib/dist/oc/constraints/constraint-solver-adaptor.js +16 -0
  79. package/lib/dist/oc/constraints/constraint-solver.d.ts +4 -0
  80. package/lib/dist/oc/constraints/curve/curve-constraint-solver.d.ts +4 -0
  81. package/lib/dist/oc/constraints/curve/curve-constraint-solver.js +3 -0
  82. package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.d.ts +6 -1
  83. package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.js +4 -0
  84. package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.d.ts +8 -0
  85. package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.js +111 -0
  86. package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.d.ts +8 -0
  87. package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.js +161 -0
  88. package/lib/dist/oc/extrude-ops.d.ts +2 -1
  89. package/lib/dist/oc/extrude-ops.js +51 -2
  90. package/lib/dist/oc/mesh.d.ts +9 -4
  91. package/lib/dist/oc/mesh.js +14 -13
  92. package/lib/dist/oc/rib-ops.d.ts +35 -0
  93. package/lib/dist/oc/rib-ops.js +619 -0
  94. package/lib/dist/oc/shell-ops.d.ts +2 -1
  95. package/lib/dist/oc/shell-ops.js +5 -2
  96. package/lib/dist/oc/topology-index.d.ts +6 -0
  97. package/lib/dist/oc/topology-index.js +36 -0
  98. package/lib/dist/rendering/mesh-builder.d.ts +3 -0
  99. package/lib/dist/rendering/mesh-builder.js +8 -4
  100. package/lib/dist/rendering/render-edge.d.ts +2 -1
  101. package/lib/dist/rendering/render-edge.js +2 -2
  102. package/lib/dist/rendering/render-face.d.ts +2 -1
  103. package/lib/dist/rendering/render-face.js +2 -2
  104. package/lib/dist/rendering/render-solid.d.ts +2 -1
  105. package/lib/dist/rendering/render-solid.js +3 -5
  106. package/lib/dist/rendering/render-wire.d.ts +2 -1
  107. package/lib/dist/rendering/render-wire.js +2 -2
  108. package/lib/dist/rendering/render.d.ts +4 -0
  109. package/lib/dist/rendering/render.js +50 -2
  110. package/lib/dist/rendering/scene-compare.js +3 -0
  111. package/lib/dist/rendering/scene.d.ts +1 -0
  112. package/lib/dist/rendering/scene.js +4 -0
  113. package/lib/dist/scene-manager.d.ts +4 -2
  114. package/lib/dist/scene-manager.js +12 -4
  115. package/lib/dist/tests/features/2d/arc.test.js +64 -0
  116. package/lib/dist/tests/features/2d/back.test.js +17 -1
  117. package/lib/dist/tests/features/2d/tarc.test.js +157 -0
  118. package/lib/dist/tests/features/color-lineage.test.js +18 -0
  119. package/lib/dist/tests/features/filter-positional.test.d.ts +1 -0
  120. package/lib/dist/tests/features/filter-positional.test.js +129 -0
  121. package/lib/dist/tests/features/repeat-user-repro.test.d.ts +1 -0
  122. package/lib/dist/tests/features/repeat-user-repro.test.js +60 -0
  123. package/lib/dist/tests/features/rib.test.d.ts +1 -0
  124. package/lib/dist/tests/features/rib.test.js +598 -0
  125. package/lib/dist/tests/features/shell.test.js +36 -0
  126. package/lib/dist/tests/global-setup.js +2 -1
  127. package/lib/dist/tests/helpers/extract-blocks.d.ts +9 -0
  128. package/lib/dist/tests/helpers/extract-blocks.js +56 -0
  129. package/lib/dist/tests/llm-docs-examples.test.d.ts +1 -0
  130. package/lib/dist/tests/llm-docs-examples.test.js +62 -0
  131. package/lib/dist/tests/scene-compare.test.d.ts +1 -0
  132. package/lib/dist/tests/scene-compare.test.js +77 -0
  133. package/lib/dist/tests/setup.js +2 -1
  134. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  135. package/llm-docs/.coverage-allowlist.txt +9 -0
  136. package/llm-docs/api/arc.md +48 -0
  137. package/llm-docs/api/axis.md +42 -0
  138. package/llm-docs/api/bezier.md +42 -0
  139. package/llm-docs/api/booleans.md +44 -0
  140. package/llm-docs/api/chamfer.md +40 -0
  141. package/llm-docs/api/circle.md +36 -0
  142. package/llm-docs/api/color.md +34 -0
  143. package/llm-docs/api/connect.md +41 -0
  144. package/llm-docs/api/constraint-qualifiers.md +48 -0
  145. package/llm-docs/api/copy.md +63 -0
  146. package/llm-docs/api/cursor-lines.md +50 -0
  147. package/llm-docs/api/cursor-move.md +61 -0
  148. package/llm-docs/api/cut.md +55 -0
  149. package/llm-docs/api/draft.md +36 -0
  150. package/llm-docs/api/edge-filter.md +57 -0
  151. package/llm-docs/api/ellipse.md +34 -0
  152. package/llm-docs/api/extrude.md +74 -0
  153. package/llm-docs/api/face-filter.md +61 -0
  154. package/llm-docs/api/fillet.md +51 -0
  155. package/llm-docs/api/index.json +139 -0
  156. package/llm-docs/api/line.md +42 -0
  157. package/llm-docs/api/load.md +37 -0
  158. package/llm-docs/api/local.md +38 -0
  159. package/llm-docs/api/loft.md +37 -0
  160. package/llm-docs/api/mirror.md +44 -0
  161. package/llm-docs/api/offset.md +36 -0
  162. package/llm-docs/api/part.md +40 -0
  163. package/llm-docs/api/plane.md +44 -0
  164. package/llm-docs/api/polygon.md +37 -0
  165. package/llm-docs/api/primitive-solids.md +39 -0
  166. package/llm-docs/api/project-intersect.md +48 -0
  167. package/llm-docs/api/rect.md +48 -0
  168. package/llm-docs/api/remove.md +32 -0
  169. package/llm-docs/api/repeat.md +79 -0
  170. package/llm-docs/api/revolve.md +38 -0
  171. package/llm-docs/api/rib.md +40 -0
  172. package/llm-docs/api/rotate.md +37 -0
  173. package/llm-docs/api/select.md +41 -0
  174. package/llm-docs/api/shell.md +41 -0
  175. package/llm-docs/api/sketch.md +76 -0
  176. package/llm-docs/api/slot.md +36 -0
  177. package/llm-docs/api/split-trim.md +42 -0
  178. package/llm-docs/api/sweep.md +43 -0
  179. package/llm-docs/api/tarc.md +45 -0
  180. package/llm-docs/api/tcircle.md +38 -0
  181. package/llm-docs/api/tline.md +42 -0
  182. package/llm-docs/api/translate.md +40 -0
  183. package/llm-docs/api/types/aline.md +35 -0
  184. package/llm-docs/api/types/arc-angles.md +29 -0
  185. package/llm-docs/api/types/arc-points.md +48 -0
  186. package/llm-docs/api/types/axis-like.md +38 -0
  187. package/llm-docs/api/types/axis.md +21 -0
  188. package/llm-docs/api/types/boolean-operation.md +50 -0
  189. package/llm-docs/api/types/circular-repeat-options.md +31 -0
  190. package/llm-docs/api/types/common.md +32 -0
  191. package/llm-docs/api/types/cut.md +125 -0
  192. package/llm-docs/api/types/draft.md +21 -0
  193. package/llm-docs/api/types/extrudable-geometry.md +23 -0
  194. package/llm-docs/api/types/extrude.md +194 -0
  195. package/llm-docs/api/types/geometry.md +51 -0
  196. package/llm-docs/api/types/hline.md +35 -0
  197. package/llm-docs/api/types/linear-repeat-options.md +31 -0
  198. package/llm-docs/api/types/loft.md +154 -0
  199. package/llm-docs/api/types/mirror.md +35 -0
  200. package/llm-docs/api/types/offset.md +31 -0
  201. package/llm-docs/api/types/plane-like.md +35 -0
  202. package/llm-docs/api/types/plane-transform-options.md +29 -0
  203. package/llm-docs/api/types/plane.md +21 -0
  204. package/llm-docs/api/types/point-like.md +22 -0
  205. package/llm-docs/api/types/point2dlike.md +26 -0
  206. package/llm-docs/api/types/polygon.md +46 -0
  207. package/llm-docs/api/types/rect.md +128 -0
  208. package/llm-docs/api/types/revolve.md +102 -0
  209. package/llm-docs/api/types/rib.md +133 -0
  210. package/llm-docs/api/types/scene-object.md +33 -0
  211. package/llm-docs/api/types/select.md +21 -0
  212. package/llm-docs/api/types/shell.md +54 -0
  213. package/llm-docs/api/types/slot.md +43 -0
  214. package/llm-docs/api/types/sweep.md +189 -0
  215. package/llm-docs/api/types/tangent-arc-two-objects.md +46 -0
  216. package/llm-docs/api/types/transformable.md +93 -0
  217. package/llm-docs/api/types/trim.md +27 -0
  218. package/llm-docs/api/types/two-objects-tangent-line.md +46 -0
  219. package/llm-docs/api/types/vertex.md +17 -0
  220. package/llm-docs/api/types/vline.md +35 -0
  221. package/llm-docs/concepts/coordinate-system.md +45 -0
  222. package/llm-docs/concepts/history-and-rollback.md +40 -0
  223. package/llm-docs/concepts/last-selection.md +49 -0
  224. package/llm-docs/concepts/scene-graph.md +37 -0
  225. package/llm-docs/index.json +1750 -0
  226. package/mcp/dist/client.d.ts +64 -0
  227. package/mcp/dist/client.js +248 -0
  228. package/mcp/dist/discovery.d.ts +11 -0
  229. package/mcp/dist/discovery.js +78 -0
  230. package/mcp/dist/docs-index.d.ts +81 -0
  231. package/mcp/dist/docs-index.js +261 -0
  232. package/mcp/dist/resources.d.ts +4 -0
  233. package/mcp/dist/resources.js +115 -0
  234. package/mcp/dist/server.d.ts +12 -0
  235. package/mcp/dist/server.js +489 -0
  236. package/mcp/dist/tools/coordination.d.ts +9 -0
  237. package/mcp/dist/tools/coordination.js +46 -0
  238. package/mcp/dist/tools/docs.d.ts +66 -0
  239. package/mcp/dist/tools/docs.js +122 -0
  240. package/mcp/dist/tools/engine.d.ts +56 -0
  241. package/mcp/dist/tools/engine.js +145 -0
  242. package/mcp/dist/tools/inspection.d.ts +75 -0
  243. package/mcp/dist/tools/inspection.js +121 -0
  244. package/mcp/dist/tools/screenshot.d.ts +63 -0
  245. package/mcp/dist/tools/screenshot.js +263 -0
  246. package/mcp/dist/tools/source.d.ts +84 -0
  247. package/mcp/dist/tools/source.js +434 -0
  248. package/mcp/dist/tools/workspaces.d.ts +13 -0
  249. package/mcp/dist/tools/workspaces.js +33 -0
  250. package/mcp/dist/types.d.ts +18 -0
  251. package/mcp/dist/types.js +11 -0
  252. package/package.json +19 -5
  253. package/server/dist/code-editor.d.ts +36 -0
  254. package/server/dist/code-editor.js +8 -0
  255. package/server/dist/fluidcad-server.d.ts +50 -0
  256. package/server/dist/fluidcad-server.js +153 -1
  257. package/server/dist/global-registry.d.ts +30 -0
  258. package/server/dist/global-registry.js +126 -0
  259. package/server/dist/index.js +171 -26
  260. package/server/dist/instance-file.d.ts +31 -0
  261. package/server/dist/instance-file.js +73 -0
  262. package/server/dist/lint-fluid-js.d.ts +15 -0
  263. package/server/dist/lint-fluid-js.js +271 -0
  264. package/server/dist/routes/editor.d.ts +24 -0
  265. package/server/dist/routes/editor.js +44 -0
  266. package/server/dist/routes/export.d.ts +1 -1
  267. package/server/dist/routes/export.js +45 -8
  268. package/server/dist/routes/health.d.ts +7 -0
  269. package/server/dist/routes/health.js +14 -0
  270. package/server/dist/routes/lint.d.ts +10 -0
  271. package/server/dist/routes/lint.js +28 -0
  272. package/server/dist/routes/render.d.ts +33 -0
  273. package/server/dist/routes/render.js +34 -0
  274. package/server/dist/routes/scene.d.ts +5 -0
  275. package/server/dist/routes/scene.js +48 -0
  276. package/server/dist/routes/screenshot.js +68 -1
  277. package/server/dist/ws-protocol.d.ts +56 -2
  278. package/ui/dist/assets/{index-DMw0OYCF.js → index-BdqrMDRu.js} +30 -30
  279. package/ui/dist/index.html +1 -1
@@ -1,16 +1,55 @@
1
+ import type { CompileError } from './ws-protocol.ts';
1
2
  export type SceneRenderedData = {
2
3
  absPath: string;
3
4
  result: any[];
4
5
  rollbackStop: number;
5
6
  breakpointHit?: boolean;
6
7
  };
8
+ export type SceneSummaryObject = {
9
+ index: number;
10
+ id: string;
11
+ kind: string;
12
+ uniqueKind: string;
13
+ name: string;
14
+ params: any;
15
+ sourceLocation?: {
16
+ filePath: string;
17
+ line: number;
18
+ column: number;
19
+ };
20
+ shapeIds: string[];
21
+ fromCache: boolean;
22
+ hasError: boolean;
23
+ errorMessage?: string;
24
+ containerId: string | null;
25
+ isContainer: boolean;
26
+ visible: boolean;
27
+ };
28
+ export type SceneSummary = {
29
+ schemaVersion: 1;
30
+ file: string;
31
+ objects: SceneSummaryObject[];
32
+ rollbackStop: number;
33
+ compileError: CompileError | null;
34
+ };
35
+ export type ShapeListEntry = {
36
+ shapeId: string;
37
+ type: string;
38
+ sceneObjectId: string;
39
+ };
40
+ export type ShapeList = {
41
+ shapes: ShapeListEntry[];
42
+ };
7
43
  export declare class FluidCadServer {
8
44
  private viteManager;
9
45
  private sceneManager;
10
46
  private previousScenes;
11
47
  private renderingCache;
48
+ private lastRendered;
12
49
  private currentFileName;
13
50
  private currentFilePath;
51
+ private lastRollbackStop;
52
+ private compileError;
14
53
  init(workspacePath: string): Promise<void>;
15
54
  processFile(filePath: string, ignoreCache?: boolean): Promise<SceneRenderedData | null>;
16
55
  updateLiveCode(fileName: string, code: string): Promise<SceneRenderedData | null>;
@@ -32,4 +71,15 @@ export declare class FluidCadServer {
32
71
  fileName: string;
33
72
  } | null;
34
73
  hitTest(shapeId: string, rayOrigin: [number, number, number], rayDir: [number, number, number], edgeThreshold: number): any;
74
+ setCompileError(err: CompileError | null): void;
75
+ getCompileError(): CompileError | null;
76
+ getCurrentFileName(): string;
77
+ /**
78
+ * Test-only seam: stage a scene under the given file name so the inspection
79
+ * accessors can read it without running the vite pipeline. Production code
80
+ * never calls this — `processFile` populates the same map.
81
+ */
82
+ _setSceneForTesting(fileName: string, scene: any, rollbackStop?: number): void;
83
+ getSceneSummary(): SceneSummary | null;
84
+ getShapesList(): ShapeList | null;
35
85
  }
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'crypto';
1
2
  import { join } from 'path';
2
3
  import { existsSync } from 'fs';
3
4
  import { ViteManager } from "./vite-manager.js";
@@ -8,8 +9,16 @@ export class FluidCadServer {
8
9
  sceneManager;
9
10
  previousScenes = new Map();
10
11
  renderingCache = new Map();
12
+ // Per-file hash + full result of the most recent successful render. Any
13
+ // incoming render request — IPC live-update from the extension, watcher-
14
+ // driven live-update under `fluidcad serve`, or HTTP /api/render from the
15
+ // MCP — short-circuits here when the new code hashes to the same value.
16
+ // Avoids redundant OCC work when multiple producers see the same write.
17
+ lastRendered = new Map();
11
18
  currentFileName = '';
12
19
  currentFilePath = '';
20
+ lastRollbackStop = -1;
21
+ compileError = null;
13
22
  async init(workspacePath) {
14
23
  await this.viteManager.init(workspacePath);
15
24
  const initFilePath = normalizePath(join(workspacePath, 'init.js'));
@@ -29,6 +38,8 @@ export class FluidCadServer {
29
38
  if (!ignoreCache) {
30
39
  const fromCache = this.renderingCache.get(normalizedFileName);
31
40
  if (fromCache) {
41
+ this.lastRollbackStop = fromCache.length - 1;
42
+ this.compileError = null;
32
43
  return {
33
44
  absPath: normalizedFileName,
34
45
  result: fromCache,
@@ -67,6 +78,8 @@ export class FluidCadServer {
67
78
  if (!filePath.startsWith('virtual:live-render')) {
68
79
  this.renderingCache.set(normalizedFileName, result);
69
80
  }
81
+ this.lastRollbackStop = result.length - 1;
82
+ this.compileError = null;
70
83
  return {
71
84
  absPath: normalizedFileName,
72
85
  result,
@@ -82,10 +95,27 @@ export class FluidCadServer {
82
95
  }
83
96
  async updateLiveCode(fileName, code) {
84
97
  fileName = normalizePath(fileName);
98
+ // Dedup against the last successful render of this file. Multiple
99
+ // producers (editor live-update, save-triggered process-file, watcher,
100
+ // MCP /api/render) commonly hand us identical content; without this
101
+ // short-circuit each one would trigger a redundant OCC pass.
102
+ const hash = hashCode(code);
103
+ const cached = this.lastRendered.get(fileName);
104
+ if (cached && cached.hash === hash) {
105
+ this.compileError = null;
106
+ this.currentFileName = fileName;
107
+ this.currentFilePath = `virtual:live-render:${fileName}`;
108
+ this.lastRollbackStop = cached.data.rollbackStop;
109
+ return cached.data;
110
+ }
85
111
  const id = `virtual:live-render:${fileName}`;
86
112
  this.viteManager.setBuffer(id, code);
87
113
  this.renderingCache.delete(fileName);
88
- return this.processFile(id, true);
114
+ const result = await this.processFile(id, true);
115
+ if (result) {
116
+ this.lastRendered.set(fileName, { hash, data: result });
117
+ }
118
+ return result;
89
119
  }
90
120
  async rollbackFromUI(index) {
91
121
  return this.rollback(this.currentFileName, index);
@@ -96,6 +126,7 @@ export class FluidCadServer {
96
126
  }
97
127
  this.previousScenes.delete(this.currentFileName);
98
128
  this.renderingCache.delete(this.currentFileName);
129
+ this.lastRendered.delete(this.currentFileName);
99
130
  return this.processFile(this.currentFilePath, true);
100
131
  }
101
132
  async rollback(fileName, index) {
@@ -110,6 +141,7 @@ export class FluidCadServer {
110
141
  const rollbackIndex = index >= totalObjects - 1 ? totalObjects - 1 : index;
111
142
  this.sceneManager.rollbackScene(scene, rollbackIndex);
112
143
  const result = scene.getRenderedObjects();
144
+ this.lastRollbackStop = index;
113
145
  return {
114
146
  absPath: fileName,
115
147
  result,
@@ -173,4 +205,124 @@ export class FluidCadServer {
173
205
  }
174
206
  return this.sceneManager.hitTest(scene, shapeId, rayOrigin, rayDir, edgeThreshold);
175
207
  }
208
+ setCompileError(err) {
209
+ this.compileError = err;
210
+ }
211
+ getCompileError() {
212
+ return this.compileError;
213
+ }
214
+ getCurrentFileName() {
215
+ return this.currentFileName;
216
+ }
217
+ /**
218
+ * Test-only seam: stage a scene under the given file name so the inspection
219
+ * accessors can read it without running the vite pipeline. Production code
220
+ * never calls this — `processFile` populates the same map.
221
+ */
222
+ _setSceneForTesting(fileName, scene, rollbackStop = -1) {
223
+ this.currentFileName = fileName;
224
+ this.previousScenes.set(fileName, scene);
225
+ this.lastRollbackStop = rollbackStop;
226
+ }
227
+ getSceneSummary() {
228
+ if (!this.currentFileName) {
229
+ return null;
230
+ }
231
+ const scene = this.previousScenes.get(this.currentFileName);
232
+ if (!scene) {
233
+ return null;
234
+ }
235
+ const rendered = scene.getRenderedObjects();
236
+ const objects = rendered.map((r, index) => ({
237
+ index,
238
+ id: r.id,
239
+ kind: r.type,
240
+ uniqueKind: r.uniqueType,
241
+ name: r.name,
242
+ params: sanitizeParams(r.object),
243
+ sourceLocation: r.sourceLocation,
244
+ shapeIds: (r.sceneShapes ?? []).map((s) => s.shapeId),
245
+ fromCache: !!r.fromCache,
246
+ hasError: !!r.hasError,
247
+ errorMessage: r.errorMessage,
248
+ containerId: r.parentId ?? null,
249
+ isContainer: !!r.isContainer,
250
+ visible: r.visible !== false,
251
+ }));
252
+ return {
253
+ schemaVersion: 1,
254
+ file: this.currentFileName,
255
+ objects,
256
+ rollbackStop: this.lastRollbackStop,
257
+ compileError: this.compileError,
258
+ };
259
+ }
260
+ getShapesList() {
261
+ if (!this.currentFileName) {
262
+ return null;
263
+ }
264
+ const scene = this.previousScenes.get(this.currentFileName);
265
+ if (!scene) {
266
+ return null;
267
+ }
268
+ const rendered = scene.getRenderedObjects();
269
+ const shapes = [];
270
+ for (const r of rendered) {
271
+ const sceneShapes = (r.sceneShapes ?? []);
272
+ for (const s of sceneShapes) {
273
+ shapes.push({
274
+ shapeId: s.shapeId,
275
+ type: s.shapeType,
276
+ sceneObjectId: r.id,
277
+ });
278
+ }
279
+ }
280
+ return { shapes };
281
+ }
282
+ }
283
+ /**
284
+ * Hash a `.fluid.js` source for dedup. Newlines are normalised to LF so a
285
+ * round-trip through an editor (CRLF) and a disk write (LF) hashes the
286
+ * same. SHA1 is plenty here — we just need a stable equality check, not
287
+ * collision resistance against an adversary.
288
+ */
289
+ function hashCode(code) {
290
+ return createHash('sha1').update(code.replace(/\r\n/g, '\n')).digest('hex');
291
+ }
292
+ const MAX_PARAM_DEPTH = 6;
293
+ function sanitizeParams(value, depth = 0) {
294
+ if (value === null || value === undefined) {
295
+ return value ?? null;
296
+ }
297
+ if (typeof value === 'number') {
298
+ return Number.isFinite(value) ? value : null;
299
+ }
300
+ if (typeof value === 'string' || typeof value === 'boolean') {
301
+ return value;
302
+ }
303
+ if (depth >= MAX_PARAM_DEPTH) {
304
+ return null;
305
+ }
306
+ if (Array.isArray(value)) {
307
+ return value.map((v) => sanitizeParams(v, depth + 1));
308
+ }
309
+ if (typeof value === 'object') {
310
+ // A scene-object reference. Render as { ref: id } so the agent can chase
311
+ // it through other tools without us shipping the whole subtree.
312
+ const maybeId = value.id;
313
+ const isSceneObjectRef = typeof maybeId === 'string' &&
314
+ typeof value.getType === 'function';
315
+ if (isSceneObjectRef) {
316
+ return { ref: maybeId };
317
+ }
318
+ const out = {};
319
+ for (const [k, v] of Object.entries(value)) {
320
+ if (typeof v === 'function') {
321
+ continue;
322
+ }
323
+ out[k] = sanitizeParams(v, depth + 1);
324
+ }
325
+ return out;
326
+ }
327
+ return null;
176
328
  }
@@ -0,0 +1,30 @@
1
+ export declare const REGISTRY_DIR_NAME = ".fluidcad";
2
+ export declare const REGISTRY_FILE_NAME = "instances.json";
3
+ export type RegistryEntry = {
4
+ workspacePath: string;
5
+ port: number;
6
+ pid: number;
7
+ version: string;
8
+ startedAt: string;
9
+ };
10
+ export declare function registryFilePath(): string;
11
+ /**
12
+ * Add or replace an entry keyed by `workspacePath`. A new entry for an
13
+ * existing workspace (second window) supersedes the prior one.
14
+ */
15
+ export declare function addInstance(entry: RegistryEntry): void;
16
+ /**
17
+ * Remove the entry that matches both workspacePath and pid. Matching on pid
18
+ * prevents a clean shutdown from clobbering a freshly-restarted instance.
19
+ */
20
+ export declare function removeInstance(workspacePath: string, pid: number): void;
21
+ /**
22
+ * Read the registry, prune entries whose PIDs are no longer alive, and
23
+ * persist the pruned result. Returns the live entries.
24
+ *
25
+ * Liveness is `process.kill(pid, 0)` — sending signal 0 doesn't deliver
26
+ * anything but throws ESRCH if the process is gone. EPERM (running but owned
27
+ * by another user) counts as alive.
28
+ */
29
+ export declare function readInstances(): RegistryEntry[];
30
+ export declare function isPidAlive(pid: number): boolean;
@@ -0,0 +1,126 @@
1
+ // Global instance registry at ~/.fluidcad/instances.json — lets a single MCP
2
+ // process enumerate every running FluidCAD workspace on this machine.
3
+ //
4
+ // Read-modify-write is not crash-safe across multiple writers (we have no
5
+ // file lock), but the worst case is a dropped registry entry that the next
6
+ // successful write or stale-prune restores. Atomicity for the file itself is
7
+ // via tmp + rename.
8
+ import fs from 'fs';
9
+ import os from 'os';
10
+ import path from 'path';
11
+ export const REGISTRY_DIR_NAME = '.fluidcad';
12
+ export const REGISTRY_FILE_NAME = 'instances.json';
13
+ const EMPTY = { schemaVersion: 1, instances: [] };
14
+ function registryDir() {
15
+ return path.join(os.homedir(), REGISTRY_DIR_NAME);
16
+ }
17
+ export function registryFilePath() {
18
+ return path.join(registryDir(), REGISTRY_FILE_NAME);
19
+ }
20
+ function readRaw() {
21
+ try {
22
+ const raw = fs.readFileSync(registryFilePath(), 'utf8');
23
+ const parsed = JSON.parse(raw);
24
+ if (isRegistryFile(parsed)) {
25
+ return parsed;
26
+ }
27
+ return EMPTY;
28
+ }
29
+ catch {
30
+ return EMPTY;
31
+ }
32
+ }
33
+ function writeRaw(file) {
34
+ const dir = registryDir();
35
+ fs.mkdirSync(dir, { recursive: true });
36
+ const destination = registryFilePath();
37
+ const tmp = path.join(dir, `${REGISTRY_FILE_NAME}.${process.pid}.tmp`);
38
+ const payload = JSON.stringify(file, null, 2) + '\n';
39
+ fs.writeFileSync(tmp, payload, { encoding: 'utf8', mode: 0o644 });
40
+ fs.renameSync(tmp, destination);
41
+ }
42
+ /**
43
+ * Add or replace an entry keyed by `workspacePath`. A new entry for an
44
+ * existing workspace (second window) supersedes the prior one.
45
+ */
46
+ export function addInstance(entry) {
47
+ const file = readRaw();
48
+ const filtered = file.instances.filter((e) => e.workspacePath !== entry.workspacePath);
49
+ filtered.push(entry);
50
+ writeRaw({ schemaVersion: 1, instances: filtered });
51
+ }
52
+ /**
53
+ * Remove the entry that matches both workspacePath and pid. Matching on pid
54
+ * prevents a clean shutdown from clobbering a freshly-restarted instance.
55
+ */
56
+ export function removeInstance(workspacePath, pid) {
57
+ const file = readRaw();
58
+ const filtered = file.instances.filter((e) => !(e.workspacePath === workspacePath && e.pid === pid));
59
+ if (filtered.length === file.instances.length) {
60
+ return;
61
+ }
62
+ writeRaw({ schemaVersion: 1, instances: filtered });
63
+ }
64
+ /**
65
+ * Read the registry, prune entries whose PIDs are no longer alive, and
66
+ * persist the pruned result. Returns the live entries.
67
+ *
68
+ * Liveness is `process.kill(pid, 0)` — sending signal 0 doesn't deliver
69
+ * anything but throws ESRCH if the process is gone. EPERM (running but owned
70
+ * by another user) counts as alive.
71
+ */
72
+ export function readInstances() {
73
+ const file = readRaw();
74
+ const alive = [];
75
+ const dead = [];
76
+ for (const entry of file.instances) {
77
+ if (isPidAlive(entry.pid)) {
78
+ alive.push(entry);
79
+ }
80
+ else {
81
+ dead.push(entry);
82
+ }
83
+ }
84
+ if (dead.length > 0) {
85
+ try {
86
+ writeRaw({ schemaVersion: 1, instances: alive });
87
+ }
88
+ catch {
89
+ // Pruning is best-effort; readers still get the live set.
90
+ }
91
+ }
92
+ return alive;
93
+ }
94
+ export function isPidAlive(pid) {
95
+ try {
96
+ process.kill(pid, 0);
97
+ return true;
98
+ }
99
+ catch (err) {
100
+ if (err && err.code === 'EPERM') {
101
+ return true;
102
+ }
103
+ return false;
104
+ }
105
+ }
106
+ function isRegistryFile(value) {
107
+ if (typeof value !== 'object' || value === null) {
108
+ return false;
109
+ }
110
+ const v = value;
111
+ if (v.schemaVersion !== 1 || !Array.isArray(v.instances)) {
112
+ return false;
113
+ }
114
+ return v.instances.every(isRegistryEntry);
115
+ }
116
+ function isRegistryEntry(value) {
117
+ if (typeof value !== 'object' || value === null) {
118
+ return false;
119
+ }
120
+ const v = value;
121
+ return (typeof v.workspacePath === 'string' &&
122
+ typeof v.port === 'number' &&
123
+ typeof v.pid === 'number' &&
124
+ typeof v.version === 'string' &&
125
+ typeof v.startedAt === 'string');
126
+ }