fluidcad 0.0.33 → 0.0.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/README.md +3 -2
  2. package/bin/commands/init.js +55 -0
  3. package/bin/commands/mcp.js +33 -0
  4. package/bin/commands/serve.js +77 -0
  5. package/bin/fluidcad.js +15 -107
  6. package/lib/dist/common/scene-object.d.ts +2 -1
  7. package/lib/dist/common/scene-object.js +3 -2
  8. package/lib/dist/core/2d/tarc.d.ts +20 -2
  9. package/lib/dist/core/2d/tarc.js +24 -0
  10. package/lib/dist/core/interfaces.d.ts +31 -2
  11. package/lib/dist/core/repeat.js +62 -46
  12. package/lib/dist/features/2d/arc.d.ts +8 -2
  13. package/lib/dist/features/2d/arc.js +94 -17
  14. package/lib/dist/features/2d/back.js +3 -2
  15. package/lib/dist/features/2d/sketch.d.ts +4 -0
  16. package/lib/dist/features/2d/sketch.js +21 -0
  17. package/lib/dist/features/2d/tarc-constrained.d.ts +2 -0
  18. package/lib/dist/features/2d/tarc-constrained.js +8 -0
  19. package/lib/dist/features/2d/tarc-radius-to-object.d.ts +16 -0
  20. package/lib/dist/features/2d/tarc-radius-to-object.js +58 -0
  21. package/lib/dist/features/2d/tarc-to-object.d.ts +18 -0
  22. package/lib/dist/features/2d/tarc-to-object.js +66 -0
  23. package/lib/dist/features/2d/tarc-to-point-tangent.d.ts +2 -0
  24. package/lib/dist/features/2d/tarc-to-point-tangent.js +3 -0
  25. package/lib/dist/features/2d/tarc-to-point.d.ts +2 -0
  26. package/lib/dist/features/2d/tarc-to-point.js +3 -0
  27. package/lib/dist/features/2d/tarc-with-tangent.d.ts +2 -0
  28. package/lib/dist/features/2d/tarc-with-tangent.js +3 -0
  29. package/lib/dist/features/2d/tarc.d.ts +2 -0
  30. package/lib/dist/features/2d/tarc.js +3 -0
  31. package/lib/dist/features/extrude-base.d.ts +9 -0
  32. package/lib/dist/features/extrude-base.js +22 -0
  33. package/lib/dist/features/extrude-to-face.js +1 -5
  34. package/lib/dist/features/extrude-two-distances.js +1 -2
  35. package/lib/dist/features/extrude.js +1 -2
  36. package/lib/dist/features/mirror-feature.d.ts +3 -2
  37. package/lib/dist/features/mirror-feature.js +1 -1
  38. package/lib/dist/features/repeat-circular.d.ts +3 -3
  39. package/lib/dist/features/repeat-circular.js +8 -1
  40. package/lib/dist/features/repeat-linear.d.ts +4 -2
  41. package/lib/dist/features/repeat-linear.js +10 -1
  42. package/lib/dist/features/repeat-matrix.d.ts +3 -1
  43. package/lib/dist/features/repeat-matrix.js +7 -2
  44. package/lib/dist/features/shell.d.ts +4 -1
  45. package/lib/dist/features/shell.js +14 -3
  46. package/lib/dist/helpers/clone-transform.d.ts +2 -1
  47. package/lib/dist/index.d.ts +7 -1
  48. package/lib/dist/index.js +3 -3
  49. package/lib/dist/math/lazy-matrix.d.ts +31 -0
  50. package/lib/dist/math/lazy-matrix.js +66 -0
  51. package/lib/dist/oc/constraints/constraint-solver-adaptor.d.ts +5 -0
  52. package/lib/dist/oc/constraints/constraint-solver-adaptor.js +16 -0
  53. package/lib/dist/oc/constraints/constraint-solver.d.ts +4 -0
  54. package/lib/dist/oc/constraints/curve/curve-constraint-solver.d.ts +4 -0
  55. package/lib/dist/oc/constraints/curve/curve-constraint-solver.js +3 -0
  56. package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.d.ts +6 -1
  57. package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.js +4 -0
  58. package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.d.ts +8 -0
  59. package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.js +111 -0
  60. package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.d.ts +8 -0
  61. package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.js +161 -0
  62. package/lib/dist/oc/mesh.d.ts +9 -4
  63. package/lib/dist/oc/mesh.js +14 -13
  64. package/lib/dist/oc/shell-ops.d.ts +2 -1
  65. package/lib/dist/oc/shell-ops.js +5 -2
  66. package/lib/dist/rendering/mesh-builder.d.ts +3 -0
  67. package/lib/dist/rendering/mesh-builder.js +8 -4
  68. package/lib/dist/rendering/render-edge.d.ts +2 -1
  69. package/lib/dist/rendering/render-edge.js +2 -2
  70. package/lib/dist/rendering/render-face.d.ts +2 -1
  71. package/lib/dist/rendering/render-face.js +2 -2
  72. package/lib/dist/rendering/render-solid.d.ts +2 -1
  73. package/lib/dist/rendering/render-solid.js +2 -2
  74. package/lib/dist/rendering/render-wire.d.ts +2 -1
  75. package/lib/dist/rendering/render-wire.js +2 -2
  76. package/lib/dist/rendering/render.d.ts +3 -0
  77. package/lib/dist/rendering/render.js +7 -2
  78. package/lib/dist/scene-manager.d.ts +4 -2
  79. package/lib/dist/scene-manager.js +12 -4
  80. package/lib/dist/tests/features/2d/arc.test.js +64 -0
  81. package/lib/dist/tests/features/2d/back.test.js +17 -1
  82. package/lib/dist/tests/features/2d/tarc.test.js +157 -0
  83. package/lib/dist/tests/features/repeat-user-repro.test.d.ts +1 -0
  84. package/lib/dist/tests/features/repeat-user-repro.test.js +60 -0
  85. package/lib/dist/tests/features/shell.test.js +36 -0
  86. package/lib/dist/tests/global-setup.js +2 -1
  87. package/lib/dist/tests/helpers/extract-blocks.d.ts +9 -0
  88. package/lib/dist/tests/helpers/extract-blocks.js +56 -0
  89. package/lib/dist/tests/llm-docs-examples.test.d.ts +1 -0
  90. package/lib/dist/tests/llm-docs-examples.test.js +62 -0
  91. package/lib/dist/tests/setup.js +2 -1
  92. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  93. package/llm-docs/.coverage-allowlist.txt +9 -0
  94. package/llm-docs/api/arc.md +48 -0
  95. package/llm-docs/api/axis.md +42 -0
  96. package/llm-docs/api/bezier.md +42 -0
  97. package/llm-docs/api/booleans.md +44 -0
  98. package/llm-docs/api/chamfer.md +40 -0
  99. package/llm-docs/api/circle.md +36 -0
  100. package/llm-docs/api/color.md +34 -0
  101. package/llm-docs/api/connect.md +41 -0
  102. package/llm-docs/api/constraint-qualifiers.md +48 -0
  103. package/llm-docs/api/copy.md +63 -0
  104. package/llm-docs/api/cursor-lines.md +50 -0
  105. package/llm-docs/api/cursor-move.md +61 -0
  106. package/llm-docs/api/cut.md +55 -0
  107. package/llm-docs/api/draft.md +36 -0
  108. package/llm-docs/api/edge-filter.md +57 -0
  109. package/llm-docs/api/ellipse.md +34 -0
  110. package/llm-docs/api/extrude.md +74 -0
  111. package/llm-docs/api/face-filter.md +61 -0
  112. package/llm-docs/api/fillet.md +51 -0
  113. package/llm-docs/api/index.json +139 -0
  114. package/llm-docs/api/line.md +42 -0
  115. package/llm-docs/api/load.md +37 -0
  116. package/llm-docs/api/local.md +38 -0
  117. package/llm-docs/api/loft.md +37 -0
  118. package/llm-docs/api/mirror.md +44 -0
  119. package/llm-docs/api/offset.md +36 -0
  120. package/llm-docs/api/part.md +40 -0
  121. package/llm-docs/api/plane.md +44 -0
  122. package/llm-docs/api/polygon.md +37 -0
  123. package/llm-docs/api/primitive-solids.md +39 -0
  124. package/llm-docs/api/project-intersect.md +48 -0
  125. package/llm-docs/api/rect.md +48 -0
  126. package/llm-docs/api/remove.md +32 -0
  127. package/llm-docs/api/repeat.md +79 -0
  128. package/llm-docs/api/revolve.md +38 -0
  129. package/llm-docs/api/rib.md +40 -0
  130. package/llm-docs/api/rotate.md +37 -0
  131. package/llm-docs/api/select.md +41 -0
  132. package/llm-docs/api/shell.md +41 -0
  133. package/llm-docs/api/sketch.md +76 -0
  134. package/llm-docs/api/slot.md +36 -0
  135. package/llm-docs/api/split-trim.md +42 -0
  136. package/llm-docs/api/sweep.md +43 -0
  137. package/llm-docs/api/tarc.md +45 -0
  138. package/llm-docs/api/tcircle.md +38 -0
  139. package/llm-docs/api/tline.md +42 -0
  140. package/llm-docs/api/translate.md +40 -0
  141. package/llm-docs/api/types/aline.md +35 -0
  142. package/llm-docs/api/types/arc-angles.md +29 -0
  143. package/llm-docs/api/types/arc-points.md +48 -0
  144. package/llm-docs/api/types/axis-like.md +38 -0
  145. package/llm-docs/api/types/axis.md +21 -0
  146. package/llm-docs/api/types/boolean-operation.md +50 -0
  147. package/llm-docs/api/types/circular-repeat-options.md +31 -0
  148. package/llm-docs/api/types/common.md +32 -0
  149. package/llm-docs/api/types/cut.md +125 -0
  150. package/llm-docs/api/types/draft.md +21 -0
  151. package/llm-docs/api/types/extrudable-geometry.md +23 -0
  152. package/llm-docs/api/types/extrude.md +194 -0
  153. package/llm-docs/api/types/geometry.md +51 -0
  154. package/llm-docs/api/types/hline.md +35 -0
  155. package/llm-docs/api/types/linear-repeat-options.md +31 -0
  156. package/llm-docs/api/types/loft.md +154 -0
  157. package/llm-docs/api/types/mirror.md +35 -0
  158. package/llm-docs/api/types/offset.md +31 -0
  159. package/llm-docs/api/types/plane-like.md +35 -0
  160. package/llm-docs/api/types/plane-transform-options.md +29 -0
  161. package/llm-docs/api/types/plane.md +21 -0
  162. package/llm-docs/api/types/point-like.md +22 -0
  163. package/llm-docs/api/types/point2dlike.md +26 -0
  164. package/llm-docs/api/types/polygon.md +46 -0
  165. package/llm-docs/api/types/rect.md +128 -0
  166. package/llm-docs/api/types/revolve.md +102 -0
  167. package/llm-docs/api/types/rib.md +133 -0
  168. package/llm-docs/api/types/scene-object.md +33 -0
  169. package/llm-docs/api/types/select.md +21 -0
  170. package/llm-docs/api/types/shell.md +54 -0
  171. package/llm-docs/api/types/slot.md +43 -0
  172. package/llm-docs/api/types/sweep.md +189 -0
  173. package/llm-docs/api/types/tangent-arc-two-objects.md +46 -0
  174. package/llm-docs/api/types/transformable.md +93 -0
  175. package/llm-docs/api/types/trim.md +27 -0
  176. package/llm-docs/api/types/two-objects-tangent-line.md +46 -0
  177. package/llm-docs/api/types/vertex.md +17 -0
  178. package/llm-docs/api/types/vline.md +35 -0
  179. package/llm-docs/concepts/coordinate-system.md +45 -0
  180. package/llm-docs/concepts/history-and-rollback.md +40 -0
  181. package/llm-docs/concepts/last-selection.md +49 -0
  182. package/llm-docs/concepts/scene-graph.md +37 -0
  183. package/llm-docs/index.json +1750 -0
  184. package/mcp/dist/client.d.ts +64 -0
  185. package/mcp/dist/client.js +248 -0
  186. package/mcp/dist/discovery.d.ts +11 -0
  187. package/mcp/dist/discovery.js +78 -0
  188. package/mcp/dist/docs-index.d.ts +81 -0
  189. package/mcp/dist/docs-index.js +261 -0
  190. package/mcp/dist/resources.d.ts +4 -0
  191. package/mcp/dist/resources.js +115 -0
  192. package/mcp/dist/server.d.ts +12 -0
  193. package/mcp/dist/server.js +489 -0
  194. package/mcp/dist/tools/coordination.d.ts +9 -0
  195. package/mcp/dist/tools/coordination.js +46 -0
  196. package/mcp/dist/tools/docs.d.ts +66 -0
  197. package/mcp/dist/tools/docs.js +122 -0
  198. package/mcp/dist/tools/engine.d.ts +56 -0
  199. package/mcp/dist/tools/engine.js +145 -0
  200. package/mcp/dist/tools/inspection.d.ts +75 -0
  201. package/mcp/dist/tools/inspection.js +121 -0
  202. package/mcp/dist/tools/screenshot.d.ts +63 -0
  203. package/mcp/dist/tools/screenshot.js +263 -0
  204. package/mcp/dist/tools/source.d.ts +84 -0
  205. package/mcp/dist/tools/source.js +434 -0
  206. package/mcp/dist/tools/workspaces.d.ts +13 -0
  207. package/mcp/dist/tools/workspaces.js +33 -0
  208. package/mcp/dist/types.d.ts +18 -0
  209. package/mcp/dist/types.js +11 -0
  210. package/package.json +19 -5
  211. package/server/dist/code-editor.d.ts +36 -0
  212. package/server/dist/code-editor.js +8 -0
  213. package/server/dist/fluidcad-server.d.ts +50 -0
  214. package/server/dist/fluidcad-server.js +153 -1
  215. package/server/dist/global-registry.d.ts +30 -0
  216. package/server/dist/global-registry.js +126 -0
  217. package/server/dist/index.js +171 -26
  218. package/server/dist/instance-file.d.ts +31 -0
  219. package/server/dist/instance-file.js +73 -0
  220. package/server/dist/lint-fluid-js.d.ts +15 -0
  221. package/server/dist/lint-fluid-js.js +271 -0
  222. package/server/dist/routes/editor.d.ts +24 -0
  223. package/server/dist/routes/editor.js +44 -0
  224. package/server/dist/routes/export.d.ts +1 -1
  225. package/server/dist/routes/export.js +45 -8
  226. package/server/dist/routes/health.d.ts +7 -0
  227. package/server/dist/routes/health.js +14 -0
  228. package/server/dist/routes/lint.d.ts +10 -0
  229. package/server/dist/routes/lint.js +28 -0
  230. package/server/dist/routes/render.d.ts +33 -0
  231. package/server/dist/routes/render.js +34 -0
  232. package/server/dist/routes/scene.d.ts +5 -0
  233. package/server/dist/routes/scene.js +48 -0
  234. package/server/dist/routes/screenshot.js +68 -1
  235. package/server/dist/ws-protocol.d.ts +56 -2
  236. package/ui/dist/assets/{index-CFi9p7wR.js → index-BdqrMDRu.js} +21 -21
  237. package/ui/dist/index.html +1 -1
