fluidcad 0.0.35 → 0.0.37

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 (186) hide show
  1. package/LICENSE.txt +21 -504
  2. package/README.md +1 -1
  3. package/bin/commands/login.js +33 -5
  4. package/bin/commands/mcp.js +3 -2
  5. package/bin/commands/publish.js +103 -8
  6. package/bin/lib/api-client.js +8 -0
  7. package/bin/lib/model-config.js +27 -4
  8. package/bin/lib/prompt.js +97 -0
  9. package/lib/dist/common/edge.d.ts +1 -1
  10. package/lib/dist/common/face.d.ts +1 -1
  11. package/lib/dist/common/scene-object.d.ts +6 -0
  12. package/lib/dist/common/scene-object.js +8 -0
  13. package/lib/dist/common/shape-factory.d.ts +1 -1
  14. package/lib/dist/common/shape-history-tracker.d.ts +1 -1
  15. package/lib/dist/common/shape.d.ts +1 -1
  16. package/lib/dist/common/solid.d.ts +1 -1
  17. package/lib/dist/common/transformable-primitive.d.ts +12 -1
  18. package/lib/dist/common/transformable-primitive.js +27 -0
  19. package/lib/dist/common/vertex.d.ts +1 -1
  20. package/lib/dist/common/wire.d.ts +1 -1
  21. package/lib/dist/core/2d/index.d.ts +1 -0
  22. package/lib/dist/core/2d/index.js +1 -0
  23. package/lib/dist/core/2d/text.d.ts +30 -0
  24. package/lib/dist/core/2d/text.js +37 -0
  25. package/lib/dist/core/helix.d.ts +20 -0
  26. package/lib/dist/core/helix.js +36 -0
  27. package/lib/dist/core/index.d.ts +3 -1
  28. package/lib/dist/core/index.js +2 -0
  29. package/lib/dist/core/interfaces.d.ts +180 -0
  30. package/lib/dist/core/wrap.d.ts +17 -0
  31. package/lib/dist/core/wrap.js +39 -0
  32. package/lib/dist/features/2d/text.d.ts +67 -0
  33. package/lib/dist/features/2d/text.js +320 -0
  34. package/lib/dist/features/cylinder.d.ts +3 -1
  35. package/lib/dist/features/cylinder.js +5 -2
  36. package/lib/dist/features/extrude-base.d.ts +1 -0
  37. package/lib/dist/features/extrude-to-face.d.ts +1 -0
  38. package/lib/dist/features/extrude-to-face.js +6 -0
  39. package/lib/dist/features/fillet.d.ts +1 -1
  40. package/lib/dist/features/helix.d.ts +41 -0
  41. package/lib/dist/features/helix.js +337 -0
  42. package/lib/dist/features/select.js +32 -8
  43. package/lib/dist/features/simple-extruder.d.ts +1 -1
  44. package/lib/dist/features/simple-extruder.js +7 -2
  45. package/lib/dist/features/sphere.d.ts +3 -1
  46. package/lib/dist/features/sphere.js +5 -2
  47. package/lib/dist/features/sweep.js +7 -2
  48. package/lib/dist/features/wrap.d.ts +39 -0
  49. package/lib/dist/features/wrap.js +116 -0
  50. package/lib/dist/filters/edge/belongs-to-face.d.ts +3 -1
  51. package/lib/dist/filters/edge/belongs-to-face.js +14 -10
  52. package/lib/dist/filters/filter.d.ts +1 -1
  53. package/lib/dist/filters/from-object.d.ts +1 -1
  54. package/lib/dist/filters/tangent-expander.d.ts +1 -1
  55. package/lib/dist/filters/tangent-expander.js +57 -40
  56. package/lib/dist/helpers/scene-helpers.d.ts +2 -0
  57. package/lib/dist/helpers/scene-helpers.js +1 -1
  58. package/lib/dist/index.d.ts +2 -0
  59. package/lib/dist/index.js +3 -1
  60. package/lib/dist/io/file-import.d.ts +7 -0
  61. package/lib/dist/io/file-import.js +28 -1
  62. package/lib/dist/io/font-registry.d.ts +45 -0
  63. package/lib/dist/io/font-registry.js +272 -0
  64. package/lib/dist/math/bspline-interpolation.d.ts +29 -0
  65. package/lib/dist/math/bspline-interpolation.js +194 -0
  66. package/lib/dist/oc/boolean-ops.d.ts +3 -1
  67. package/lib/dist/oc/boolean-ops.js +15 -1
  68. package/lib/dist/oc/color-transfer.d.ts +1 -1
  69. package/lib/dist/oc/constraints/constraint-helpers.d.ts +4 -4
  70. package/lib/dist/oc/constraints/curve/tangent-circle-solver.js +10 -9
  71. package/lib/dist/oc/constraints/curve/tangent-line-solver.js +5 -6
  72. package/lib/dist/oc/convert.d.ts +1 -1
  73. package/lib/dist/oc/draft-ops.d.ts +1 -1
  74. package/lib/dist/oc/edge-ops.d.ts +2 -2
  75. package/lib/dist/oc/edge-ops.js +13 -14
  76. package/lib/dist/oc/edge-props.d.ts +1 -1
  77. package/lib/dist/oc/edge-query.d.ts +1 -1
  78. package/lib/dist/oc/edge-query.js +3 -8
  79. package/lib/dist/oc/errors.d.ts +8 -0
  80. package/lib/dist/oc/errors.js +27 -0
  81. package/lib/dist/oc/explorer.d.ts +2 -2
  82. package/lib/dist/oc/extrude-ops.d.ts +28 -2
  83. package/lib/dist/oc/extrude-ops.js +56 -7
  84. package/lib/dist/oc/face-ops.d.ts +2 -1
  85. package/lib/dist/oc/face-ops.js +11 -0
  86. package/lib/dist/oc/face-props.d.ts +1 -1
  87. package/lib/dist/oc/face-query.d.ts +12 -1
  88. package/lib/dist/oc/face-query.js +39 -0
  89. package/lib/dist/oc/fillet-ops.d.ts +1 -1
  90. package/lib/dist/oc/fillet-ops.js +4 -4
  91. package/lib/dist/oc/geometry.d.ts +1 -1
  92. package/lib/dist/oc/geometry.js +12 -14
  93. package/lib/dist/oc/helix-ops.d.ts +37 -0
  94. package/lib/dist/oc/helix-ops.js +88 -0
  95. package/lib/dist/oc/hit-test.d.ts +1 -1
  96. package/lib/dist/oc/index.d.ts +4 -0
  97. package/lib/dist/oc/index.js +2 -0
  98. package/lib/dist/oc/init.d.ts +1 -1
  99. package/lib/dist/oc/init.js +1 -1
  100. package/lib/dist/oc/intersection.js +1 -1
  101. package/lib/dist/oc/io.d.ts +6 -6
  102. package/lib/dist/oc/io.js +31 -24
  103. package/lib/dist/oc/measure/classify.d.ts +34 -0
  104. package/lib/dist/oc/measure/classify.js +246 -0
  105. package/lib/dist/oc/measure/measure-ops.d.ts +9 -0
  106. package/lib/dist/oc/measure/measure-ops.js +210 -0
  107. package/lib/dist/oc/measure/measure-types.d.ts +39 -0
  108. package/lib/dist/oc/measure/measure-types.js +1 -0
  109. package/lib/dist/oc/measure/sampling.d.ts +9 -0
  110. package/lib/dist/oc/measure/sampling.js +77 -0
  111. package/lib/dist/oc/measure/vec.d.ts +13 -0
  112. package/lib/dist/oc/measure/vec.js +23 -0
  113. package/lib/dist/oc/mesh.d.ts +1 -1
  114. package/lib/dist/oc/mesh.js +40 -28
  115. package/lib/dist/oc/path-sampler.d.ts +29 -0
  116. package/lib/dist/oc/path-sampler.js +63 -0
  117. package/lib/dist/oc/props.d.ts +1 -1
  118. package/lib/dist/oc/props.js +4 -1
  119. package/lib/dist/oc/shape-hash.d.ts +26 -0
  120. package/lib/dist/oc/shape-hash.js +32 -0
  121. package/lib/dist/oc/shape-ops.d.ts +5 -3
  122. package/lib/dist/oc/shape-ops.js +6 -5
  123. package/lib/dist/oc/sweep-ops.d.ts +22 -1
  124. package/lib/dist/oc/sweep-ops.js +206 -18
  125. package/lib/dist/oc/text-outline.d.ts +62 -0
  126. package/lib/dist/oc/text-outline.js +212 -0
  127. package/lib/dist/oc/topology-index.d.ts +1 -1
  128. package/lib/dist/oc/vertex-ops.d.ts +1 -1
  129. package/lib/dist/oc/wire-ops.d.ts +1 -1
  130. package/lib/dist/oc/wire-ops.js +1 -1
  131. package/lib/dist/oc/wrap-development.d.ts +105 -0
  132. package/lib/dist/oc/wrap-development.js +179 -0
  133. package/lib/dist/oc/wrap-ops.d.ts +100 -0
  134. package/lib/dist/oc/wrap-ops.js +406 -0
  135. package/lib/dist/rendering/render-solid.js +10 -2
  136. package/lib/dist/scene-manager.d.ts +2 -0
  137. package/lib/dist/scene-manager.js +29 -0
  138. package/lib/dist/tests/features/cylinder-curve-filter.test.js +3 -3
  139. package/lib/dist/tests/features/extrude-to-face.test.js +38 -1
  140. package/lib/dist/tests/features/helix.test.d.ts +1 -0
  141. package/lib/dist/tests/features/helix.test.js +295 -0
  142. package/lib/dist/tests/features/repeat-primitive.test.d.ts +1 -0
  143. package/lib/dist/tests/features/repeat-primitive.test.js +60 -0
  144. package/lib/dist/tests/features/rib.test.js +6 -1
  145. package/lib/dist/tests/features/sweep.test.js +125 -1
  146. package/lib/dist/tests/features/text.test.d.ts +1 -0
  147. package/lib/dist/tests/features/text.test.js +347 -0
  148. package/lib/dist/tests/features/wrap-development.test.d.ts +1 -0
  149. package/lib/dist/tests/features/wrap-development.test.js +130 -0
  150. package/lib/dist/tests/features/wrap-extruded-target.test.d.ts +1 -0
  151. package/lib/dist/tests/features/wrap-extruded-target.test.js +106 -0
  152. package/lib/dist/tests/features/wrap-repeat.test.d.ts +1 -0
  153. package/lib/dist/tests/features/wrap-repeat.test.js +93 -0
  154. package/lib/dist/tests/features/wrap.test.d.ts +1 -0
  155. package/lib/dist/tests/features/wrap.test.js +331 -0
  156. package/lib/dist/tests/math/bspline-interpolation.test.d.ts +1 -0
  157. package/lib/dist/tests/math/bspline-interpolation.test.js +119 -0
  158. package/lib/dist/tests/measure.test.d.ts +1 -0
  159. package/lib/dist/tests/measure.test.js +288 -0
  160. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  161. package/llm-docs/api/helix.md +64 -0
  162. package/llm-docs/api/index.json +11 -2
  163. package/llm-docs/api/text.md +52 -0
  164. package/llm-docs/api/types/helix.md +105 -0
  165. package/llm-docs/api/types/text.md +138 -0
  166. package/llm-docs/api/types/wrap.md +131 -0
  167. package/llm-docs/api/wrap.md +62 -0
  168. package/llm-docs/index.json +121 -1
  169. package/mcp/dist/server.js +20 -1
  170. package/mcp/dist/tools/inspection.d.ts +17 -0
  171. package/mcp/dist/tools/inspection.js +14 -0
  172. package/package.json +7 -3
  173. package/server/dist/fluidcad-server.d.ts +29 -0
  174. package/server/dist/fluidcad-server.js +40 -0
  175. package/server/dist/index.js +4 -2
  176. package/server/dist/model-package/pack.js +7 -6
  177. package/server/dist/model-package/types.d.ts +4 -3
  178. package/server/dist/preferences.d.ts +4 -0
  179. package/server/dist/preferences.js +2 -0
  180. package/server/dist/routes/measure.d.ts +3 -0
  181. package/server/dist/routes/measure.js +32 -0
  182. package/server/dist/routes/preferences.js +6 -0
  183. package/server/dist/routes/sketch-edits.js +2 -1
  184. package/ui/dist/assets/{index-CDJmUpFI.css → index-dAFdg2Un.css} +1 -1
  185. package/ui/dist/assets/{index-MRqwG9Vh.js → index-no7mtr5s.js} +149 -102
  186. package/ui/dist/index.html +2 -2
