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
@@ -1,107 +1,206 @@
1
1
  import { createHash } from 'crypto';
2
2
  import { join } from 'path';
3
3
  import { existsSync } from 'fs';
4
- import { ViteManager } from "./vite-manager.js";
4
+ import { LocalSceneHost } from "./host/local-scene-host.js";
5
5
  import { normalizePath } from "./normalize-path.js";
6
6
  import { BreakpointHit } from '../../lib/dist/common/breakpoint-hit.js';
7
+ import { createParamRegistry, getParamRegistry } from '../../lib/dist/index.js';
8
+ /**
9
+ * `sessionId` is the per-renderer state key. In desktop mode it equals the
10
+ * file path being edited (so per-file state survives switching files). In
11
+ * hub mode it's a WebSocket connection UUID (so concurrent viewers stay
12
+ * isolated). Map keys called `sessionId` accept either flavour.
13
+ */
7
14
  export class FluidCadServer {
8
- viteManager = new ViteManager();
15
+ host;
9
16
  sceneManager;
17
+ // Per-session render output, scene cache, and param overrides. Desktop's
18
+ // sessionId is the normalized filePath; hub mode's sessionId is the WS
19
+ // connection UUID. Maps must be cleared via `destroySession` on hub-side
20
+ // disconnect to avoid leaks.
10
21
  previousScenes = new Map();
11
22
  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.
23
+ // Records the last successful render per session as `{ paramsHash, data }`.
24
+ // Any subsequent render request short-circuits when the new params hash to
25
+ // the same value avoids redundant OCC work when desktop producers see the
26
+ // same code+params, or hub clients re-emit the same param mutation.
17
27
  lastRendered = new Map();
28
+ paramOverrides = new Map();
29
+ // What file each session is rendering. For desktop, sessionId === filePath
30
+ // (set lazily on first processFile call). For hub, set explicitly via
31
+ // createSession with the bundle's manifest entry.
32
+ sessionFiles = new Map();
33
+ // Serializes OCC calls across all sessions. OCC isn't thread-safe and we
34
+ // share one engine instance per host process; concurrent param edits from
35
+ // multiple hub clients have to queue. Promise-chain pattern: each render
36
+ // awaits the previous one's settlement before starting.
37
+ renderMutex = Promise.resolve();
18
38
  currentFileName = '';
19
39
  currentFilePath = '';
20
40
  lastRollbackStop = -1;
21
41
  compileError = null;
42
+ constructor(host = new LocalSceneHost()) {
43
+ this.host = host;
44
+ }
45
+ getCurrentCode() {
46
+ if (!this.currentFileName)
47
+ return null;
48
+ return this.host.getBuffer(this.currentFileName);
49
+ }
22
50
  async init(workspacePath) {
23
- await this.viteManager.init(workspacePath);
51
+ await this.host.init(workspacePath);
24
52
  const initFilePath = normalizePath(join(workspacePath, 'init.js'));
25
53
  if (existsSync(initFilePath)) {
26
- const { default: _sceneManager } = await this.viteManager.loadModule(initFilePath);
54
+ const { default: _sceneManager } = await this.host.loadModule(initFilePath);
27
55
  this.sceneManager = await _sceneManager;
28
56
  }
29
57
  }
30
- async processFile(filePath, ignoreCache = false) {
31
- if (!this.sceneManager) {
32
- return null;
58
+ /**
59
+ * Capture an already-initialized SceneManager. Used by the hub-mode entry
60
+ * after running the packed bundle once to materialize the engine globals.
61
+ */
62
+ setSceneManager(manager) {
63
+ this.sceneManager = manager;
64
+ }
65
+ /**
66
+ * Run `fn` with exclusive access to the OCC engine. The mutex is process-
67
+ * wide: in hub mode concurrent client sessions land here too. Order is
68
+ * first-come, first-served via Promise chain.
69
+ */
70
+ async serialized(fn) {
71
+ const prev = this.renderMutex;
72
+ let release = () => { };
73
+ const next = new Promise((resolve) => { release = resolve; });
74
+ this.renderMutex = next;
75
+ try {
76
+ await prev;
77
+ return await fn();
33
78
  }
34
- filePath = normalizePath(filePath);
35
- const normalizedFileName = filePath.replace('virtual:live-render:', '');
36
- this.currentFileName = normalizedFileName;
37
- this.currentFilePath = filePath;
38
- if (!ignoreCache) {
39
- const fromCache = this.renderingCache.get(normalizedFileName);
40
- if (fromCache) {
41
- this.lastRollbackStop = fromCache.length - 1;
42
- this.compileError = null;
43
- return {
44
- absPath: normalizedFileName,
45
- result: fromCache,
46
- rollbackStop: fromCache.length - 1,
47
- };
48
- }
79
+ finally {
80
+ release();
49
81
  }
50
- try {
51
- let scene = this.sceneManager.startScene();
52
- this.sceneManager.setCurrentFile(normalizedFileName);
53
- this.viteManager.invalidateModule();
54
- let breakpointHit = false;
55
- try {
56
- await this.viteManager.loadModule(filePath);
82
+ }
83
+ // ---------------------------------------------------------------------------
84
+ // Session lifecycle (hub mode)
85
+ // ---------------------------------------------------------------------------
86
+ createSession(sessionId, entryFilePath) {
87
+ this.sessionFiles.set(sessionId, normalizePath(entryFilePath));
88
+ }
89
+ destroySession(sessionId) {
90
+ this.previousScenes.delete(sessionId);
91
+ this.renderingCache.delete(sessionId);
92
+ this.lastRendered.delete(sessionId);
93
+ this.paramOverrides.delete(sessionId);
94
+ this.sessionFiles.delete(sessionId);
95
+ }
96
+ /**
97
+ * Re-render the session's entry, ignoring caches. Hub clients call this
98
+ * after editing a param. Returns the fresh render or null if no manager.
99
+ */
100
+ async recomputeForSession(sessionId) {
101
+ const filePath = this.sessionFiles.get(sessionId);
102
+ if (!filePath)
103
+ return null;
104
+ this.renderingCache.delete(sessionId);
105
+ this.lastRendered.delete(sessionId);
106
+ return this.processFileInternal(sessionId, filePath, true);
107
+ }
108
+ // ---------------------------------------------------------------------------
109
+ // Render — internal core used by both desktop and hub entry points
110
+ // ---------------------------------------------------------------------------
111
+ async processFileInternal(sessionId, filePath, ignoreCache) {
112
+ return this.serialized(async () => {
113
+ if (!this.sceneManager) {
114
+ return null;
57
115
  }
58
- catch (e) {
59
- if (e instanceof BreakpointHit) {
60
- breakpointHit = true;
61
- }
62
- else {
63
- throw e;
116
+ const normalizedFileName = filePath.replace('virtual:live-render:', '');
117
+ this.currentFileName = normalizedFileName;
118
+ this.currentFilePath = filePath;
119
+ if (!ignoreCache) {
120
+ const fromCache = this.renderingCache.get(sessionId);
121
+ if (fromCache) {
122
+ this.lastRollbackStop = fromCache.length - 1;
123
+ this.compileError = null;
124
+ return {
125
+ absPath: normalizedFileName,
126
+ result: fromCache,
127
+ rollbackStop: fromCache.length - 1,
128
+ };
64
129
  }
65
130
  }
66
- if (this.previousScenes.has(normalizedFileName)) {
67
- const previousScene = this.previousScenes.get(normalizedFileName);
68
- scene = this.sceneManager.compare(previousScene, scene);
69
- }
70
- this.previousScenes.set(normalizedFileName, scene);
71
- this.sceneManager.renderScene(scene);
72
- const result = scene.getRenderedObjects();
73
- for (const obj of result) {
74
- if (obj.sourceLocation) {
75
- obj.sourceLocation.filePath = obj.sourceLocation.filePath.replace('virtual:live-render:', '');
131
+ try {
132
+ let scene = this.sceneManager.startScene();
133
+ this.sceneManager.setCurrentFile(normalizedFileName);
134
+ this.host.invalidateModule();
135
+ const registry = createParamRegistry();
136
+ const overrides = this.paramOverrides.get(sessionId);
137
+ if (overrides) {
138
+ registry.setOverrides(overrides);
139
+ }
140
+ let breakpointHit = false;
141
+ try {
142
+ await this.host.loadModule(filePath);
143
+ }
144
+ catch (e) {
145
+ if (e instanceof BreakpointHit) {
146
+ breakpointHit = true;
147
+ }
148
+ else {
149
+ throw e;
150
+ }
151
+ }
152
+ const params = getParamRegistry().getDefinitions();
153
+ if (this.previousScenes.has(sessionId)) {
154
+ const previousScene = this.previousScenes.get(sessionId);
155
+ scene = this.sceneManager.compare(previousScene, scene);
76
156
  }
157
+ this.previousScenes.set(sessionId, scene);
158
+ this.sceneManager.renderScene(scene);
159
+ const result = scene.getRenderedObjects();
160
+ for (const obj of result) {
161
+ if (obj.sourceLocation) {
162
+ obj.sourceLocation.filePath = obj.sourceLocation.filePath.replace('virtual:live-render:', '');
163
+ }
164
+ }
165
+ if (!filePath.startsWith('virtual:live-render')) {
166
+ this.renderingCache.set(sessionId, result);
167
+ }
168
+ this.lastRollbackStop = result.length - 1;
169
+ this.compileError = null;
170
+ return {
171
+ absPath: normalizedFileName,
172
+ result,
173
+ rollbackStop: result.length - 1,
174
+ breakpointHit,
175
+ params,
176
+ };
77
177
  }
78
- if (!filePath.startsWith('virtual:live-render')) {
79
- this.renderingCache.set(normalizedFileName, result);
178
+ catch (error) {
179
+ this.host.invalidateModule();
180
+ console.log('Error processing file:', error);
181
+ throw error;
80
182
  }
81
- this.lastRollbackStop = result.length - 1;
82
- this.compileError = null;
83
- return {
84
- absPath: normalizedFileName,
85
- result,
86
- rollbackStop: result.length - 1,
87
- breakpointHit,
88
- };
89
- }
90
- catch (error) {
91
- this.viteManager.invalidateModule();
92
- console.log('Error processing file:', error);
93
- throw error;
94
- }
183
+ });
184
+ }
185
+ // ---------------------------------------------------------------------------
186
+ // Desktop API — sessionId is implicit (filePath)
187
+ // ---------------------------------------------------------------------------
188
+ async processFile(filePath, ignoreCache = false) {
189
+ filePath = normalizePath(filePath);
190
+ const sessionId = filePath.replace('virtual:live-render:', '');
191
+ this.sessionFiles.set(sessionId, sessionId);
192
+ return this.processFileInternal(sessionId, filePath, ignoreCache);
95
193
  }
96
194
  async updateLiveCode(fileName, code) {
97
195
  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);
196
+ // Dedup against the last successful render. Multiple producers (editor
197
+ // live-update, save-triggered process-file, watcher, MCP /api/render)
198
+ // commonly hand us identical content; without this short-circuit each
199
+ // would trigger a redundant OCC pass. paramsHash mixes code content with
200
+ // current param overrides so a param change invalidates the cache.
201
+ const paramsHash = this.computeParamsHash(fileName, code);
103
202
  const cached = this.lastRendered.get(fileName);
104
- if (cached && cached.hash === hash) {
203
+ if (cached && cached.paramsHash === paramsHash) {
105
204
  this.compileError = null;
106
205
  this.currentFileName = fileName;
107
206
  this.currentFilePath = `virtual:live-render:${fileName}`;
@@ -109,11 +208,12 @@ export class FluidCadServer {
109
208
  return cached.data;
110
209
  }
111
210
  const id = `virtual:live-render:${fileName}`;
112
- this.viteManager.setBuffer(id, code);
211
+ this.host.setBuffer(id, code);
113
212
  this.renderingCache.delete(fileName);
114
- const result = await this.processFile(id, true);
213
+ this.sessionFiles.set(fileName, fileName);
214
+ const result = await this.processFileInternal(fileName, id, true);
115
215
  if (result) {
116
- this.lastRendered.set(fileName, { hash, data: result });
216
+ this.lastRendered.set(fileName, { paramsHash, data: result });
117
217
  }
118
218
  return result;
119
219
  }
@@ -124,10 +224,10 @@ export class FluidCadServer {
124
224
  if (!this.currentFilePath) {
125
225
  return null;
126
226
  }
127
- this.previousScenes.delete(this.currentFileName);
128
- this.renderingCache.delete(this.currentFileName);
129
- this.lastRendered.delete(this.currentFileName);
130
- return this.processFile(this.currentFilePath, true);
227
+ const sessionId = this.currentFileName;
228
+ this.renderingCache.delete(sessionId);
229
+ this.lastRendered.delete(sessionId);
230
+ return this.processFileInternal(sessionId, this.currentFilePath, true);
131
231
  }
132
232
  async rollback(fileName, index) {
133
233
  if (!this.sceneManager) {
@@ -195,6 +295,36 @@ export class FluidCadServer {
195
295
  }
196
296
  return this.sceneManager.exportShapes(scene, shapeIds, options);
197
297
  }
298
+ /**
299
+ * Export every solid of a hub session's latest render. The session-keyed twin
300
+ * of `exportShapes` (which reads the desktop `currentFileName`): hub mode keys
301
+ * each render's scene by `sessionId`, so exporting/downloading from a hub
302
+ * session must look it up the same way — exactly why `hitTestForSession`
303
+ * exists. Gathers all solids itself ("download the whole model"); returns null
304
+ * when the session has no rendered scene or it holds no solids (the caller maps
305
+ * that to a "nothing to export" response).
306
+ */
307
+ exportShapesForSession(sessionId, options) {
308
+ if (!this.sceneManager) {
309
+ return null;
310
+ }
311
+ const scene = this.previousScenes.get(sessionId);
312
+ if (!scene) {
313
+ return null;
314
+ }
315
+ const shapeIds = [];
316
+ for (const obj of scene.getAllSceneObjects()) {
317
+ for (const shape of obj.getAddedShapes()) {
318
+ if (shape.isSolid()) {
319
+ shapeIds.push(shape.id);
320
+ }
321
+ }
322
+ }
323
+ if (shapeIds.length === 0) {
324
+ return null;
325
+ }
326
+ return this.sceneManager.exportShapes(scene, shapeIds, options);
327
+ }
198
328
  hitTest(shapeId, rayOrigin, rayDir, edgeThreshold) {
199
329
  if (!this.sceneManager) {
200
330
  return null;
@@ -205,12 +335,41 @@ export class FluidCadServer {
205
335
  }
206
336
  return this.sceneManager.hitTest(scene, shapeId, rayOrigin, rayDir, edgeThreshold);
207
337
  }
338
+ hitTestForSession(sessionId, shapeId, rayOrigin, rayDir, edgeThreshold) {
339
+ if (!this.sceneManager) {
340
+ return null;
341
+ }
342
+ const scene = this.previousScenes.get(sessionId);
343
+ if (!scene) {
344
+ return null;
345
+ }
346
+ return this.sceneManager.hitTest(scene, shapeId, rayOrigin, rayDir, edgeThreshold);
347
+ }
208
348
  setCompileError(err) {
209
349
  this.compileError = err;
210
350
  }
211
351
  getCompileError() {
212
352
  return this.compileError;
213
353
  }
354
+ setParam(sessionId, label, value) {
355
+ sessionId = normalizePath(sessionId);
356
+ if (!this.paramOverrides.has(sessionId)) {
357
+ this.paramOverrides.set(sessionId, new Map());
358
+ }
359
+ this.paramOverrides.get(sessionId).set(label, value);
360
+ this.lastRendered.delete(sessionId);
361
+ }
362
+ resetParams(sessionId) {
363
+ sessionId = normalizePath(sessionId);
364
+ this.paramOverrides.delete(sessionId);
365
+ this.lastRendered.delete(sessionId);
366
+ }
367
+ getParamOverrides(sessionId) {
368
+ const map = this.paramOverrides.get(normalizePath(sessionId));
369
+ if (!map)
370
+ return {};
371
+ return Object.fromEntries(map);
372
+ }
214
373
  getCurrentFileName() {
215
374
  return this.currentFileName;
216
375
  }
@@ -279,15 +438,22 @@ export class FluidCadServer {
279
438
  }
280
439
  return { shapes };
281
440
  }
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');
441
+ /**
442
+ * Compose a stable cache key over the rendering inputs: the source bytes
443
+ * being rendered plus the param overrides currently in effect for the
444
+ * session. Param changes flip the hash so cached entries don't shadow a
445
+ * recompute, even when the code text is byte-identical.
446
+ */
447
+ computeParamsHash(sessionId, codeOrBundle) {
448
+ const overrides = this.paramOverrides.get(sessionId);
449
+ const sortedEntries = overrides ? [...overrides.entries()].sort(([a], [b]) => a.localeCompare(b)) : [];
450
+ const normalized = codeOrBundle.replace(/\r\n/g, '\n');
451
+ return createHash('sha1')
452
+ .update(normalized)
453
+ .update('\0')
454
+ .update(JSON.stringify(sortedEntries))
455
+ .digest('hex');
456
+ }
291
457
  }
292
458
  const MAX_PARAM_DEPTH = 6;
293
459
  function sanitizeParams(value, depth = 0) {
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Node built-in modules that user `.fluid.js` code must not import. Same set
3
+ * enforced by LocalSceneHost (at SSR transform time) and the model packer
4
+ * (at bundle time) so a packed model can't smuggle in capabilities a live
5
+ * workspace would have rejected.
6
+ */
7
+ export declare const BLOCKED_NODE_MODULES: Set<string>;
8
+ export declare function getBlockedNodeModule(id: string): string | null;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Node built-in modules that user `.fluid.js` code must not import. Same set
3
+ * enforced by LocalSceneHost (at SSR transform time) and the model packer
4
+ * (at bundle time) so a packed model can't smuggle in capabilities a live
5
+ * workspace would have rejected.
6
+ */
7
+ export const BLOCKED_NODE_MODULES = new Set([
8
+ 'fs',
9
+ 'child_process',
10
+ 'net',
11
+ 'dgram',
12
+ 'tls',
13
+ 'http',
14
+ 'https',
15
+ 'http2',
16
+ 'os',
17
+ 'worker_threads',
18
+ 'vm',
19
+ 'cluster',
20
+ 'dns',
21
+ 'module',
22
+ ]);
23
+ export function getBlockedNodeModule(id) {
24
+ let name = id;
25
+ if (name.startsWith('node:')) {
26
+ name = name.slice(5);
27
+ }
28
+ const baseName = name.split('/')[0];
29
+ return BLOCKED_NODE_MODULES.has(baseName) ? baseName : null;
30
+ }
@@ -1,10 +1,12 @@
1
1
  import { type ViteDevServer } from 'vite';
2
- export declare class ViteManager {
2
+ import type { SceneHost } from './scene-host.ts';
3
+ export declare class LocalSceneHost implements SceneHost {
3
4
  server: ViteDevServer;
4
5
  private rootPath;
5
6
  private buffers;
6
7
  init(rootPath: string): Promise<void>;
7
8
  setBuffer(id: string, code: string): void;
9
+ getBuffer(fileName: string): string | null;
8
10
  loadModule(filePath: string): Promise<Record<string, any>>;
9
11
  invalidateModule(): void;
10
12
  }
@@ -1,30 +1,7 @@
1
1
  import { createServer } from 'vite';
2
2
  import { dirname, resolve, isAbsolute } from 'path';
3
- import { normalizePath } from "./normalize-path.js";
4
- const BLOCKED_NODE_MODULES = new Set([
5
- 'fs',
6
- 'child_process',
7
- 'net',
8
- 'dgram',
9
- 'tls',
10
- 'http',
11
- 'https',
12
- 'http2',
13
- 'os',
14
- 'worker_threads',
15
- 'vm',
16
- 'cluster',
17
- 'dns',
18
- 'module',
19
- ]);
20
- function getBlockedNodeModule(id) {
21
- let name = id;
22
- if (name.startsWith('node:')) {
23
- name = name.slice(5);
24
- }
25
- const baseName = name.split('/')[0];
26
- return BLOCKED_NODE_MODULES.has(baseName) ? baseName : null;
27
- }
3
+ import { normalizePath } from "../normalize-path.js";
4
+ import { getBlockedNodeModule } from "./blocked-imports.js";
28
5
  const IMPORT_PATTERN = /\b(?:import|export)\s[\s\S]*?from\s+['"]([^'"]+)['"]|\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g;
29
6
  function scanForBlockedImports(code) {
30
7
  let match;
@@ -38,7 +15,7 @@ function scanForBlockedImports(code) {
38
15
  }
39
16
  return null;
40
17
  }
41
- export class ViteManager {
18
+ export class LocalSceneHost {
42
19
  server;
43
20
  rootPath = '';
44
21
  buffers = new Map();
@@ -102,6 +79,9 @@ export class ViteManager {
102
79
  setBuffer(id, code) {
103
80
  this.buffers.set(id, code);
104
81
  }
82
+ getBuffer(fileName) {
83
+ return this.buffers.get(`virtual:live-render:${fileName}`) ?? null;
84
+ }
105
85
  async loadModule(filePath) {
106
86
  const mod = await this.server.ssrLoadModule(filePath);
107
87
  for (const value of Object.values(mod)) {
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Source-of-truth + execution seam for a workspace's scene code.
3
+ *
4
+ * `FluidCadServer` owns the engine pipeline (param registry, scene cache,
5
+ * render orchestration) and does not care where `.fluid.js` source comes
6
+ * from or how it gets turned into a runnable module. That responsibility
7
+ * lives behind this interface.
8
+ *
9
+ * Implementations:
10
+ * - LocalSceneHost: Vite SSR over the workspace directory (desktop).
11
+ * - HubSceneHost (Phase 2): in-memory consumer of a packed model bundle.
12
+ */
13
+ export interface SceneHost {
14
+ init(workspacePath: string): Promise<void>;
15
+ loadModule(filePath: string): Promise<Record<string, any>>;
16
+ setBuffer(id: string, code: string): void;
17
+ getBuffer(fileName: string): string | null;
18
+ invalidateModule(): void;
19
+ }
@@ -0,0 +1 @@
1
+ export {};