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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-05-29T18:50:46.977Z",
3
+ "generatedAt": "2026-06-12T18:19:00.445Z",
4
4
  "docs": [
5
5
  {
6
6
  "id": "api/arc",
@@ -394,6 +394,27 @@
394
394
  "file": "api/fillet.md",
395
395
  "bodyLength": 1243
396
396
  },
397
+ {
398
+ "id": "api/helix",
399
+ "title": "helix(axis | source)",
400
+ "summary": "Creates a helical wire around an axis or derived from existing geometry (cylindrical/conical face, line or circular edge) — the path for springs, threads, and coils. Sweep a profile along it to make material.",
401
+ "tags": [
402
+ "api",
403
+ "3d",
404
+ "wire"
405
+ ],
406
+ "symbols": [
407
+ "helix"
408
+ ],
409
+ "seeAlso": [
410
+ "api/sweep",
411
+ "api/axis",
412
+ "api/select",
413
+ "api/types/helix"
414
+ ],
415
+ "file": "api/helix.md",
416
+ "bodyLength": 2133
417
+ },
397
418
  {
398
419
  "id": "api/line",
399
420
  "title": "line(end) / line(start, end)",
@@ -887,6 +908,27 @@
887
908
  "file": "api/tcircle.md",
888
909
  "bodyLength": 755
889
910
  },
911
+ {
912
+ "id": "api/text",
913
+ "title": "text(string) / text(plane, string) / text(string, path)",
914
+ "summary": "Renders a text string as sketch geometry — glyph outlines that extrude, wrap, or follow a path like any other profile.",
915
+ "tags": [
916
+ "api",
917
+ "2d",
918
+ "sketch"
919
+ ],
920
+ "symbols": [
921
+ "text"
922
+ ],
923
+ "seeAlso": [
924
+ "api/sketch",
925
+ "api/wrap",
926
+ "api/extrude",
927
+ "api/types/text"
928
+ ],
929
+ "file": "api/text.md",
930
+ "bodyLength": 1591
931
+ },
890
932
  {
891
933
  "id": "api/tline",
892
934
  "title": "tLine — tangent line",
@@ -1187,6 +1229,25 @@
1187
1229
  "file": "api/types/geometry.md",
1188
1230
  "bodyLength": 909
1189
1231
  },
1232
+ {
1233
+ "id": "api/types/helix",
1234
+ "title": "Helix",
1235
+ "summary": "A 3D helix wire — a single edge that traces a helix curve on a cylindrical or conical surface.",
1236
+ "tags": [
1237
+ "api",
1238
+ "type",
1239
+ "interface"
1240
+ ],
1241
+ "symbols": [
1242
+ "Helix",
1243
+ "IHelix"
1244
+ ],
1245
+ "seeAlso": [
1246
+ "api/types/scene-object"
1247
+ ],
1248
+ "file": "api/types/helix.md",
1249
+ "bodyLength": 2680
1250
+ },
1190
1251
  {
1191
1252
  "id": "api/types/hline",
1192
1253
  "title": "HLine",
@@ -1585,6 +1646,25 @@
1585
1646
  "file": "api/types/tangent-arc-two-objects.md",
1586
1647
  "bodyLength": 847
1587
1648
  },
1649
+ {
1650
+ "id": "api/types/text",
1651
+ "title": "Text",
1652
+ "summary": "The Text type. Extends ExtrudableGeometry; adds 11 methods.",
1653
+ "tags": [
1654
+ "api",
1655
+ "type",
1656
+ "interface"
1657
+ ],
1658
+ "symbols": [
1659
+ "Text",
1660
+ "IText"
1661
+ ],
1662
+ "seeAlso": [
1663
+ "api/types/extrudable-geometry"
1664
+ ],
1665
+ "file": "api/types/text.md",
1666
+ "bodyLength": 4156
1667
+ },
1588
1668
  {
1589
1669
  "id": "api/types/transformable",
1590
1670
  "title": "Transformable",
@@ -1684,6 +1764,46 @@
1684
1764
  "file": "api/types/vline.md",
1685
1765
  "bodyLength": 641
1686
1766
  },
1767
+ {
1768
+ "id": "api/types/wrap",
1769
+ "title": "Wrap",
1770
+ "summary": "The Wrap type. Extends BooleanOperation; adds 10 methods.",
1771
+ "tags": [
1772
+ "api",
1773
+ "type",
1774
+ "interface"
1775
+ ],
1776
+ "symbols": [
1777
+ "Wrap",
1778
+ "IWrap"
1779
+ ],
1780
+ "seeAlso": [
1781
+ "api/types/boolean-operation"
1782
+ ],
1783
+ "file": "api/types/wrap.md",
1784
+ "bodyLength": 3888
1785
+ },
1786
+ {
1787
+ "id": "api/wrap",
1788
+ "title": "wrap(thickness, sketch, face)",
1789
+ "summary": "Develops a sketch onto a cylindrical or conical face and raises it by a thickness — embossed or engraved labels, logos, and features that follow a curved wall.",
1790
+ "tags": [
1791
+ "api",
1792
+ "3d",
1793
+ "solid"
1794
+ ],
1795
+ "symbols": [
1796
+ "wrap"
1797
+ ],
1798
+ "seeAlso": [
1799
+ "api/sketch",
1800
+ "api/types/text",
1801
+ "api/select",
1802
+ "api/extrude"
1803
+ ],
1804
+ "file": "api/wrap.md",
1805
+ "bodyLength": 2024
1806
+ },
1687
1807
  {
1688
1808
  "id": "concepts/coordinate-system",
1689
1809
  "title": "Coordinate systems and sketch axes",
@@ -11,7 +11,7 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
11
11
  import { z } from 'zod';
12
12
  import { listWorkspaces } from "./tools/workspaces.js";
13
13
  import { getApiSignature, getTypeDefinition, listDocs, readDoc, searchDocs, } from "./tools/docs.js";
14
- import { getCompileError, getEdgeProperties, getFaceProperties, getSceneSummary, getShapeProperties, hitTest, listShapes, } from "./tools/inspection.js";
14
+ import { getCompileError, getEdgeProperties, getFaceProperties, getSceneSummary, getShapeProperties, hitTest, listShapes, measure, } from "./tools/inspection.js";
15
15
  import { getCameraState, screenshot, screenshotMulti, screenshotShape, } from "./tools/screenshot.js";
16
16
  import { waitForIdle } from "./tools/coordination.js";
17
17
  import { editRange, listFluidFiles, readFile, writeFile, } from "./tools/source.js";
@@ -186,6 +186,25 @@ export function buildServer(options = {}) {
186
186
  edgeIndex: edgeIndexArg,
187
187
  },
188
188
  }, async ({ workspace, shapeId, edgeIndex }) => toMcp(await getEdgeProperties({ workspace, shapeId, edgeIndex })));