@@ -2,7 +2,8 @@ import { resolve } from 'path';
2
2
  import { getHubUrl, readCredentials } from '../lib/config.js';
3
3
  import { HubClient } from '../lib/api-client.js';
4
4
  import { findEntry, readPackageVersion, readWorkspacePackage } from '../lib/workspace.js';
5
- import { readModelId, writeModelId } from '../lib/model-config.js';
5
+ import { readModelIdentity, writeModelConfig } from '../lib/model-config.js';
6
+ import { isInteractive, select } from '../lib/prompt.js';
6
7
  import { openBrowser } from '../lib/browser.js';
7
8
 
8
9
  async function runPublish(opts) {
@@ -20,7 +21,31 @@ async function runPublish(opts) {
20
21
  const pkg = readWorkspacePackage(workspace);
21
22
  const name = opts.name ?? pkg.name;
22
23
  const description = opts.description ?? pkg.description;
23
- const modelId = readModelId(workspace);
24
+ const { modelId: priorModelId, name: priorName } = readModelIdentity(workspace);
25
+
26
+ // Surface who we are and where this is going *before* any bytes leave the
27
+ // machine, so a wrong account or hub is caught before the upload starts.
28
+ // (The model's own page URL is minted by the hub and printed afterwards.)
29
+ console.log('Publishing to the FluidCAD hub:');
30
+ console.log(` account: ${creds.email || '(unknown account)'}`);
31
+ console.log(` url: ${hubUrl}`);
32
+ console.log('');
33
+
34
+ // Decide new-model vs new-version BEFORE the heavy build, so the user makes
35
+ // the call (and we do any model-list lookup) without first waiting ~110ms for
36
+ // the engine to load. A null target ⇒ the hub mints a fresh model.
37
+ const targetModelId = await resolveTargetModel({
38
+ opts,
39
+ hubUrl,
40
+ token: creds.token,
41
+ priorModelId,
42
+ priorName,
43
+ });
44
+ console.log(
45
+ targetModelId
46
+ ? `Publishing a new version${priorName ? ` of ${priorName}` : ''}.\n`
47
+ : 'Publishing as a new model.\n',
48
+ );
24
49
 
25
50
  // Render once to capture the full param schema for the manifest. This also
26
51
  // acts as a build gate — a compile/runtime error fails the publish here,
@@ -66,8 +91,8 @@ async function runPublish(opts) {
66
91
 
67
92
  const form = new FormData();
68
93
  form.append('fluidpkg', new Blob([zip], { type: 'application/zip' }), 'model.fluidpkg');
69
- if (modelId) {
70
- form.append('modelId', modelId);
94
+ if (targetModelId) {
95
+ form.append('modelId', targetModelId);
71
96
  }
72
97
  if (name) {
73
98
  form.append('name', name);
@@ -90,10 +115,12 @@ async function runPublish(opts) {
90
115
  throw new Error(body.error || `Publish failed (HTTP ${status})`);
91
116
  }
92
117
 
93
- // First publish for this workspace persist the hub-minted id so the next
94
- // publish lands as a new version of the same model.
95
- if (!modelId && body.modelId) {
96
- writeModelId(workspace, body.modelId);
118
+ // Persist the hub-authoritative id (and the name we used) whenever it changed
119
+ // or the workspace had no config covers the first publish, a deliberate new
120
+ // model, and re-attaching a deleted fluidcad.json (the user picked an existing
121
+ // model from the list). An owned-match with an unchanged name is a no-op.
122
+ if (body.modelId && (body.modelId !== priorModelId || (name && name !== priorName))) {
123
+ writeModelConfig(workspace, { modelId: body.modelId, name });
97
124
  }
98
125
 
99
126
  console.log('');
@@ -113,6 +140,72 @@ async function runPublish(opts) {
113
140
  }
114
141
  }
115
142
 
143
+ /**
144
+ * Decide which model this publish targets: an existing model id (→ a new
145
+ * version) or null (→ the hub mints a new model). Honors --new-model /
146
+ * --new-version; otherwise asks when interactive; and with no TTY falls back to
147
+ * today's behavior (a saved fluidcad.json id ⇒ a version, else a new model).
148
+ */
149
+ async function resolveTargetModel({ opts, hubUrl, token, priorModelId, priorName }) {
150
+ if (opts.newModel && opts.newVersion) {
151
+ throw new Error('Pass only one of --new-model / --new-version.');
152
+ }
153
+ if (opts.newModel) return null;
154
+ if (opts.newVersion) {
155
+ if (priorModelId) return priorModelId;
156
+ if (isInteractive()) return pickExistingModel(hubUrl, token);
157
+ throw new Error(
158
+ 'No fluidcad.json here, so there is no model to version. Drop --new-version to ' +
159
+ 'publish a new model, or run interactively to pick an existing one.',
160
+ );
161
+ }
162
+
163
+ // No explicit flag.
164
+ if (!isInteractive()) {
165
+ // Non-interactive (CI, piped input): keep the historical default.
166
+ return priorModelId;
167
+ }
168
+ if (priorModelId) {
169
+ const label = priorName ? `${priorName} (${priorModelId})` : priorModelId;
170
+ return select('How should this publish go up?', [
171
+ { label: `Publish a new version of ${label}`, value: priorModelId },
172
+ { label: 'Publish as a new model', value: null },
173
+ ]);
174
+ }
175
+ const choice = await select('How should this publish go up?', [
176
+ { label: 'Publish a new version of an existing model', value: '__existing__' },
177
+ { label: 'Publish as a new model', value: null },
178
+ ]);
179
+ return choice === '__existing__' ? pickExistingModel(hubUrl, token) : null;
180
+ }
181
+
182
+ /**
183
+ * Fetch the user's own models from the hub and let them pick which one this is a
184
+ * new version of. Returns the chosen model id, or null when they have none yet
185
+ * (the caller then mints a new model).
186
+ */
187
+ async function pickExistingModel(hubUrl, token) {
188
+ const { status, body } = await new HubClient(hubUrl, token).getJson('/api/cli/models');
189
+ if (status === 401) {
190
+ throw new Error('Your session has expired. Run `fluidcad login` again.');
191
+ }
192
+ if (status !== 200) {
193
+ throw new Error(body.error || `Could not list your models (HTTP ${status})`);
194
+ }
195
+ const models = Array.isArray(body.models) ? body.models : [];
196
+ if (models.length === 0) {
197
+ console.log('\nYou have no models on the hub yet — publishing this as a new model.\n');
198
+ return null;
199
+ }
200
+ return select(
201
+ 'Which model is this a new version of?',
202
+ models.map((m) => ({
203
+ label: `${m.name} · ${m.latestVersion ? 'v' + m.latestVersion : 'no versions yet'}`,
204
+ value: m.id,
205
+ })),
206
+ );
207
+ }
208
+
116
209
  export function registerPublishCommand(program) {
117
210
  program
118
211
  .command('publish')
@@ -121,6 +214,8 @@ export function registerPublishCommand(program) {
121
214
  .option('-e, --entry <file>', 'Entry .fluid.js file (auto-detected if only one exists)')
122
215
  .option('-n, --name <name>', 'Model name (defaults to the package name)')
123
216
  .option('-d, --description <text>', 'Optional human description')
217
+ .option('--new-model', 'Publish as a new model, ignoring any saved model id')
218
+ .option('--new-version', 'Publish a new version of the saved (or chosen) model')
124
219
  .option('--visibility <visibility>', 'public | unlisted | private (default: unlisted)')
125
220
  .option('--hub <url>', 'Hub base URL (default: the hub you logged into)')
126
221
  .action((opts) => {
@@ -20,6 +20,14 @@ export class HubClient {
20
20
  return { status: res.status, body };
21
21
  }
22
22
 
23
+ async getJson(path) {
24
+ const res = await fetch(this.base + path, {
25
+ method: 'GET',
26
+ headers: this.#authHeaders(),
27
+ });
28
+ return this.#result(res);
29
+ }
30
+
23
31
  async postJson(path, body) {
24
32
  const res = await fetch(this.base + path, {
25
33
  method: 'POST',
@@ -28,11 +28,34 @@ export function readModelId(workspace) {
28
28
  }
29
29
 
30
30
  /**
31
- * Persist the hub-minted model id, preserving any other fields already in
32
- * `fluidcad.json`. Called after the first publish writes back the new id.
31
+ * The persisted identity for this workspace: the hub model `id` and its
32
+ * last-known `name`. Both come back null when absent first publish, or a
33
+ * `fluidcad.json` the user deleted. The name is shown in the publish prompt so
34
+ * we can name the model offline without a round-trip.
33
35
  */
34
- export function writeModelId(workspace, modelId) {
36
+ export function readModelIdentity(workspace) {
37
+ const cfg = readModelConfig(workspace);
38
+ return {
39
+ modelId: typeof cfg.modelId === 'string' && cfg.modelId ? cfg.modelId : null,
40
+ name: typeof cfg.name === 'string' && cfg.name ? cfg.name : null,
41
+ };
42
+ }
43
+
44
+ /**
45
+ * Shallow-merge `patch` into `fluidcad.json`, preserving any other fields, and
46
+ * write it back (creating the file if needed). `undefined`/`null` values in the
47
+ * patch are skipped, so callers can pass a partial identity without clobbering
48
+ * what's already there.
49
+ */
50
+ export function writeModelConfig(workspace, patch) {
35
51
  const cfg = readModelConfig(workspace);
36
- cfg.modelId = modelId;
52
+ for (const [key, value] of Object.entries(patch)) {
53
+ if (value !== undefined && value !== null) cfg[key] = value;
54
+ }
37
55
  writeFileSync(modelConfigPath(workspace), JSON.stringify(cfg, null, 2) + '\n');
38
56
  }
57
+
58
+ /** Persist just the hub-minted model id (thin wrapper over `writeModelConfig`). */
59
+ export function writeModelId(workspace, modelId) {
60
+ writeModelConfig(workspace, { modelId });
61
+ }
@@ -0,0 +1,97 @@
1
+ import { emitKeypressEvents } from 'readline';
2
+
3
+ /**
4
+ * Minimal interactive prompts for the CLI, built on Node's built-in `readline`
5
+ * keypress events — no dependency (the CLI only ships `commander`). Used by
6
+ * `publish` to choose between a new model and a new version.
7
+ */
8
+
9
+ /**
10
+ * Whether we can run an interactive prompt — both stdin and stdout must be a
11
+ * TTY. CI and piped input are not, so callers fall back to flags/defaults there.
12
+ */
13
+ export function isInteractive() {
14
+ return Boolean(process.stdin.isTTY && process.stdout.isTTY);
15
+ }
16
+
17
+ const HIDE_CURSOR = '\x1b[?25l';
18
+ const SHOW_CURSOR = '\x1b[?25h';
19
+ const CLEAR_LINE = '\x1b[2K';
20
+ const CYAN = '\x1b[36m';
21
+ const DIM = '\x1b[2m';
22
+ const RESET = '\x1b[0m';
23
+
24
+ /**
25
+ * Arrow-key (or j/k) single-select. Renders a menu with one highlighted row,
26
+ * moves the highlight on ↑/↓/k/j, confirms on Enter, and aborts on Ctrl-C.
27
+ * Resolves to the chosen entry's `value`. Assumes `isInteractive()` — it puts
28
+ * stdin in raw mode, so don't call it without a TTY.
29
+ *
30
+ * `choices`: `[{ label, value }]`.
31
+ */
32
+ export function select(message, choices) {
33
+ return new Promise((resolveChoice) => {
34
+ const input = process.stdin;
35
+ const output = process.stdout;
36
+ let active = 0;
37
+
38
+ // Truncate labels so a long one never wraps — a wrapped row would occupy two
39
+ // terminal lines and throw off the cursor-up redraw math below.
40
+ const width = Math.max(8, (output.columns || 80) - 2);
41
+ const fit = (s) => (s.length > width ? s.slice(0, width - 1) + '…' : s);
42
+
43
+ output.write(`${message}\n`);
44
+ output.write(`${DIM} ↑/↓ or j/k to move · Enter to confirm${RESET}\n`);
45
+ output.write(HIDE_CURSOR);
46
+
47
+ const draw = (initial) => {
48
+ if (!initial) output.write(`\x1b[${choices.length}A`); // back up to the first row
49
+ choices.forEach((c, i) => {
50
+ const on = i === active;
51
+ const row = `${on ? '❯' : ' '} ${fit(c.label)}`;
52
+ output.write(`${CLEAR_LINE}${on ? `${CYAN}${row}${RESET}` : row}\n`);
53
+ });
54
+ };
55
+ draw(true);
56
+
57
+ emitKeypressEvents(input);
58
+ const wasRaw = Boolean(input.isRaw);
59
+ input.setRawMode(true);
60
+ input.resume();
61
+
62
+ const restore = () => {
63
+ input.removeListener('keypress', onKey);
64
+ input.setRawMode(wasRaw);
65
+ input.pause();
66
+ output.write(SHOW_CURSOR);
67
+ };
68
+
69
+ const onKey = (_str, key) => {
70
+ if (!key) return;
71
+ if (key.ctrl && key.name === 'c') {
72
+ restore();
73
+ output.write('\n');
74
+ process.exit(130); // 128 + SIGINT, the shell convention for Ctrl-C
75
+ }
76
+ switch (key.name) {
77
+ case 'up':
78
+ case 'k':
79
+ active = (active - 1 + choices.length) % choices.length;
80
+ draw(false);
81
+ break;
82
+ case 'down':
83
+ case 'j':
84
+ active = (active + 1) % choices.length;
85
+ draw(false);
86
+ break;
87
+ case 'return':
88
+ case 'enter':
89
+ restore();
90
+ resolveChoice(choices[active].value);
91
+ break;
92
+ }
93
+ };
94
+
95
+ input.on('keypress', onKey);
96
+ });
97
+ }
@@ -1,5 +1,5 @@
1
1
  import { ShapeType } from "./shape-type.js";
2
- import type { TopoDS_Edge } from "occjs-wrapper";
2
+ import type { TopoDS_Edge } from "ocjs-fluidcad";
3
3
  import { Shape } from "./shape.js";
4
4
  import { Vertex } from "./vertex.js";
5
5
  export declare class Edge extends Shape<TopoDS_Edge> {
@@ -1,4 +1,4 @@
1
- import type { TopoDS_Edge, TopoDS_Face } from "occjs-wrapper";
1
+ import type { TopoDS_Edge, TopoDS_Face } from "ocjs-fluidcad";
2
2
  import { BoundingBox } from "../helpers/types.js";
3
3
  import { ShapeType } from "./shape-type.js";
4
4
  import { Wire } from "./wire.js";
@@ -93,6 +93,12 @@ export declare abstract class SceneObject implements Comparable<SceneObject>, Se
93
93
  clone(): SceneObject[];
94
94
  setTransform(matrix: Matrix4 | LazyMatrix): void;
95
95
  getTransform(): Matrix4 | null;
96
+ /**
97
+ * The raw clone-transform reference set by cloneWithTransform. All clones
98
+ * belonging to one repeat/mirror instance share the same LazyMatrix, so
99
+ * reference identity distinguishes instances of the same container.
100
+ */
101
+ getTransformRef(): LazyMatrix | null;
96
102
  setCloneSource(source: SceneObject): void;
97
103
  getCloneSource(): SceneObject | null;
98
104
  getTransformMatrix(): Matrix4 | null;
@@ -206,6 +206,14 @@ export class SceneObject {
206
206
  getTransform() {
207
207
  return this._transform ? this._transform.resolve() : null;
208
208
  }
209
+ /**
210
+ * The raw clone-transform reference set by cloneWithTransform. All clones
211
+ * belonging to one repeat/mirror instance share the same LazyMatrix, so
212
+ * reference identity distinguishes instances of the same container.
213
+ */
214
+ getTransformRef() {
215
+ return this._transform;
216
+ }
209
217
  setCloneSource(source) {
210
218
  this._cloneSource = source;
211
219
  }
@@ -1,4 +1,4 @@
1
- import type { TopoDS_Shape } from "occjs-wrapper";
1
+ import type { TopoDS_Shape } from "ocjs-fluidcad";
2
2
  import { Solid } from "./solid.js";
3
3
  import { Wire } from "./wire.js";
4
4
  import { Face } from "./face.js";
@@ -1,4 +1,4 @@
1
- import type { BRepBuilderAPI_MakeShape } from "occjs-wrapper";
1
+ import type { BRepBuilderAPI_MakeShape } from "ocjs-fluidcad";
2
2
  import { Face } from "./face.js";
3
3
  import { Edge } from "./edge.js";
4
4
  import { Shape } from "./shape.js";
@@ -1,4 +1,4 @@
1
- import type { TopoDS_Shape } from "occjs-wrapper";
1
+ import type { TopoDS_Shape } from "ocjs-fluidcad";
2
2
  import { ShapeType } from "./shape-type.js";
3
3
  import { SceneObjectMesh } from "../rendering/scene.js";
4
4
  import { Matrix4 } from "../math/matrix4.js";
@@ -1,4 +1,4 @@
1
- import type { TopoDS_Edge, TopoDS_Face, TopoDS_Solid, TopTools_IndexedDataMapOfShapeListOfShape } from "occjs-wrapper";
1
+ import type { TopoDS_Edge, TopoDS_Face, TopoDS_Solid, TopTools_IndexedDataMapOfShapeListOfShape } from "ocjs-fluidcad";
2
2
  import { ShapeType } from "./shape-type.js";
3
3
  import { Shape } from "./shape.js";
4
4
  import { Face } from "./face.js";
@@ -1,4 +1,5 @@
1
- import { SceneObject } from "./scene-object.js";
1
+ import { BuildSceneObjectContext, SceneObject } from "./scene-object.js";
2
+ import { Shape } from "./shape.js";
2
3
  import { Matrix4 } from "../math/matrix4.js";
3
4
  import type { AxisLike } from "../math/axis.js";
4
5
  import type { PlaneLike } from "../math/plane.js";
@@ -6,6 +7,16 @@ import type { PointLike } from "../math/point.js";
6
7
  import { type NumberParam } from "../core/param.js";
7
8
  export declare abstract class TransformablePrimitive extends SceneObject {
8
9
  transform(matrix: Matrix4): this;
10
+ /**
11
+ * Adds a primitive's built shape, baking in the clone transform when this
12
+ * primitive is a repeat/mirror copy. The renderer post-applies the user's
13
+ * own transform (translate/rotate/mirror) after build, so the clone
14
+ * transform is conjugated to land outside it:
15
+ * own · (own⁻¹ · clone · own) = clone · own.
16
+ */
17
+ protected addPrimitiveShape(shape: Shape, context?: BuildSceneObjectContext): void;
18
+ /** Carries the user's own transform onto a repeat/mirror copy. */
19
+ protected syncPrimitiveWith(source: TransformablePrimitive): this;
9
20
  translate(x: NumberParam): this;
10
21
  translate(x: NumberParam, y: NumberParam): this;
11
22
  translate(x: NumberParam, y: NumberParam, z: NumberParam): this;
@@ -3,12 +3,39 @@ import { Matrix4 } from "../math/matrix4.js";
3
3
  import { Point } from "../math/point.js";
4
4
  import { Vector3d } from "../math/vector3d.js";
5
5
  import { rad } from "../helpers/math-helpers.js";
6
+ import { ShapeOps } from "../oc/shape-ops.js";
6
7
  import { isNumberParam, resolveParam } from "../core/param.js";
7
8
  export class TransformablePrimitive extends SceneObject {
8
9
  transform(matrix) {
9
10
  this.composeAppliedTransform(matrix);
10
11
  return this;
11
12
  }
13
+ /**
14
+ * Adds a primitive's built shape, baking in the clone transform when this
15
+ * primitive is a repeat/mirror copy. The renderer post-applies the user's
16
+ * own transform (translate/rotate/mirror) after build, so the clone
17
+ * transform is conjugated to land outside it:
18
+ * own · (own⁻¹ · clone · own) = clone · own.
19
+ */
20
+ addPrimitiveShape(shape, context) {
21
+ const cloneTransform = context?.getTransform() ?? null;
22
+ if (cloneTransform) {
23
+ const own = this.getAppliedTransform();
24
+ const matrix = own
25
+ ? own.inverse().multiply(cloneTransform).multiply(own)
26
+ : cloneTransform;
27
+ shape = ShapeOps.transform(shape, matrix);
28
+ }
29
+ this.addShape(shape);
30
+ }
31
+ /** Carries the user's own transform onto a repeat/mirror copy. */
32
+ syncPrimitiveWith(source) {
33
+ const applied = source.getAppliedTransform();
34
+ if (applied) {
35
+ this.transform(applied);
36
+ }
37
+ return this;
38
+ }
12
39
  translate(a, b, c) {
13
40
  let x, y, z;
14
41
  if (isNumberParam(a)) {
@@ -1,4 +1,4 @@
1
- import type { TopoDS_Vertex } from "occjs-wrapper";
1
+ import type { TopoDS_Vertex } from "ocjs-fluidcad";
2
2
  import { ShapeType } from "./shape-type.js";
3
3
  import { Shape } from "./shape.js";
4
4
  import { Point, Point2D } from "../math/point.js";
@@ -1,4 +1,4 @@
1
- import type { TopoDS_Wire } from "occjs-wrapper";
1
+ import type { TopoDS_Wire } from "ocjs-fluidcad";
2
2
  import { ShapeType } from "./shape-type.js";
3
3
  import { Shape } from "./shape.js";
4
4
  import { Vector3d } from "../math/vector3d.js";
@@ -21,5 +21,6 @@ export { default as offset } from './offset.js';
21
21
  export { default as project } from './project.js';
22
22
  export { default as intersect } from './intersect.js';
23
23
  export { default as bezier } from './bezier.js';
24
+ export { default as text } from './text.js';
24
25
  export { default as center } from './center.js';
25
26
  export { default as back } from './back.js';
@@ -21,5 +21,6 @@ export { default as offset } from './offset.js';
21
21
  export { default as project } from './project.js';
22
22
  export { default as intersect } from './intersect.js';
23
23
  export { default as bezier } from './bezier.js';
24
+ export { default as text } from './text.js';
24
25
  export { default as center } from './center.js';
25
26
  export { default as back } from './back.js';
@@ -0,0 +1,30 @@
1
+ import { PlaneLike } from "../../math/plane.js";
2
+ import { IText, ISceneObject } from "../interfaces.js";
3
+ interface TextFunction {
4
+ /**
5
+ * Renders a text string as extrudable outline geometry inside the current
6
+ * sketch, at the sketch cursor.
7
+ * @param text - The string to render.
8
+ */
9
+ (text: string): IText;
10
+ /**
11
+ * Renders a text string on a specific plane (standalone, outside a sketch).
12
+ * @param plane - The plane (e.g. "xy") or face to render the text on.
13
+ * @param text - The string to render.
14
+ */
15
+ (plane: PlaneLike | ISceneObject, text: string): IText;
16
+ /**
17
+ * Renders a text string following a planar curve. Each glyph is placed
18
+ * upright along the path's arc length; the text plane is the path's plane.
19
+ * Works inside a sketch (following a curve of that sketch) or standalone.
20
+ * The path is left in place — mark it `.guide()` to keep it out of
21
+ * extruded profiles.
22
+ * @param text - The string to render.
23
+ * @param path - The curve to follow: a sketch curve (line/arc/circle), a
24
+ * whole sketch, a planar primitive, or a selected edge/edge loop
25
+ * (e.g. `select(edge().circle())`).
26
+ */
27
+ (text: string, path: ISceneObject): IText;
28
+ }
29
+ declare const _default: TextFunction;
30
+ export default _default;
@@ -0,0 +1,37 @@
1
+ import { Text } from "../../features/2d/text.js";
2
+ import { registerBuilder } from "../../index.js";
3
+ import { isPlaneLike } from "../../math/plane.js";
4
+ import { SceneObject } from "../../common/scene-object.js";
5
+ import { resolvePlane } from "../../helpers/resolve.js";
6
+ function build(context) {
7
+ return function text() {
8
+ const first = arguments[0];
9
+ const second = arguments[1];
10
+ // A trailing scene object is a path to follow: `text("Hi", path)`.
11
+ // Valid both standalone and inside a sketch (following a sketch curve).
12
+ if (arguments.length >= 2 && second instanceof SceneObject) {
13
+ if (typeof first !== "string") {
14
+ throw new Error("text: when following a path, the first argument must be the text string.");
15
+ }
16
+ const obj = new Text(first, null, second);
17
+ context.addSceneObject(obj);
18
+ return obj;
19
+ }
20
+ // A leading plane/face is only valid standalone and only when a string
21
+ // follows it; `text("xy")` (one arg) renders the literal string "xy".
22
+ const standalone = arguments.length >= 2 && (isPlaneLike(first) || first instanceof SceneObject);
23
+ if (standalone) {
24
+ if (context.getActiveSketch() !== null) {
25
+ throw new Error("text(plane, ...) cannot be used inside a sketch. Use text(...) instead.");
26
+ }
27
+ const planeObj = resolvePlane(first, context);
28
+ const obj = new Text(String(arguments[1] ?? ""), planeObj);
29
+ context.addSceneObject(obj);
30
+ return obj;
31
+ }
32
+ const obj = new Text(String(first ?? ""));
33
+ context.addSceneObject(obj);
34
+ return obj;
35
+ };
36
+ }
37
+ export default registerBuilder(build);
@@ -0,0 +1,20 @@
1
+ import { AxisLike } from "../math/axis.js";
2
+ import { IHelix, ISceneObject } from "./interfaces.js";
3
+ interface HelixFunction {
4
+ /**
5
+ * Creates a helix wire along the given axis. Use chained methods (.pitch(),
6
+ * .turns(), .height(), .radius(), .endRadius()) to configure geometry.
7
+ * @param axis - The axis to build the helix around.
8
+ */
9
+ (axis: AxisLike): IHelix;
10
+ /**
11
+ * Creates a helix wire derived from a scene object's geometry.
12
+ * - A cylindrical or conical face: axis + radii + height come from the face.
13
+ * - A line edge: axis = the line, height = line length.
14
+ * - A circular edge: axis = circle normal at center, radius = circle radius.
15
+ * @param source - The scene object whose face/edge defines the helix.
16
+ */
17
+ (source: ISceneObject): IHelix;
18
+ }
19
+ declare const _default: HelixFunction;
20
+ export default _default;
@@ -0,0 +1,36 @@
1
+ import { registerBuilder } from "../index.js";
2
+ import { Helix } from "../features/helix.js";
3
+ import { isAxisLike } from "../math/axis.js";
4
+ import { AxisObject } from "../features/axis.js";
5
+ import { AxisObjectBase } from "../features/axis-renderable-base.js";
6
+ import { SceneObject } from "../common/scene-object.js";
7
+ import { normalizeAxis } from "../helpers/normalize.js";
8
+ function build(context) {
9
+ return function helix() {
10
+ if (arguments.length === 0) {
11
+ throw new Error("helix() requires an axis or scene object argument.");
12
+ }
13
+ const arg = arguments[0];
14
+ let source;
15
+ if (arg instanceof AxisObjectBase) {
16
+ source = arg;
17
+ context.addSceneObject(source);
18
+ }
19
+ else if (isAxisLike(arg)) {
20
+ const axis = normalizeAxis(arg);
21
+ source = new AxisObject(axis);
22
+ context.addSceneObject(source);
23
+ }
24
+ else if (arg instanceof SceneObject) {
25
+ source = arg;
26
+ context.addSceneObject(source);
27
+ }
28
+ else {
29
+ throw new Error("helix(): first argument must be an AxisLike or SceneObject.");
30
+ }
31
+ const result = new Helix(source);
32
+ context.addSceneObject(result);
33
+ return result;
34
+ };
35
+ }
36
+ export default registerBuilder(build);
@@ -1,4 +1,4 @@
1
- export type { ISceneObject, ITransformable, IBooleanOperation, IPlane, IAxis, ISelect, IGeometry, IExtrudableGeometry, IRect, ISlot, IPolygon, ITwoObjectsTangentLine, ITangentArcTwoObjects, IExtrude, ICut, ICommon, ISweep, ILoft, IRevolve, IDraft, IRib } from "./interfaces.js";
1
+ export type { ISceneObject, ITransformable, IBooleanOperation, IPlane, IAxis, ISelect, IGeometry, IExtrudableGeometry, IText, IRect, ISlot, IPolygon, ITwoObjectsTangentLine, ITangentArcTwoObjects, IExtrude, ICut, ICommon, ISweep, ILoft, IRevolve, IDraft, IRib, IHelix, IWrap } from "./interfaces.js";
2
2
  export { default as axis } from "./axis.js";
3
3
  export { default as local } from "./local.js";
4
4
  export { default as plane } from "./plane.js";
@@ -24,6 +24,8 @@ export { default as load } from "./load.js";
24
24
  export { default as loft } from "./loft.js";
25
25
  export { default as sweep } from "./sweep.js";
26
26
  export { default as rib } from "./rib.js";
27
+ export { default as wrap } from "./wrap.js";
28
+ export { default as helix } from "./helix.js";
27
29
  export { default as color } from "./color.js";
28
30
  export { default as draft } from "./draft.js";
29
31
  export { default as remove } from "./remove.js";
@@ -23,6 +23,8 @@ export { default as load } from "./load.js";
23
23
  export { default as loft } from "./loft.js";
24
24
  export { default as sweep } from "./sweep.js";
25
25
  export { default as rib } from "./rib.js";
26
+ export { default as wrap } from "./wrap.js";
27
+ export { default as helix } from "./helix.js";
26
28
  export { default as color } from "./color.js";
27
29
  export { default as draft } from "./draft.js";
28
30
  export { default as remove } from "./remove.js";