fluidcad 0.0.36 → 0.0.38

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 (188) hide show
  1. package/LICENSE.txt +21 -504
  2. package/README.md +1 -1
  3. package/lib/dist/common/edge.d.ts +1 -1
  4. package/lib/dist/common/face.d.ts +1 -1
  5. package/lib/dist/common/scene-object.d.ts +6 -0
  6. package/lib/dist/common/scene-object.js +8 -0
  7. package/lib/dist/common/shape-factory.d.ts +1 -1
  8. package/lib/dist/common/shape-history-tracker.d.ts +1 -1
  9. package/lib/dist/common/shape.d.ts +1 -1
  10. package/lib/dist/common/solid.d.ts +1 -1
  11. package/lib/dist/common/transformable-primitive.d.ts +12 -1
  12. package/lib/dist/common/transformable-primitive.js +27 -0
  13. package/lib/dist/common/vertex.d.ts +1 -1
  14. package/lib/dist/common/wire.d.ts +1 -1
  15. package/lib/dist/core/2d/index.d.ts +1 -0
  16. package/lib/dist/core/2d/index.js +1 -0
  17. package/lib/dist/core/2d/text.d.ts +30 -0
  18. package/lib/dist/core/2d/text.js +37 -0
  19. package/lib/dist/core/helix.d.ts +20 -0
  20. package/lib/dist/core/helix.js +36 -0
  21. package/lib/dist/core/index.d.ts +3 -1
  22. package/lib/dist/core/index.js +2 -0
  23. package/lib/dist/core/interfaces.d.ts +180 -0
  24. package/lib/dist/core/plane.d.ts +26 -6
  25. package/lib/dist/core/plane.js +21 -44
  26. package/lib/dist/core/wrap.d.ts +17 -0
  27. package/lib/dist/core/wrap.js +39 -0
  28. package/lib/dist/features/2d/offset.js +2 -2
  29. package/lib/dist/features/2d/text.d.ts +67 -0
  30. package/lib/dist/features/2d/text.js +320 -0
  31. package/lib/dist/features/cylinder.d.ts +3 -1
  32. package/lib/dist/features/cylinder.js +5 -2
  33. package/lib/dist/features/extrude-base.d.ts +1 -0
  34. package/lib/dist/features/extrude-to-face.d.ts +1 -0
  35. package/lib/dist/features/extrude-to-face.js +6 -0
  36. package/lib/dist/features/fillet.d.ts +1 -1
  37. package/lib/dist/features/helix.d.ts +41 -0
  38. package/lib/dist/features/helix.js +337 -0
  39. package/lib/dist/features/plane-from-object.d.ts +16 -4
  40. package/lib/dist/features/plane-from-object.js +101 -8
  41. package/lib/dist/features/select.js +32 -8
  42. package/lib/dist/features/simple-extruder.d.ts +1 -1
  43. package/lib/dist/features/simple-extruder.js +7 -2
  44. package/lib/dist/features/sphere.d.ts +3 -1
  45. package/lib/dist/features/sphere.js +5 -2
  46. package/lib/dist/features/sweep.js +7 -2
  47. package/lib/dist/features/wrap.d.ts +39 -0
  48. package/lib/dist/features/wrap.js +116 -0
  49. package/lib/dist/filters/edge/belongs-to-face.d.ts +3 -1
  50. package/lib/dist/filters/edge/belongs-to-face.js +14 -10
  51. package/lib/dist/filters/filter.d.ts +1 -1
  52. package/lib/dist/filters/from-object.d.ts +1 -1
  53. package/lib/dist/filters/tangent-expander.d.ts +1 -1
  54. package/lib/dist/filters/tangent-expander.js +57 -40
  55. package/lib/dist/helpers/scene-helpers.d.ts +2 -0
  56. package/lib/dist/helpers/scene-helpers.js +1 -1
  57. package/lib/dist/index.d.ts +2 -0
  58. package/lib/dist/index.js +3 -1
  59. package/lib/dist/io/file-import.d.ts +7 -0
  60. package/lib/dist/io/file-import.js +28 -1
  61. package/lib/dist/io/font-registry.d.ts +45 -0
  62. package/lib/dist/io/font-registry.js +272 -0
  63. package/lib/dist/math/bspline-interpolation.d.ts +29 -0
  64. package/lib/dist/math/bspline-interpolation.js +194 -0
  65. package/lib/dist/oc/boolean-ops.d.ts +3 -1
  66. package/lib/dist/oc/boolean-ops.js +15 -1
  67. package/lib/dist/oc/color-transfer.d.ts +1 -1
  68. package/lib/dist/oc/constraints/constraint-helpers.d.ts +4 -4
  69. package/lib/dist/oc/constraints/curve/tangent-circle-solver.js +10 -9
  70. package/lib/dist/oc/constraints/curve/tangent-line-solver.js +5 -6
  71. package/lib/dist/oc/convert.d.ts +1 -1
  72. package/lib/dist/oc/draft-ops.d.ts +1 -1
  73. package/lib/dist/oc/edge-ops.d.ts +2 -2
  74. package/lib/dist/oc/edge-ops.js +13 -14
  75. package/lib/dist/oc/edge-props.d.ts +1 -1
  76. package/lib/dist/oc/edge-query.d.ts +1 -1
  77. package/lib/dist/oc/edge-query.js +3 -8
  78. package/lib/dist/oc/errors.d.ts +8 -0
  79. package/lib/dist/oc/errors.js +27 -0
  80. package/lib/dist/oc/explorer.d.ts +2 -2
  81. package/lib/dist/oc/extrude-ops.d.ts +28 -2
  82. package/lib/dist/oc/extrude-ops.js +56 -7
  83. package/lib/dist/oc/face-ops.d.ts +2 -1
  84. package/lib/dist/oc/face-ops.js +11 -0
  85. package/lib/dist/oc/face-props.d.ts +1 -1
  86. package/lib/dist/oc/face-query.d.ts +12 -1
  87. package/lib/dist/oc/face-query.js +39 -0
  88. package/lib/dist/oc/fillet-ops.d.ts +1 -1
  89. package/lib/dist/oc/fillet-ops.js +4 -4
  90. package/lib/dist/oc/geometry.d.ts +1 -1
  91. package/lib/dist/oc/geometry.js +12 -14
  92. package/lib/dist/oc/helix-ops.d.ts +37 -0
  93. package/lib/dist/oc/helix-ops.js +88 -0
  94. package/lib/dist/oc/hit-test.d.ts +1 -1
  95. package/lib/dist/oc/index.d.ts +4 -0
  96. package/lib/dist/oc/index.js +2 -0
  97. package/lib/dist/oc/init.d.ts +1 -1
  98. package/lib/dist/oc/init.js +1 -1
  99. package/lib/dist/oc/intersection.js +1 -1
  100. package/lib/dist/oc/io.d.ts +6 -6
  101. package/lib/dist/oc/io.js +31 -24
  102. package/lib/dist/oc/measure/classify.d.ts +34 -0
  103. package/lib/dist/oc/measure/classify.js +246 -0
  104. package/lib/dist/oc/measure/measure-ops.d.ts +9 -0
  105. package/lib/dist/oc/measure/measure-ops.js +210 -0
  106. package/lib/dist/oc/measure/measure-types.d.ts +39 -0
  107. package/lib/dist/oc/measure/measure-types.js +1 -0
  108. package/lib/dist/oc/measure/sampling.d.ts +9 -0
  109. package/lib/dist/oc/measure/sampling.js +77 -0
  110. package/lib/dist/oc/measure/vec.d.ts +13 -0
  111. package/lib/dist/oc/measure/vec.js +23 -0
  112. package/lib/dist/oc/mesh.d.ts +1 -1
  113. package/lib/dist/oc/mesh.js +40 -28
  114. package/lib/dist/oc/path-sampler.d.ts +29 -0
  115. package/lib/dist/oc/path-sampler.js +63 -0
  116. package/lib/dist/oc/props.d.ts +1 -1
  117. package/lib/dist/oc/props.js +4 -1
  118. package/lib/dist/oc/shape-hash.d.ts +26 -0
  119. package/lib/dist/oc/shape-hash.js +32 -0
  120. package/lib/dist/oc/shape-ops.d.ts +5 -3
  121. package/lib/dist/oc/shape-ops.js +6 -5
  122. package/lib/dist/oc/sweep-ops.d.ts +13 -1
  123. package/lib/dist/oc/sweep-ops.js +174 -18
  124. package/lib/dist/oc/text-outline.d.ts +62 -0
  125. package/lib/dist/oc/text-outline.js +212 -0
  126. package/lib/dist/oc/thin-face-maker.d.ts +0 -19
  127. package/lib/dist/oc/thin-face-maker.js +3 -68
  128. package/lib/dist/oc/topology-index.d.ts +1 -1
  129. package/lib/dist/oc/vertex-ops.d.ts +1 -1
  130. package/lib/dist/oc/wire-ops.d.ts +18 -3
  131. package/lib/dist/oc/wire-ops.js +56 -5
  132. package/lib/dist/oc/wrap-development.d.ts +105 -0
  133. package/lib/dist/oc/wrap-development.js +179 -0
  134. package/lib/dist/oc/wrap-ops.d.ts +100 -0
  135. package/lib/dist/oc/wrap-ops.js +406 -0
  136. package/lib/dist/rendering/render-solid.js +10 -2
  137. package/lib/dist/scene-manager.d.ts +2 -0
  138. package/lib/dist/scene-manager.js +29 -0
  139. package/lib/dist/tests/features/2d/offset.test.js +74 -1
  140. package/lib/dist/tests/features/cylinder-curve-filter.test.js +3 -3
  141. package/lib/dist/tests/features/extrude-to-face.test.js +38 -1
  142. package/lib/dist/tests/features/helix.test.d.ts +1 -0
  143. package/lib/dist/tests/features/helix.test.js +295 -0
  144. package/lib/dist/tests/features/plane.test.js +95 -0
  145. package/lib/dist/tests/features/repeat-primitive.test.d.ts +1 -0
  146. package/lib/dist/tests/features/repeat-primitive.test.js +60 -0
  147. package/lib/dist/tests/features/rib.test.js +6 -1
  148. package/lib/dist/tests/features/sweep.test.js +170 -1
  149. package/lib/dist/tests/features/text.test.d.ts +1 -0
  150. package/lib/dist/tests/features/text.test.js +347 -0
  151. package/lib/dist/tests/features/wrap-development.test.d.ts +1 -0
  152. package/lib/dist/tests/features/wrap-development.test.js +130 -0
  153. package/lib/dist/tests/features/wrap-extruded-target.test.d.ts +1 -0
  154. package/lib/dist/tests/features/wrap-extruded-target.test.js +106 -0
  155. package/lib/dist/tests/features/wrap-repeat.test.d.ts +1 -0
  156. package/lib/dist/tests/features/wrap-repeat.test.js +93 -0
  157. package/lib/dist/tests/features/wrap.test.d.ts +1 -0
  158. package/lib/dist/tests/features/wrap.test.js +331 -0
  159. package/lib/dist/tests/math/bspline-interpolation.test.d.ts +1 -0
  160. package/lib/dist/tests/math/bspline-interpolation.test.js +119 -0
  161. package/lib/dist/tests/measure.test.d.ts +1 -0
  162. package/lib/dist/tests/measure.test.js +288 -0
  163. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  164. package/llm-docs/api/helix.md +64 -0
  165. package/llm-docs/api/index.json +11 -2
  166. package/llm-docs/api/text.md +52 -0
  167. package/llm-docs/api/types/helix.md +105 -0
  168. package/llm-docs/api/types/text.md +138 -0
  169. package/llm-docs/api/types/wrap.md +131 -0
  170. package/llm-docs/api/wrap.md +62 -0
  171. package/llm-docs/index.json +121 -1
  172. package/mcp/dist/server.js +20 -1
  173. package/mcp/dist/tools/inspection.d.ts +17 -0
  174. package/mcp/dist/tools/inspection.js +14 -0
  175. package/package.json +7 -3
  176. package/server/dist/fluidcad-server.d.ts +11 -1
  177. package/server/dist/fluidcad-server.js +21 -1
  178. package/server/dist/index.js +4 -2
  179. package/server/dist/preferences.d.ts +4 -0
  180. package/server/dist/preferences.js +2 -0
  181. package/server/dist/routes/measure.d.ts +3 -0
  182. package/server/dist/routes/measure.js +32 -0
  183. package/server/dist/routes/params.js +1 -1
  184. package/server/dist/routes/preferences.js +6 -0
  185. package/server/dist/routes/sketch-edits.js +2 -1
  186. package/ui/dist/assets/{index-MRqwG9Vh.js → index-D8zV21wB.js} +149 -102
  187. package/ui/dist/assets/{index-CDJmUpFI.css → index-dAFdg2Un.css} +1 -1
  188. package/ui/dist/index.html +2 -2
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-06-06T08:18:55.590Z",
3
+ "generatedAt": "2026-06-21T11:15:51.248Z",
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.36",
3
+ "version": "0.0.38",
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';
@@ -109,12 +114,17 @@ export declare class FluidCadServer {
109
114
  processFile(filePath: string, ignoreCache?: boolean): Promise<SceneRenderedData | null>;
110
115
  updateLiveCode(fileName: string, code: string): Promise<SceneRenderedData | null>;
111
116
  rollbackFromUI(index: number): Promise<SceneRenderedData | null>;
112
- recomputeCurrentFile(): Promise<SceneRenderedData | null>;
117
+ recomputeCurrentFile(forceFullRebuild?: boolean): Promise<SceneRenderedData | null>;
113
118
  rollback(fileName: string, index: number): Promise<SceneRenderedData | null>;
114
119
  importFile(workspacePath: string, fileName: string, data: string): Promise<void>;
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;
@@ -220,13 +220,23 @@ export class FluidCadServer {
220
220
  async rollbackFromUI(index) {
221
221
  return this.rollback(this.currentFileName, index);
222
222
  }
223
- async recomputeCurrentFile() {
223
+ async recomputeCurrentFile(forceFullRebuild = false) {
224
224
  if (!this.currentFilePath) {
225
225
  return null;
226
226
  }
227
227
  const sessionId = this.currentFileName;
228
228
  this.renderingCache.delete(sessionId);
229
229
  this.lastRendered.delete(sessionId);
230
+ if (forceFullRebuild) {
231
+ // Drop the incremental-compare baseline so every object is rebuilt from
232
+ // scratch instead of being carried over as cached. Without this, an
233
+ // unchanged file compares equal at every index, the whole scene is
234
+ // marked cached, and the render reuses all geometry — so the explicit
235
+ // "Recompute scene" action does no visible work and reports no build
236
+ // timings (buildDurationMs is only recorded for objects that rebuild).
237
+ // Param edits keep the baseline so slider drags stay fast.
238
+ this.previousScenes.delete(sessionId);
239
+ }
230
240
  return this.processFileInternal(sessionId, this.currentFilePath, true);
231
241
  }
232
242
  async rollback(fileName, index) {
@@ -285,6 +295,16 @@ export class FluidCadServer {
285
295
  }
286
296
  return this.sceneManager.getEdgeProperties(scene, shapeId, edgeIndex);
287
297
  }
298
+ measure(refs) {
299
+ if (!this.sceneManager) {
300
+ return null;
301
+ }
302
+ const scene = this.previousScenes.get(this.currentFileName);
303
+ if (!scene) {
304
+ return null;
305
+ }
306
+ return this.sceneManager.measure(scene, refs);
307
+ }
288
308
  exportShapes(shapeIds, options) {
289
309
  if (!this.sceneManager) {
290
310
  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
  }
@@ -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
+ }
@@ -2,7 +2,7 @@ import { Router } from 'express';
2
2
  export function createParamsRouter(fluidCadServer, sendToExtension, broadcastToUI) {
3
3
  const router = Router();
4
4
  router.post('/recompute', async (_req, res) => {
5
- const data = await fluidCadServer.recomputeCurrentFile();
5
+ const data = await fluidCadServer.recomputeCurrentFile(true);
6
6
  if (!data) {
7
7
  res.status(404).json({ error: 'No active scene' });
8
8
  return;
@@ -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, '');