189
+ const measureEntityArg = z.object({
190
+ shapeId: shapeIdArg,
191
+ kind: z.enum(['face', 'edge']).describe('Whether the index refers to a face or an edge of the shape.'),
192
+ index: z.number().int().nonnegative().describe('Zero-based face/edge index inside the shape.'),
193
+ });
194
+ server.registerTool('measure', {
195
+ title: 'Measure distances and angles between faces/edges',
196
+ description: 'Measures the selected faces/edges like a CAD measure tool. One entity returns its area/length; two entities ' +
197
+ 'return min/max distance with their realizing points, plus parallel/center/axis distance and angle when the ' +
198
+ 'geometry relation supports them. `primary` names the headline value. All lengths are mm, angles deg.',
199
+ inputSchema: {
200
+ ...workspaceArg,
201
+ entities: z
202
+ .array(measureEntityArg)
203
+ .min(1)
204
+ .max(8)
205
+ .describe('Faces/edges to measure (1-8). Pairwise measurements are computed when exactly 2 are given.'),
206
+ },
207
+ }, async ({ workspace, entities }) => toMcp(await measure({ workspace, entities })));
189
208
  const namedViewArg = z.enum([
190
209
  'front', 'back', 'left', 'right', 'top', 'bottom',
191
210
  'iso-ftr', 'iso-fbr', 'iso-ftl', 'iso-fbl',
@@ -73,3 +73,20 @@ export declare function hitTest(input: HitTestInput): Promise<{
73
73
  ok: true;
74
74
  data: unknown;
75
75
  }>;
76
+ export type MeasureEntityInput = {
77
+ shapeId: string;
78
+ kind: 'face' | 'edge';
79
+ index: number;
80
+ };
81
+ export type MeasureInput = WorkspaceArg & {
82
+ entities: MeasureEntityInput[];
83
+ };
84
+ export declare function measure(input: MeasureInput): Promise<{
85
+ ok: false;
86
+ code: import("../types.ts").ToolErrorCode;
87
+ message: string;
88
+ details?: unknown;
89
+ } | {
90
+ ok: true;
91
+ data: unknown;
92
+ }>;
@@ -114,6 +114,20 @@ export async function hitTest(input) {
114
114
  };
115
115
  return callWithClient(input, (client) => client.postJson('/api/hit-test', body));
116
116
  }
117
+ export async function measure(input) {
118
+ const entities = input?.entities;
119
+ if (!Array.isArray(entities) || entities.length < 1 || entities.length > 8) {
120
+ return err('invalid-input', '`entities` must be an array of 1-8 face/edge references.');
121
+ }
122
+ for (const entity of entities) {
123
+ const validKind = entity?.kind === 'face' || entity?.kind === 'edge';
124
+ const validIndex = typeof entity?.index === 'number' && Number.isInteger(entity.index) && entity.index >= 0;
125
+ if (!entity || typeof entity.shapeId !== 'string' || !entity.shapeId || !validKind || !validIndex) {
126
+ return err('invalid-input', 'Each entity needs a `shapeId`, a `kind` (face|edge) and a non-negative `index`.');
127
+ }
128
+ }
129
+ return callWithClient(input, (client) => client.postJson('/api/measure', { entities }));
130
+ }
117
131
  function isVec3(value) {
118
132
  return (Array.isArray(value) &&
119
133
  value.length === 3 &&
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "fluidcad",
3
- "version": "0.0.35",
3
+ "version": "0.0.37",
4
4
  "description": "Parametric CAD modeling library using javascript",
5
5
  "author": "Marwan Aouida <contact@marwan.dev>",
6
+ "license": "MIT",
6
7
  "homepage": "https://fluidcad.io",
7
8
  "repository": {
8
9
  "type": "git",
@@ -69,10 +70,12 @@
69
70
  "commander": "^14.0.3",
70
71
  "esbuild": "^0.27.7",
71
72
  "express": "^5.2.1",
73
+ "fontkit": "^2.0.4",
74
+ "get-system-fonts": "^2.0.2",
75
+ "iconoir": "^7.11.0",
72
76
  "ignore": "^5.3.2",
73
77
  "jszip": "^3.10.1",
74
- "iconoir": "^7.11.0",
75
- "occjs-wrapper": "npm:occjs-fluidcad@9.0.0",
78
+ "ocjs-fluidcad": "^1.1.0",
76
79
  "open": "^11.0.0",
77
80
  "stacktrace-parser": "^0.1.11",
78
81
  "tree-sitter-wasms": "^0.1.13",
@@ -87,6 +90,7 @@
87
90
  "@tabler/icons": "^3.40.0",
88
91
  "@tailwindcss/vite": "^4.2.2",
89
92
  "@types/express": "^5.0.6",
93
+ "@types/fontkit": "^2.0.9",
90
94
  "@types/js-yaml": "^4.0.9",
91
95
  "@types/node": "^22.14.1",
92
96
  "@types/three": "^0.180.0",
@@ -11,6 +11,11 @@ type SceneManager = {
11
11
  getShapeProperties(scene: any, shapeId: string): any;
12
12
  getFaceProperties(scene: any, shapeId: string, faceIndex: number): any;
13
13
  getEdgeProperties(scene: any, shapeId: string, edgeIndex: number): any;
14
+ measure(scene: any, refs: {
15
+ shapeId: string;
16
+ kind: 'face' | 'edge';
17
+ index: number;
18
+ }[]): any;
14
19
  hitTest(scene: any, shapeId: string, rayOrigin: [number, number, number], rayDir: [number, number, number], edgeThreshold: number): any;
15
20
  exportShapes(scene: any, shapeIds: string[], options: {
16
21
  format: 'step' | 'stl';
@@ -115,6 +120,11 @@ export declare class FluidCadServer {
115
120
  getShapeProperties(shapeId: string): any;
116
121
  getFaceProperties(shapeId: string, faceIndex: number): any;
117
122
  getEdgeProperties(shapeId: string, edgeIndex: number): any;
123
+ measure(refs: {
124
+ shapeId: string;
125
+ kind: 'face' | 'edge';
126
+ index: number;
127
+ }[]): any;
118
128
  exportShapes(shapeIds: string[], options: {
119
129
  format: 'step' | 'stl';
120
130
  includeColors?: boolean;
@@ -125,6 +135,25 @@ export declare class FluidCadServer {
125
135
  data: string | Uint8Array;
126
136
  fileName: string;
127
137
  } | null;
138
+ /**
139
+ * Export every solid of a hub session's latest render. The session-keyed twin
140
+ * of `exportShapes` (which reads the desktop `currentFileName`): hub mode keys
141
+ * each render's scene by `sessionId`, so exporting/downloading from a hub
142
+ * session must look it up the same way — exactly why `hitTestForSession`
143
+ * exists. Gathers all solids itself ("download the whole model"); returns null
144
+ * when the session has no rendered scene or it holds no solids (the caller maps
145
+ * that to a "nothing to export" response).
146
+ */
147
+ exportShapesForSession(sessionId: string, options: {
148
+ format: 'step' | 'stl';
149
+ includeColors?: boolean;
150
+ resolution?: string;
151
+ customLinearDeflection?: number;
152
+ customAngularDeflectionDeg?: number;
153
+ }): {
154
+ data: string | Uint8Array;
155
+ fileName: string;
156
+ } | null;
128
157
  hitTest(shapeId: string, rayOrigin: [number, number, number], rayDir: [number, number, number], edgeThreshold: number): any;
129
158
  hitTestForSession(sessionId: string, shapeId: string, rayOrigin: [number, number, number], rayDir: [number, number, number], edgeThreshold: number): any;
130
159
  setCompileError(err: CompileError | null): void;
@@ -285,6 +285,16 @@ export class FluidCadServer {
285
285
  }
286
286
  return this.sceneManager.getEdgeProperties(scene, shapeId, edgeIndex);
287
287
  }
288
+ measure(refs) {
289
+ if (!this.sceneManager) {
290
+ return null;
291
+ }
292
+ const scene = this.previousScenes.get(this.currentFileName);
293
+ if (!scene) {
294
+ return null;
295
+ }
296
+ return this.sceneManager.measure(scene, refs);
297
+ }
288
298
  exportShapes(shapeIds, options) {
289
299
  if (!this.sceneManager) {
290
300
  return null;
@@ -295,6 +305,36 @@ export class FluidCadServer {
295
305
  }
296
306
  return this.sceneManager.exportShapes(scene, shapeIds, options);
297
307
  }
308
+ /**
309
+ * Export every solid of a hub session's latest render. The session-keyed twin
310
+ * of `exportShapes` (which reads the desktop `currentFileName`): hub mode keys
311
+ * each render's scene by `sessionId`, so exporting/downloading from a hub
312
+ * session must look it up the same way — exactly why `hitTestForSession`
313
+ * exists. Gathers all solids itself ("download the whole model"); returns null
314
+ * when the session has no rendered scene or it holds no solids (the caller maps
315
+ * that to a "nothing to export" response).
316
+ */
317
+ exportShapesForSession(sessionId, options) {
318
+ if (!this.sceneManager) {
319
+ return null;
320
+ }
321
+ const scene = this.previousScenes.get(sessionId);
322
+ if (!scene) {
323
+ return null;
324
+ }
325
+ const shapeIds = [];
326
+ for (const obj of scene.getAllSceneObjects()) {
327
+ for (const shape of obj.getAddedShapes()) {
328
+ if (shape.isSolid()) {
329
+ shapeIds.push(shape.id);
330
+ }
331
+ }
332
+ }
333
+ if (shapeIds.length === 0) {
334
+ return null;
335
+ }
336
+ return this.sceneManager.exportShapes(scene, shapeIds, options);
337
+ }
298
338
  hitTest(shapeId, rayOrigin, rayDir, edgeThreshold) {
299
339
  if (!this.sceneManager) {
300
340
  return null;
@@ -7,6 +7,7 @@ import { createServerCore } from "./server-core.js";
7
7
  import { createPropertiesRouter } from "./routes/properties.js";
8
8
  import { createParamsRouter } from "./routes/params.js";
9
9
  import { createHitTestRouter } from "./routes/hit-test.js";
10
+ import { createMeasureRouter } from "./routes/measure.js";
10
11
  import { createTimelineRouter } from "./routes/timeline.js";
11
12
  import { createSketchEditsRouter } from "./routes/sketch-edits.js";
12
13
  import { createExportRouter } from "./routes/export.js";
@@ -21,7 +22,7 @@ import { createPackRouter } from "./routes/pack.js";
21
22
  import { normalizePath } from "./normalize-path.js";
22
23
  import { writeInstanceFile, deleteInstanceFile } from "./instance-file.js";
23
24
  import { addInstance, removeInstance } from "./global-registry.js";
24
- import { extractSourceLocation } from '../../lib/dist/index.js';
25
+ import { extractSourceLocation, describeOcException } from '../../lib/dist/index.js';
25
26
  const PORT = parseInt(process.env.FLUIDCAD_SERVER_PORT || '3100', 10);
26
27
  const WORKSPACE_PATH = normalizePath(process.env.FLUIDCAD_WORKSPACE_PATH || '');
27
28
  const UI_DIST = path.resolve(import.meta.dirname, '../../ui/dist');
@@ -72,6 +73,7 @@ app.use('/api', createHealthRouter({
72
73
  app.use('/api', createPropertiesRouter(fluidCadServer));
73
74
  app.use('/api', createParamsRouter(fluidCadServer, sendToExtension, broadcastToUI));
74
75
  app.use('/api', createHitTestRouter(fluidCadServer));
76
+ app.use('/api', createMeasureRouter(fluidCadServer));
75
77
  app.use('/api', createTimelineRouter(fluidCadServer, sendToExtension, broadcastToUI));
76
78
  app.use('/api', createSketchEditsRouter(fluidCadServer, sendToExtension, WORKSPACE_PATH));
77
79
  app.use('/api', createExportRouter(fluidCadServer, WORKSPACE_PATH));
@@ -252,7 +254,7 @@ async function handleExtensionMessage(msg) {
252
254
  sendToExtension({ type: 'import-complete', success: true });
253
255
  }
254
256
  catch (err) {
255
- sendToExtension({ type: 'error', message: err.stack || err.message || String(err) });
257
+ sendToExtension({ type: 'error', message: describeOcException(err) });
256
258
  }
257
259
  break;
258
260
  }
@@ -116,12 +116,13 @@ async function collectImportAssetPaths(workspacePath) {
116
116
  await walk(workspacePath);
117
117
  return out.sort();
118
118
  }
119
- // Enforced on top of any `.gitignore`: dependency trees and prior pack outputs
120
- // (the latter would otherwise recurse into the next pack). `node_modules` is
121
- // also pruned during the walk for speed. Hidden dot-entries are excluded by the
122
- // walk directly (see below), so VCS metadata (`.git`) and secrets (`.env`) need
123
- // no pattern here.
124
- const ALWAYS_EXCLUDE = ['node_modules', '*.fluidpkg'];
119
+ // Enforced on top of any `.gitignore`: dependency trees, prior pack outputs
120
+ // (the latter would otherwise recurse into the next pack), and `fluidcad.json`
121
+ // (the local hub binding model id + name which the hub already owns and
122
+ // should never ship as model source). `node_modules` is also pruned during the
123
+ // walk for speed. Hidden dot-entries are excluded by the walk directly (see
124
+ // below), so VCS metadata (`.git`) and secrets (`.env`) need no pattern here.
125
+ const ALWAYS_EXCLUDE = ['node_modules', '*.fluidpkg', 'fluidcad.json'];
125
126
  // `ignore` ships a CJS `module.exports = factory`, but its bundled types use
126
127
  // `export default`, which loses the call signature under `module: nodenext`.
127
128
  // Pin the factory's real signature; the runtime value is the callable factory.
@@ -43,9 +43,10 @@ export interface ModelPackageManifest {
43
43
  * brep/STEP) are NOT repeated here; `files` + `assets` is the whole package.
44
44
  *
45
45
  * Selection respects a root `.gitignore` (via the `ignore` package) and
46
- * always excludes `node_modules`, prior `*.fluidpkg` outputs, and every
47
- * hidden dot-entry (`.git`, `.env`, `.claude`, `.vscode`, … — never model
48
- * content, may hold secrets), whether or not they're gitignored.
46
+ * always excludes `node_modules`, prior `*.fluidpkg` outputs, `fluidcad.json`
47
+ * (the local hub binding), and every hidden dot-entry (`.git`, `.env`,
48
+ * `.claude`, `.vscode`, … — never model content, may hold secrets), whether or
49
+ * not they're gitignored.
49
50
  */
50
51
  files: string[];
51
52
  params?: Record<string, ParamValue>;
@@ -1,8 +1,12 @@
1
+ export type MeasureLengthUnit = 'mm' | 'cm' | 'm' | 'in';
2
+ export type MeasureAngleUnit = 'deg' | 'rad';
1
3
  export interface Preferences {
2
4
  theme: string;
3
5
  showGrid: boolean;
4
6
  cameraMode: 'perspective' | 'orthographic';
5
7
  showBuildTimings: boolean;
8
+ measureLengthUnit: MeasureLengthUnit;
9
+ measureAngleUnit: MeasureAngleUnit;
6
10
  }
7
11
  export declare function loadPreferences(): Promise<Preferences>;
8
12
  export declare function savePreferences(prefs: Preferences): Promise<void>;
@@ -6,6 +6,8 @@ const DEFAULTS = {
6
6
  showGrid: true,
7
7
  cameraMode: 'orthographic',
8
8
  showBuildTimings: false,
9
+ measureLengthUnit: 'mm',
10
+ measureAngleUnit: 'deg',
9
11
  };
10
12
  function getConfigDir() {
11
13
  const platform = process.platform;
@@ -0,0 +1,3 @@
1
+ import { Router } from 'express';
2
+ import type { FluidCadServer } from '../fluidcad-server.ts';
3
+ export declare function createMeasureRouter(fluidCadServer: FluidCadServer): Router;
@@ -0,0 +1,32 @@
1
+ import { Router } from 'express';
2
+ const MAX_ENTITIES = 8;
3
+ export function createMeasureRouter(fluidCadServer) {
4
+ const router = Router();
5
+ router.post('/measure', (req, res) => {
6
+ const entities = req.body?.entities;
7
+ if (!Array.isArray(entities) || entities.length < 1 || entities.length > MAX_ENTITIES) {
8
+ res.status(400).json({ error: `entities must be an array of 1-${MAX_ENTITIES} face/edge references` });
9
+ return;
10
+ }
11
+ for (const entity of entities) {
12
+ const validKind = entity?.kind === 'face' || entity?.kind === 'edge';
13
+ const validIndex = Number.isInteger(entity?.index) && entity.index >= 0;
14
+ if (!entity || typeof entity.shapeId !== 'string' || !entity.shapeId || !validKind || !validIndex) {
15
+ res.status(400).json({ error: 'Each entity needs a shapeId, a kind (face|edge) and a non-negative index' });
16
+ return;
17
+ }
18
+ }
19
+ try {
20
+ const result = fluidCadServer.measure(entities);
21
+ if (!result) {
22
+ res.status(404).json({ error: 'Entity not found' });
23
+ return;
24
+ }
25
+ res.json(result);
26
+ }
27
+ catch (err) {
28
+ res.status(500).json({ error: err?.message ?? String(err) });
29
+ }
30
+ });
31
+ return router;
32
+ }
@@ -27,6 +27,12 @@ export function createPreferencesRouter() {
27
27
  if (typeof body.showBuildTimings === 'boolean') {
28
28
  current.showBuildTimings = body.showBuildTimings;
29
29
  }
30
+ if (['mm', 'cm', 'm', 'in'].includes(body.measureLengthUnit)) {
31
+ current.measureLengthUnit = body.measureLengthUnit;
32
+ }
33
+ if (['deg', 'rad'].includes(body.measureAngleUnit)) {
34
+ current.measureAngleUnit = body.measureAngleUnit;
35
+ }
30
36
  await savePreferences(current);
31
37
  res.json(current);
32
38
  }
@@ -1,4 +1,5 @@
1
1
  import { Router } from 'express';
2
+ import { describeOcException } from '../../../lib/dist/index.js';
2
3
  import { addBreakpoint, removeBreakpoint, toggleBreakpoint, clearBreakpoints, insertPoint, removePoint, addPick, removePick, setPickPoints, insertGeometryCallWithVariable, updateGeometryPosition, setLinePosition, setChainPositions, updateDimension, updateDimensionExpressionWithVariable, getDimensionExpression, extractVariablesInScope, setRectDimensions, } from "../code-editor.js";
3
4
  const NEW_VAR_NAME_RE = /^[a-zA-Z_$][\w$]*$/;
4
5
  function validateNewVariable(input) {
@@ -32,7 +33,7 @@ export function createSketchEditsRouter(fluidCadServer, sendToExtension, workspa
32
33
  await fluidCadServer.importFile(workspacePath, fileName, data);
33
34
  }
34
35
  catch (err) {
35
- res.status(500).json({ error: err.message || String(err) });
36
+ res.status(500).json({ error: describeOcException(err) });
36
37
  return;
37
38
  }
38
39
  const loadName = fileName.replace(/\.(step|stp)$/i, '');