@@ -8,7 +8,12 @@ import { Mesh } from "../oc/mesh.js";
8
8
  import { Profiler } from "../common/profiler.js";
9
9
  import { describeError } from "../common/describe-error.js";
10
10
  export class SceneRenderer {
11
- meshBuilder = new MeshBuilder();
11
+ meshConfig;
12
+ meshBuilder;
13
+ constructor(meshConfig) {
14
+ this.meshConfig = meshConfig;
15
+ this.meshBuilder = new MeshBuilder(meshConfig);
16
+ }
12
17
  render(scene) {
13
18
  const sceneObjects = scene.getAllSceneObjects();
14
19
  console.log("============ Rendering ==============", sceneObjects.length);
@@ -121,7 +126,7 @@ export class SceneRenderer {
121
126
  const compound = ShapeOps.makeCompoundRaw([...targets].map(s => s.getShape()));
122
127
  try {
123
128
  const t0 = performance.now();
124
- Mesh.ensureTriangulated(compound);
129
+ Mesh.ensureTriangulated(compound, this.meshConfig);
125
130
  console.log(`Batched mesh: ${targets.size} shapes in ${(performance.now() - t0).toFixed(1)}ms`);
126
131
  }
127
132
  finally {
@@ -1,5 +1,7 @@
1
1
  import { Scene } from "./rendering/scene.js";
2
2
  import { SceneRenderer } from "./rendering/render.js";
3
+ import type { MeshConfig } from "./oc/mesh.js";
4
+ import type { FluidCADOptions } from "./index.js";
3
5
  import type { ExportOptions } from "./io/file-export.js";
4
6
  import type { ShapeProperties } from "./oc/props.js";
5
7
  import type { FaceProperties } from "./oc/face-props.js";
@@ -10,7 +12,7 @@ declare class SceneManager {
10
12
  currentScene: Scene;
11
13
  currentFile: string;
12
14
  renderer: SceneRenderer;
13
- constructor(rootPath: string);
15
+ constructor(rootPath: string, meshConfig: MeshConfig);
14
16
  setCurrentFile(filePath: string): void;
15
17
  startScene(): Scene;
16
18
  renderScene(scene: Scene): Scene;
@@ -26,7 +28,7 @@ declare class SceneManager {
26
28
  };
27
29
  hitTest(scene: Scene, shapeId: string, rayOrigin: [number, number, number], rayDir: [number, number, number], edgeThreshold: number): HitTestResult;
28
30
  }
29
- export declare function createManager(rootPath: string): SceneManager;
31
+ export declare function createManager(rootPath: string, options?: FluidCADOptions): SceneManager;
30
32
  export declare function getCurrentScene(): Scene;
31
33
  export declare function getCurrentFile(): string;
32
34
  export declare function setCurrentFile(filePath: string): void;
@@ -1,6 +1,7 @@
1
1
  import { Scene } from "./rendering/scene.js";
2
2
  import { SceneRenderer } from "./rendering/render.js";
3
3
  import { SceneCompare } from "./rendering/scene-compare.js";
4
+ import { DEFAULT_MESH_CONFIG } from "./oc/mesh.js";
4
5
  import { FileImport } from "./io/file-import.js";
5
6
  import { FileExport } from "./io/file-export.js";
6
7
  import { ShapeProps } from "./oc/props.js";
@@ -12,9 +13,10 @@ class SceneManager {
12
13
  rootPath;
13
14
  currentScene = new Scene();
14
15
  currentFile = '';
15
- renderer = new SceneRenderer();
16
- constructor(rootPath) {
16
+ renderer;
17
+ constructor(rootPath, meshConfig) {
17
18
  this.rootPath = rootPath;
19
+ this.renderer = new SceneRenderer(meshConfig);
18
20
  }
19
21
  setCurrentFile(filePath) {
20
22
  this.currentFile = filePath;
@@ -100,9 +102,15 @@ class SceneManager {
100
102
  }
101
103
  }
102
104
  let currentManager = null;
103
- export function createManager(rootPath) {
105
+ function resolveMeshConfig(options) {
106
+ return {
107
+ linDefl: options?.mesh?.lineDeflection ?? DEFAULT_MESH_CONFIG.linDefl,
108
+ angDefl: options?.mesh?.angularDeflection ?? DEFAULT_MESH_CONFIG.angDefl,
109
+ };
110
+ }
111
+ export function createManager(rootPath, options) {
104
112
  console.log(`Creating SceneManager with root path: ${rootPath}`);
105
- currentManager = new SceneManager(rootPath);
113
+ currentManager = new SceneManager(rootPath, resolveMeshConfig(options));
106
114
  return currentManager;
107
115
  }
108
116
  export function getCurrentScene() {
@@ -36,6 +36,70 @@ describe("arc", () => {
36
36
  expect(arcEdges.length).toBeGreaterThan(0);
37
37
  });
38
38
  });
39
+ describe("clockwise arc with .cw()", () => {
40
+ it("should create a CW arc from start to end around a center point", () => {
41
+ sketch("xy", () => {
42
+ arc([0, 0], [20, 0]).center([10, 0]).cw();
43
+ line([0, 0]);
44
+ });
45
+ const e = extrude(10);
46
+ render();
47
+ expect(e.getShapes()).toHaveLength(1);
48
+ const solid = e.getShapes()[0];
49
+ const arcEdges = getEdgesByType(solid, "arc");
50
+ expect(arcEdges.length).toBeGreaterThan(0);
51
+ });
52
+ it("should create a valid arc when chained from current position", () => {
53
+ sketch("xy", () => {
54
+ hLine(20);
55
+ arc([0, 0]).center([10, 0]).cw();
56
+ });
57
+ const e = extrude(10);
58
+ render();
59
+ expect(e.getShapes()).toHaveLength(1);
60
+ const solid = e.getShapes()[0];
61
+ const arcEdges = getEdgesByType(solid, "arc");
62
+ expect(arcEdges.length).toBeGreaterThan(0);
63
+ });
64
+ });
65
+ describe("major arc with .major()", () => {
66
+ it("should create a major arc (>180°) from current position", () => {
67
+ sketch("xy", () => {
68
+ arc([30, 0]).radius(20).major();
69
+ line([0, 0]);
70
+ });
71
+ const e = extrude(10);
72
+ render();
73
+ expect(e.getShapes()).toHaveLength(1);
74
+ const solid = e.getShapes()[0];
75
+ const arcEdges = getEdgesByType(solid, "arc");
76
+ expect(arcEdges.length).toBeGreaterThan(0);
77
+ });
78
+ it("should create a major arc with two explicit points", () => {
79
+ sketch("xy", () => {
80
+ arc([0, 0], [30, 0]).radius(20).major();
81
+ line([0, 0]);
82
+ });
83
+ const e = extrude(10);
84
+ render();
85
+ expect(e.getShapes()).toHaveLength(1);
86
+ const solid = e.getShapes()[0];
87
+ const arcEdges = getEdgesByType(solid, "arc");
88
+ expect(arcEdges.length).toBeGreaterThan(0);
89
+ });
90
+ it("should create a CW major arc with negative radius and major", () => {
91
+ sketch("xy", () => {
92
+ arc([30, 0]).radius(-20).major();
93
+ line([0, 0]);
94
+ });
95
+ const e = extrude(10);
96
+ render();
97
+ expect(e.getShapes()).toHaveLength(1);
98
+ const solid = e.getShapes()[0];
99
+ const arcEdges = getEdgesByType(solid, "arc");
100
+ expect(arcEdges.length).toBeGreaterThan(0);
101
+ });
102
+ });
39
103
  describe("combined with lines", () => {
40
104
  it("should create a shape with straight and curved edges", () => {
41
105
  sketch("xy", () => {
@@ -2,8 +2,9 @@ import { describe, it, expect } from "vitest";
2
2
  import { setupOC, render } from "../../setup.js";
3
3
  import sketch from "../../../core/sketch.js";
4
4
  import extrude from "../../../core/extrude.js";
5
- import { move, hMove, back, rect } from "../../../core/2d/index.js";
5
+ import { move, hMove, back, rect, hLine, vLine, tLine } from "../../../core/2d/index.js";
6
6
  import { ShapeOps } from "../../../oc/shape-ops.js";
7
+ import { Edge } from "../../../common/edge.js";
7
8
  describe("back", () => {
8
9
  setupOC();
9
10
  it("should revert cursor to the previous position", () => {
@@ -57,4 +58,19 @@ describe("back", () => {
57
58
  const bbox = ShapeOps.getBoundingBox(e.getShapes()[0]);
58
59
  expect(bbox.minX).toBeCloseTo(40, 0);
59
60
  });
61
+ it("should restore the tangent to its prior value, not just the position", () => {
62
+ let t;
63
+ sketch("xy", () => {
64
+ hLine(10); // tangent (+1, 0), pos (10, 0)
65
+ vLine(10); // tangent (0, +1), pos (10, 10)
66
+ back(); // pos reverts to (10, 0); tangent should revert to (+1, 0)
67
+ t = tLine(5); // with restored tangent, this should go to (15, 0)
68
+ });
69
+ render();
70
+ const edges = t.getOwnShapes().filter((sh) => sh instanceof Edge);
71
+ expect(edges).toHaveLength(1);
72
+ const endPoint = edges[0].getLastVertex().toPoint();
73
+ expect(endPoint.x).toBeCloseTo(15, 1);
74
+ expect(endPoint.y).toBeCloseTo(0, 1);
75
+ });
60
76
  });
@@ -234,4 +234,161 @@ describe("tArc", () => {
234
234
  render();
235
235
  });
236
236
  });
237
+ describe("tangent arc from current position to target line (target)", () => {
238
+ it("should create a tangent arc from a horizontal line ending tangent to a vertical line", () => {
239
+ const s = sketch("xy", () => {
240
+ const v = vLine([200, 200], 100).guide();
241
+ move([0, 0]);
242
+ hLine(100);
243
+ tArc(v);
244
+ });
245
+ render();
246
+ const shapes = s.getShapes();
247
+ // hLine + tArc (guide excluded) = at least 2 edges
248
+ expect(shapes.length).toBeGreaterThanOrEqual(2);
249
+ });
250
+ it("should accept a qualified line target", () => {
251
+ const s = sketch("xy", () => {
252
+ const v = vLine([200, 200], 100).guide();
253
+ move([0, 0]);
254
+ hLine(100);
255
+ tArc(outside(v));
256
+ });
257
+ render();
258
+ const shapes = s.getShapes();
259
+ expect(shapes.length).toBeGreaterThanOrEqual(2);
260
+ });
261
+ it("should allow chaining geometry after the solved arc", () => {
262
+ const s = sketch("xy", () => {
263
+ const v = vLine([200, 200], 100).guide();
264
+ move([0, 0]);
265
+ hLine(100);
266
+ tArc(v);
267
+ vLine(40);
268
+ });
269
+ render();
270
+ const shapes = s.getShapes();
271
+ // chain continues — hLine + tArc + vLine
272
+ expect(shapes.length).toBeGreaterThanOrEqual(3);
273
+ });
274
+ it("default arc curves to the left of the start tangent", () => {
275
+ const s = sketch("xy", () => {
276
+ const h = hLine([-150, 100], 300).guide();
277
+ move([0, 0]);
278
+ vLine(80);
279
+ tArc(h);
280
+ });
281
+ render();
282
+ const arcs = getEdgesByType(s.getShapes(), "arc");
283
+ expect(arcs.length).toBe(1);
284
+ // start tangent is +Y; "left" of +Y is -X. End vertex sits at negative x.
285
+ const endX = arcs[0].getLastVertex().toPoint().x;
286
+ expect(endX).toBeLessThan(0);
287
+ });
288
+ it(".flip() reverses the curve to the right of the start tangent", () => {
289
+ const s = sketch("xy", () => {
290
+ const h = hLine([-150, 100], 300).guide();
291
+ move([0, 0]);
292
+ vLine(80);
293
+ tArc(h).flip();
294
+ });
295
+ render();
296
+ const arcs = getEdgesByType(s.getShapes(), "arc");
297
+ expect(arcs.length).toBe(1);
298
+ const endX = arcs[0].getLastVertex().toPoint().x;
299
+ expect(endX).toBeGreaterThan(0);
300
+ });
301
+ it("should reject circle targets", () => {
302
+ const s = sketch("xy", () => {
303
+ const c = circle([200, 80], 30).guide();
304
+ move([0, 0]);
305
+ hLine(100);
306
+ tArc(c);
307
+ });
308
+ render();
309
+ const children = s.getChildren();
310
+ const arc = children[children.length - 1];
311
+ expect(arc.getError()).toMatch(/only line targets are supported/);
312
+ });
313
+ });
314
+ describe("tangent arc with radius ending at intersection (radius, target)", () => {
315
+ it("should end at the intersection with a line target", () => {
316
+ const s = sketch("xy", () => {
317
+ const h = hLine([-200, 100], 400).guide();
318
+ move([0, 0]);
319
+ vLine(50);
320
+ tArc(60, h);
321
+ });
322
+ render();
323
+ const arcs = getEdgesByType(s.getShapes(), "arc");
324
+ expect(arcs.length).toBe(1);
325
+ // End vertex must lie on the target line y = 100.
326
+ const endY = arcs[0].getLastVertex().toPoint().y;
327
+ expect(endY).toBeCloseTo(100, 6);
328
+ });
329
+ it("should end at the intersection with a circle target", () => {
330
+ const s = sketch("xy", () => {
331
+ // circle(diameter); 80 diameter = 40 radius
332
+ const c = circle([80, 0], 80).guide();
333
+ move([0, 0]);
334
+ hLine(40);
335
+ tArc(50, c);
336
+ });
337
+ render();
338
+ const arcs = getEdgesByType(s.getShapes(), "arc");
339
+ expect(arcs.length).toBe(1);
340
+ // End vertex must lie on the target circle (radius 40 from (80, 0)).
341
+ const end = arcs[0].getLastVertex().toPoint();
342
+ const dist = Math.hypot(end.x - 80, end.y - 0);
343
+ expect(dist).toBeCloseTo(40, 6);
344
+ });
345
+ it("negative radius flips the sweep direction", () => {
346
+ // Same line, same start, same tangent — only the sign of the radius
347
+ // differs. Positive radius (CCW) curves to the left of the tangent;
348
+ // negative radius (CW) curves to the right.
349
+ const sCCW = sketch("xy", () => {
350
+ const h = hLine([-200, 100], 400).guide();
351
+ move([0, 0]);
352
+ vLine(50);
353
+ tArc(60, h);
354
+ });
355
+ render();
356
+ const sCW = sketch("xy", () => {
357
+ const h = hLine([-200, 100], 400).guide();
358
+ move([0, 0]);
359
+ vLine(50);
360
+ tArc(-60, h);
361
+ });
362
+ render();
363
+ const ccwArc = getEdgesByType(sCCW.getShapes(), "arc")[0];
364
+ const cwArc = getEdgesByType(sCW.getShapes(), "arc")[0];
365
+ // T̂ = (0, +1); "left" of the tangent is −x, "right" is +x.
366
+ expect(ccwArc.getLastVertex().toPoint().x).toBeLessThan(0);
367
+ expect(cwArc.getLastVertex().toPoint().x).toBeGreaterThan(0);
368
+ });
369
+ it("should chain geometry after the solved arc", () => {
370
+ const s = sketch("xy", () => {
371
+ const h = hLine([-200, 100], 400).guide();
372
+ move([0, 0]);
373
+ vLine(50);
374
+ tArc(60, h);
375
+ hLine(40);
376
+ });
377
+ render();
378
+ const shapes = s.getShapes();
379
+ expect(shapes.length).toBeGreaterThanOrEqual(3);
380
+ });
381
+ it("should record an error when there is no intersection", () => {
382
+ const s = sketch("xy", () => {
383
+ const h = hLine([-200, 500], 400).guide();
384
+ move([0, 0]);
385
+ vLine(10);
386
+ tArc(20, h);
387
+ });
388
+ render();
389
+ const children = s.getChildren();
390
+ const arc = children[children.length - 1];
391
+ expect(arc.getError()).toMatch(/does not intersect target/);
392
+ });
393
+ });
237
394
  });
@@ -0,0 +1,60 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { setupOC, render } from "../setup.js";
3
+ import sketch from "../../core/sketch.js";
4
+ import extrude from "../../core/extrude.js";
5
+ import axis from "../../core/axis.js";
6
+ import repeat from "../../core/repeat.js";
7
+ import sweep from "../../core/sweep.js";
8
+ import cut from "../../core/cut.js";
9
+ import { rect, circle, vLine, tArc, tLine, slot, move, hMove } from "../../core/2d/index.js";
10
+ import copy from "../../core/copy.js";
11
+ import { countShapes } from "../utils.js";
12
+ // Regression: a circular repeat whose axis is an AxisFromEdge derived from
13
+ // a sketch line, whose targets are cuts on sketch-region slots. Exercises:
14
+ // - LazyMatrix.rotation with an unbuilt AxisObjectBase source
15
+ // - Extrude.getSourceDependencies pulling the containing Sketch into the
16
+ // clone set so Sketch.build's clone-mode path carries shapes for cloned
17
+ // geometry primitives (Slot here) that would otherwise lose their
18
+ // Sketch parent context after clone.
19
+ describe("repeat circular with axis-from-sketch-line and cut-of-slot targets", () => {
20
+ setupOC();
21
+ it("renders without errors and produces the expected cut instances", () => {
22
+ const spine = sketch("front", () => {
23
+ vLine(1.5);
24
+ tArc(-4, 45);
25
+ const topSegment = tLine(1.5);
26
+ return { topSegment };
27
+ }).reusable();
28
+ const profile = sketch("top", () => {
29
+ const innerPipe = circle(1.5);
30
+ const outerPipe = circle(2);
31
+ return { innerPipe, outerPipe };
32
+ });
33
+ const pipe = sweep(spine, profile.regions.outerPipe);
34
+ sketch("top", () => {
35
+ rect(3.5).centered().radius(0.5);
36
+ move([-2.5 / 2, -2.5 / 2]);
37
+ const c = circle(0.5);
38
+ copy("circular", [0, 0], { count: 4, angle: 360 }, c);
39
+ });
40
+ extrude(0.375);
41
+ sketch(pipe.endFaces(), () => {
42
+ circle(4);
43
+ });
44
+ const upperFlange = extrude(-0.625);
45
+ sweep(spine, profile.regions.innerPipe).remove();
46
+ const slots = sketch(upperFlange.endFaces(), () => {
47
+ hMove(3.25 / 2);
48
+ const outerSlot = slot(1, 0.75 / 2);
49
+ const innerSlot = slot(1, 0.45 / 2);
50
+ return { outerSlot, innerSlot };
51
+ });
52
+ const s1 = cut(slots.regions.innerSlot);
53
+ const s2 = cut(0.25, slots.regions.outerSlot);
54
+ const a = axis(spine.regions.topSegment);
55
+ repeat("circular", a, { count: 4, angle: 360 }, s1, s2);
56
+ const scene = render();
57
+ expect(countShapes(scene)).toBeGreaterThan(0);
58
+ expect(scene.getAllSceneObjects().some(o => o.getError())).toBe(false);
59
+ });
60
+ });
@@ -6,6 +6,7 @@ import shell from "../../core/shell.js";
6
6
  import select from "../../core/select.js";
7
7
  import cylinder from "../../core/cylinder.js";
8
8
  import { rect } from "../../core/2d/index.js";
9
+ import { Shell } from "../../features/shell.js";
9
10
  import { countShapes } from "../utils.js";
10
11
  import { ShapeProps } from "../../oc/props.js";
11
12
  import { face } from "../../filters/index.js";
@@ -198,6 +199,41 @@ describe("shell", () => {
198
199
  expect(first.getShapes()[0].isSame(allShapes[0])).toBe(true);
199
200
  });
200
201
  });
202
+ describe("join type", () => {
203
+ const buildShelledBox = (apply) => {
204
+ sketch("xy", () => {
205
+ rect(100, 100);
206
+ });
207
+ extrude(50);
208
+ select(face().onPlane("xy", 50));
209
+ const s = shell(5);
210
+ apply(s);
211
+ const scene = render();
212
+ return scene.getAllSceneObjects()
213
+ .flatMap(o => o.getShapes())
214
+ .find(sh => sh.getType() === "solid");
215
+ };
216
+ it("should produce a valid solid with default join type (arc)", () => {
217
+ const solid = buildShelledBox(() => { });
218
+ expect(solid).toBeDefined();
219
+ expect(ShapeProps.getProperties(solid.getShape()).volumeMm3).toBeGreaterThan(0);
220
+ });
221
+ it("should produce a valid solid with join('intersection')", () => {
222
+ const solid = buildShelledBox(s => s.join('intersection'));
223
+ expect(solid).toBeDefined();
224
+ expect(ShapeProps.getProperties(solid.getShape()).volumeMm3).toBeGreaterThan(0);
225
+ });
226
+ it("should produce a valid solid with join('tangent')", () => {
227
+ const solid = buildShelledBox(s => s.join('tangent'));
228
+ expect(solid).toBeDefined();
229
+ expect(ShapeProps.getProperties(solid.getShape()).volumeMm3).toBeGreaterThan(0);
230
+ });
231
+ it("should treat shells with different join types as not equal", () => {
232
+ const a = new Shell(5);
233
+ const b = new Shell(5).join('intersection');
234
+ expect(a.compareTo(b)).toBe(false);
235
+ });
236
+ });
201
237
  describe("shell with multiple selections", () => {
202
238
  it("should shell a box by removing two faces", () => {
203
239
  sketch("xy", () => {
@@ -1,5 +1,6 @@
1
1
  import { beforeAll } from "vitest";
2
2
  import { init } from "../index.js";
3
3
  beforeAll(async () => {
4
- await init("/tmp/fluidcad-test");
4
+ process.env.FLUIDCAD_WORKSPACE_PATH = "/tmp/fluidcad-test";
5
+ await init();
5
6
  });
@@ -0,0 +1,9 @@
1
+ export type FluidBlock = {
2
+ /** Path relative to the docs root, forward-slash. */
3
+ file: string;
4
+ /** 1-based line number where the block body starts (the line after the opening fence). */
5
+ line: number;
6
+ /** Block body — no fences. */
7
+ block: string;
8
+ };
9
+ export declare function extractFluidJsBlocks(docsRootRel: string): FluidBlock[];
@@ -0,0 +1,56 @@
1
+ import fs from "fs";
2
+ import path from "path";
3
+ const FENCE_RE = /^(\s*)```([A-Za-z0-9_.-]*)\s*$/;
4
+ const RUNNABLE_LANGS = new Set(["fluid.js", "fluid", "fluidjs"]);
5
+ function listMarkdown(dir) {
6
+ const out = [];
7
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
8
+ if (entry.name.startsWith(".") || entry.name.startsWith("_")) {
9
+ continue;
10
+ }
11
+ const full = path.join(dir, entry.name);
12
+ if (entry.isDirectory()) {
13
+ out.push(...listMarkdown(full));
14
+ }
15
+ else if (entry.isFile() && entry.name.endsWith(".md")) {
16
+ out.push(full);
17
+ }
18
+ }
19
+ return out;
20
+ }
21
+ function extractFromFile(file, docsRoot) {
22
+ const text = fs.readFileSync(file, "utf8");
23
+ const lines = text.split("\n");
24
+ const rel = path.relative(docsRoot, file).split(path.sep).join("/");
25
+ const blocks = [];
26
+ let i = 0;
27
+ while (i < lines.length) {
28
+ const openMatch = lines[i].match(FENCE_RE);
29
+ if (!openMatch || !RUNNABLE_LANGS.has(openMatch[2])) {
30
+ i++;
31
+ continue;
32
+ }
33
+ const startLine = i + 2;
34
+ const body = [];
35
+ i++;
36
+ while (i < lines.length) {
37
+ const closeMatch = lines[i].match(FENCE_RE);
38
+ if (closeMatch && closeMatch[2] === "") {
39
+ break;
40
+ }
41
+ body.push(lines[i]);
42
+ i++;
43
+ }
44
+ blocks.push({ file: rel, line: startLine, block: body.join("\n") });
45
+ i++;
46
+ }
47
+ return blocks;
48
+ }
49
+ export function extractFluidJsBlocks(docsRootRel) {
50
+ const docsRoot = path.resolve(docsRootRel);
51
+ const blocks = [];
52
+ for (const file of listMarkdown(docsRoot).sort()) {
53
+ blocks.push(...extractFromFile(file, docsRoot));
54
+ }
55
+ return blocks;
56
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,62 @@
1
+ // Runs every fluid.js fenced block in llm-docs/**/*.md against the real
2
+ // FluidCAD runtime and asserts the scene compiles and produces ≥1 rendered
3
+ // object. Catches three classes of doc rot:
4
+ //
5
+ // 1. Syntax errors in examples (renamed args, dropped commas, etc.).
6
+ // 2. API drift — an example calls a method that no longer exists.
7
+ // 3. Geometry-empty examples — the snippet runs but yields nothing visible,
8
+ // which usually means the user-facing intent broke.
9
+ //
10
+ // The runner reuses the global OCC init from global-setup.ts and the
11
+ // startScene/render plumbing from setup.ts, so it sits on the same OCC
12
+ // initialisation the rest of the test suite uses.
13
+ import { describe, it, expect } from "vitest";
14
+ import { setupOC, render } from "./setup.js";
15
+ import { extractFluidJsBlocks } from "./helpers/extract-blocks.js";
16
+ import * as core from "../core/index.js";
17
+ import * as filters from "../filters/index.js";
18
+ import * as constraints from "../features/2d/constraints/geometry-qualifier.js";
19
+ import * as math from "../math/index.js";
20
+ import { countShapes } from "./utils.js";
21
+ function buildGlobals() {
22
+ return {
23
+ ...core,
24
+ ...filters,
25
+ ...constraints,
26
+ ...math,
27
+ };
28
+ }
29
+ // Doc examples often show the `import { foo } from "fluidcad/..."` line a
30
+ // real user would write at the top of a .fluid.js file. The runner injects
31
+ // every public export as a Function() parameter, so those imports are dead
32
+ // weight here — and `new Function` can't parse them anyway. Strip them out
33
+ // before executing.
34
+ const IMPORT_LINE_RE = /^\s*import\s[\s\S]*?from\s+['"][^'"]+['"]\s*;?\s*$/gm;
35
+ function stripImports(block) {
36
+ return block.replace(IMPORT_LINE_RE, "");
37
+ }
38
+ function runBlock(block, file, line) {
39
+ const globals = buildGlobals();
40
+ const paramNames = Object.keys(globals);
41
+ const paramValues = paramNames.map((n) => globals[n]);
42
+ const wrapped = `"use strict";\n${stripImports(block)}\n//# sourceURL=llm-docs/${file}:${line}`;
43
+ const fn = new Function(...paramNames, wrapped);
44
+ fn(...paramValues);
45
+ }
46
+ const blocks = extractFluidJsBlocks("llm-docs");
47
+ describe("llm-docs fluid.js examples", () => {
48
+ setupOC();
49
+ if (blocks.length === 0) {
50
+ it("found at least one fluid.js block", () => {
51
+ expect(blocks.length).toBeGreaterThan(0);
52
+ });
53
+ return;
54
+ }
55
+ for (const { file, line, block } of blocks) {
56
+ it(`${file}:${line} compiles and produces ≥1 rendered object`, () => {
57
+ runBlock(block, file, line);
58
+ const scene = render();
59
+ expect(countShapes(scene)).toBeGreaterThan(0);
60
+ });
61
+ }
62
+ });
@@ -1,7 +1,8 @@
1
1
  import { beforeEach } from "vitest";
2
2
  import { getSceneManager, getCurrentScene } from "../scene-manager.js";
3
3
  import { SceneRenderer } from "../rendering/render.js";
4
- const renderer = new SceneRenderer();
4
+ import { DEFAULT_MESH_CONFIG } from "../oc/mesh.js";
5
+ const renderer = new SceneRenderer(DEFAULT_MESH_CONFIG);
5
6
  export function setupOC() {
6
7
  beforeEach(() => {
7
8
  getSceneManager().startScene();