fluidcad 0.0.32 → 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 (279) 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 +4 -1
  7. package/lib/dist/common/scene-object.js +9 -2
  8. package/lib/dist/common/solid.d.ts +4 -1
  9. package/lib/dist/common/solid.js +13 -0
  10. package/lib/dist/core/2d/tarc.d.ts +20 -2
  11. package/lib/dist/core/2d/tarc.js +24 -0
  12. package/lib/dist/core/index.d.ts +2 -1
  13. package/lib/dist/core/index.js +1 -0
  14. package/lib/dist/core/interfaces.d.ts +107 -2
  15. package/lib/dist/core/load.d.ts +2 -2
  16. package/lib/dist/core/repeat.js +62 -46
  17. package/lib/dist/core/rib.d.ts +18 -0
  18. package/lib/dist/core/rib.js +37 -0
  19. package/lib/dist/features/2d/arc.d.ts +8 -2
  20. package/lib/dist/features/2d/arc.js +94 -17
  21. package/lib/dist/features/2d/back.js +3 -2
  22. package/lib/dist/features/2d/sketch.d.ts +4 -0
  23. package/lib/dist/features/2d/sketch.js +21 -0
  24. package/lib/dist/features/2d/tarc-constrained.d.ts +2 -0
  25. package/lib/dist/features/2d/tarc-constrained.js +8 -0
  26. package/lib/dist/features/2d/tarc-radius-to-object.d.ts +16 -0
  27. package/lib/dist/features/2d/tarc-radius-to-object.js +58 -0
  28. package/lib/dist/features/2d/tarc-to-object.d.ts +18 -0
  29. package/lib/dist/features/2d/tarc-to-object.js +66 -0
  30. package/lib/dist/features/2d/tarc-to-point-tangent.d.ts +2 -0
  31. package/lib/dist/features/2d/tarc-to-point-tangent.js +3 -0
  32. package/lib/dist/features/2d/tarc-to-point.d.ts +2 -0
  33. package/lib/dist/features/2d/tarc-to-point.js +3 -0
  34. package/lib/dist/features/2d/tarc-with-tangent.d.ts +2 -0
  35. package/lib/dist/features/2d/tarc-with-tangent.js +3 -0
  36. package/lib/dist/features/2d/tarc.d.ts +2 -0
  37. package/lib/dist/features/2d/tarc.js +3 -0
  38. package/lib/dist/features/extrude-base.d.ts +9 -0
  39. package/lib/dist/features/extrude-base.js +22 -0
  40. package/lib/dist/features/extrude-to-face.js +1 -5
  41. package/lib/dist/features/extrude-two-distances.js +1 -2
  42. package/lib/dist/features/extrude.js +1 -2
  43. package/lib/dist/features/load.d.ts +6 -0
  44. package/lib/dist/features/load.js +53 -1
  45. package/lib/dist/features/mirror-feature.d.ts +3 -2
  46. package/lib/dist/features/mirror-feature.js +1 -1
  47. package/lib/dist/features/repeat-circular.d.ts +3 -3
  48. package/lib/dist/features/repeat-circular.js +8 -1
  49. package/lib/dist/features/repeat-linear.d.ts +4 -2
  50. package/lib/dist/features/repeat-linear.js +10 -1
  51. package/lib/dist/features/repeat-matrix.d.ts +3 -1
  52. package/lib/dist/features/repeat-matrix.js +7 -2
  53. package/lib/dist/features/rib.d.ts +31 -0
  54. package/lib/dist/features/rib.js +321 -0
  55. package/lib/dist/features/select.d.ts +1 -0
  56. package/lib/dist/features/select.js +81 -10
  57. package/lib/dist/features/shell.d.ts +4 -1
  58. package/lib/dist/features/shell.js +14 -3
  59. package/lib/dist/filters/edge/belongs-to-face.d.ts +12 -9
  60. package/lib/dist/filters/edge/belongs-to-face.js +64 -15
  61. package/lib/dist/filters/filter-builder-base.d.ts +25 -0
  62. package/lib/dist/filters/filter-builder-base.js +47 -0
  63. package/lib/dist/filters/filter.js +39 -14
  64. package/lib/dist/filters/from-object.d.ts +4 -0
  65. package/lib/dist/filters/from-object.js +10 -0
  66. package/lib/dist/helpers/clone-transform.d.ts +2 -1
  67. package/lib/dist/helpers/scene-helpers.d.ts +1 -1
  68. package/lib/dist/helpers/scene-helpers.js +146 -12
  69. package/lib/dist/index.d.ts +7 -1
  70. package/lib/dist/index.js +3 -3
  71. package/lib/dist/io/file-import.d.ts +5 -1
  72. package/lib/dist/io/file-import.js +29 -18
  73. package/lib/dist/math/lazy-matrix.d.ts +31 -0
  74. package/lib/dist/math/lazy-matrix.js +66 -0
  75. package/lib/dist/oc/color-transfer.d.ts +19 -8
  76. package/lib/dist/oc/color-transfer.js +70 -12
  77. package/lib/dist/oc/constraints/constraint-solver-adaptor.d.ts +5 -0
  78. package/lib/dist/oc/constraints/constraint-solver-adaptor.js +16 -0
  79. package/lib/dist/oc/constraints/constraint-solver.d.ts +4 -0
  80. package/lib/dist/oc/constraints/curve/curve-constraint-solver.d.ts +4 -0
  81. package/lib/dist/oc/constraints/curve/curve-constraint-solver.js +3 -0
  82. package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.d.ts +6 -1
  83. package/lib/dist/oc/constraints/geometric/geometric-constraint-solver.js +4 -0
  84. package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.d.ts +8 -0
  85. package/lib/dist/oc/constraints/geometric/tangent-arc-from-point-tangent.js +111 -0
  86. package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.d.ts +8 -0
  87. package/lib/dist/oc/constraints/geometric/tangent-arc-radius-to-object.js +161 -0
  88. package/lib/dist/oc/extrude-ops.d.ts +2 -1
  89. package/lib/dist/oc/extrude-ops.js +51 -2
  90. package/lib/dist/oc/mesh.d.ts +9 -4
  91. package/lib/dist/oc/mesh.js +14 -13
  92. package/lib/dist/oc/rib-ops.d.ts +35 -0
  93. package/lib/dist/oc/rib-ops.js +619 -0
  94. package/lib/dist/oc/shell-ops.d.ts +2 -1
  95. package/lib/dist/oc/shell-ops.js +5 -2
  96. package/lib/dist/oc/topology-index.d.ts +6 -0
  97. package/lib/dist/oc/topology-index.js +36 -0
  98. package/lib/dist/rendering/mesh-builder.d.ts +3 -0
  99. package/lib/dist/rendering/mesh-builder.js +8 -4
  100. package/lib/dist/rendering/render-edge.d.ts +2 -1
  101. package/lib/dist/rendering/render-edge.js +2 -2
  102. package/lib/dist/rendering/render-face.d.ts +2 -1
  103. package/lib/dist/rendering/render-face.js +2 -2
  104. package/lib/dist/rendering/render-solid.d.ts +2 -1
  105. package/lib/dist/rendering/render-solid.js +3 -5
  106. package/lib/dist/rendering/render-wire.d.ts +2 -1
  107. package/lib/dist/rendering/render-wire.js +2 -2
  108. package/lib/dist/rendering/render.d.ts +4 -0
  109. package/lib/dist/rendering/render.js +50 -2
  110. package/lib/dist/rendering/scene-compare.js +3 -0
  111. package/lib/dist/rendering/scene.d.ts +1 -0
  112. package/lib/dist/rendering/scene.js +4 -0
  113. package/lib/dist/scene-manager.d.ts +4 -2
  114. package/lib/dist/scene-manager.js +12 -4
  115. package/lib/dist/tests/features/2d/arc.test.js +64 -0
  116. package/lib/dist/tests/features/2d/back.test.js +17 -1
  117. package/lib/dist/tests/features/2d/tarc.test.js +157 -0
  118. package/lib/dist/tests/features/color-lineage.test.js +18 -0
  119. package/lib/dist/tests/features/filter-positional.test.d.ts +1 -0
  120. package/lib/dist/tests/features/filter-positional.test.js +129 -0
  121. package/lib/dist/tests/features/repeat-user-repro.test.d.ts +1 -0
  122. package/lib/dist/tests/features/repeat-user-repro.test.js +60 -0
  123. package/lib/dist/tests/features/rib.test.d.ts +1 -0
  124. package/lib/dist/tests/features/rib.test.js +598 -0
  125. package/lib/dist/tests/features/shell.test.js +36 -0
  126. package/lib/dist/tests/global-setup.js +2 -1
  127. package/lib/dist/tests/helpers/extract-blocks.d.ts +9 -0
  128. package/lib/dist/tests/helpers/extract-blocks.js +56 -0
  129. package/lib/dist/tests/llm-docs-examples.test.d.ts +1 -0
  130. package/lib/dist/tests/llm-docs-examples.test.js +62 -0
  131. package/lib/dist/tests/scene-compare.test.d.ts +1 -0
  132. package/lib/dist/tests/scene-compare.test.js +77 -0
  133. package/lib/dist/tests/setup.js +2 -1
  134. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  135. package/llm-docs/.coverage-allowlist.txt +9 -0
  136. package/llm-docs/api/arc.md +48 -0
  137. package/llm-docs/api/axis.md +42 -0
  138. package/llm-docs/api/bezier.md +42 -0
  139. package/llm-docs/api/booleans.md +44 -0
  140. package/llm-docs/api/chamfer.md +40 -0
  141. package/llm-docs/api/circle.md +36 -0
  142. package/llm-docs/api/color.md +34 -0
  143. package/llm-docs/api/connect.md +41 -0
  144. package/llm-docs/api/constraint-qualifiers.md +48 -0
  145. package/llm-docs/api/copy.md +63 -0
  146. package/llm-docs/api/cursor-lines.md +50 -0
  147. package/llm-docs/api/cursor-move.md +61 -0
  148. package/llm-docs/api/cut.md +55 -0
  149. package/llm-docs/api/draft.md +36 -0
  150. package/llm-docs/api/edge-filter.md +57 -0
  151. package/llm-docs/api/ellipse.md +34 -0
  152. package/llm-docs/api/extrude.md +74 -0
  153. package/llm-docs/api/face-filter.md +61 -0
  154. package/llm-docs/api/fillet.md +51 -0
  155. package/llm-docs/api/index.json +139 -0
  156. package/llm-docs/api/line.md +42 -0
  157. package/llm-docs/api/load.md +37 -0
  158. package/llm-docs/api/local.md +38 -0
  159. package/llm-docs/api/loft.md +37 -0
  160. package/llm-docs/api/mirror.md +44 -0
  161. package/llm-docs/api/offset.md +36 -0
  162. package/llm-docs/api/part.md +40 -0
  163. package/llm-docs/api/plane.md +44 -0
  164. package/llm-docs/api/polygon.md +37 -0
  165. package/llm-docs/api/primitive-solids.md +39 -0
  166. package/llm-docs/api/project-intersect.md +48 -0
  167. package/llm-docs/api/rect.md +48 -0
  168. package/llm-docs/api/remove.md +32 -0
  169. package/llm-docs/api/repeat.md +79 -0
  170. package/llm-docs/api/revolve.md +38 -0
  171. package/llm-docs/api/rib.md +40 -0
  172. package/llm-docs/api/rotate.md +37 -0
  173. package/llm-docs/api/select.md +41 -0
  174. package/llm-docs/api/shell.md +41 -0
  175. package/llm-docs/api/sketch.md +76 -0
  176. package/llm-docs/api/slot.md +36 -0
  177. package/llm-docs/api/split-trim.md +42 -0
  178. package/llm-docs/api/sweep.md +43 -0
  179. package/llm-docs/api/tarc.md +45 -0
  180. package/llm-docs/api/tcircle.md +38 -0
  181. package/llm-docs/api/tline.md +42 -0
  182. package/llm-docs/api/translate.md +40 -0
  183. package/llm-docs/api/types/aline.md +35 -0
  184. package/llm-docs/api/types/arc-angles.md +29 -0
  185. package/llm-docs/api/types/arc-points.md +48 -0
  186. package/llm-docs/api/types/axis-like.md +38 -0
  187. package/llm-docs/api/types/axis.md +21 -0
  188. package/llm-docs/api/types/boolean-operation.md +50 -0
  189. package/llm-docs/api/types/circular-repeat-options.md +31 -0
  190. package/llm-docs/api/types/common.md +32 -0
  191. package/llm-docs/api/types/cut.md +125 -0
  192. package/llm-docs/api/types/draft.md +21 -0
  193. package/llm-docs/api/types/extrudable-geometry.md +23 -0
  194. package/llm-docs/api/types/extrude.md +194 -0
  195. package/llm-docs/api/types/geometry.md +51 -0
  196. package/llm-docs/api/types/hline.md +35 -0
  197. package/llm-docs/api/types/linear-repeat-options.md +31 -0
  198. package/llm-docs/api/types/loft.md +154 -0
  199. package/llm-docs/api/types/mirror.md +35 -0
  200. package/llm-docs/api/types/offset.md +31 -0
  201. package/llm-docs/api/types/plane-like.md +35 -0
  202. package/llm-docs/api/types/plane-transform-options.md +29 -0
  203. package/llm-docs/api/types/plane.md +21 -0
  204. package/llm-docs/api/types/point-like.md +22 -0
  205. package/llm-docs/api/types/point2dlike.md +26 -0
  206. package/llm-docs/api/types/polygon.md +46 -0
  207. package/llm-docs/api/types/rect.md +128 -0
  208. package/llm-docs/api/types/revolve.md +102 -0
  209. package/llm-docs/api/types/rib.md +133 -0
  210. package/llm-docs/api/types/scene-object.md +33 -0
  211. package/llm-docs/api/types/select.md +21 -0
  212. package/llm-docs/api/types/shell.md +54 -0
  213. package/llm-docs/api/types/slot.md +43 -0
  214. package/llm-docs/api/types/sweep.md +189 -0
  215. package/llm-docs/api/types/tangent-arc-two-objects.md +46 -0
  216. package/llm-docs/api/types/transformable.md +93 -0
  217. package/llm-docs/api/types/trim.md +27 -0
  218. package/llm-docs/api/types/two-objects-tangent-line.md +46 -0
  219. package/llm-docs/api/types/vertex.md +17 -0
  220. package/llm-docs/api/types/vline.md +35 -0
  221. package/llm-docs/concepts/coordinate-system.md +45 -0
  222. package/llm-docs/concepts/history-and-rollback.md +40 -0
  223. package/llm-docs/concepts/last-selection.md +49 -0
  224. package/llm-docs/concepts/scene-graph.md +37 -0
  225. package/llm-docs/index.json +1750 -0
  226. package/mcp/dist/client.d.ts +64 -0
  227. package/mcp/dist/client.js +248 -0
  228. package/mcp/dist/discovery.d.ts +11 -0
  229. package/mcp/dist/discovery.js +78 -0
  230. package/mcp/dist/docs-index.d.ts +81 -0
  231. package/mcp/dist/docs-index.js +261 -0
  232. package/mcp/dist/resources.d.ts +4 -0
  233. package/mcp/dist/resources.js +115 -0
  234. package/mcp/dist/server.d.ts +12 -0
  235. package/mcp/dist/server.js +489 -0
  236. package/mcp/dist/tools/coordination.d.ts +9 -0
  237. package/mcp/dist/tools/coordination.js +46 -0
  238. package/mcp/dist/tools/docs.d.ts +66 -0
  239. package/mcp/dist/tools/docs.js +122 -0
  240. package/mcp/dist/tools/engine.d.ts +56 -0
  241. package/mcp/dist/tools/engine.js +145 -0
  242. package/mcp/dist/tools/inspection.d.ts +75 -0
  243. package/mcp/dist/tools/inspection.js +121 -0
  244. package/mcp/dist/tools/screenshot.d.ts +63 -0
  245. package/mcp/dist/tools/screenshot.js +263 -0
  246. package/mcp/dist/tools/source.d.ts +84 -0
  247. package/mcp/dist/tools/source.js +434 -0
  248. package/mcp/dist/tools/workspaces.d.ts +13 -0
  249. package/mcp/dist/tools/workspaces.js +33 -0
  250. package/mcp/dist/types.d.ts +18 -0
  251. package/mcp/dist/types.js +11 -0
  252. package/package.json +19 -5
  253. package/server/dist/code-editor.d.ts +36 -0
  254. package/server/dist/code-editor.js +8 -0
  255. package/server/dist/fluidcad-server.d.ts +50 -0
  256. package/server/dist/fluidcad-server.js +153 -1
  257. package/server/dist/global-registry.d.ts +30 -0
  258. package/server/dist/global-registry.js +126 -0
  259. package/server/dist/index.js +171 -26
  260. package/server/dist/instance-file.d.ts +31 -0
  261. package/server/dist/instance-file.js +73 -0
  262. package/server/dist/lint-fluid-js.d.ts +15 -0
  263. package/server/dist/lint-fluid-js.js +271 -0
  264. package/server/dist/routes/editor.d.ts +24 -0
  265. package/server/dist/routes/editor.js +44 -0
  266. package/server/dist/routes/export.d.ts +1 -1
  267. package/server/dist/routes/export.js +45 -8
  268. package/server/dist/routes/health.d.ts +7 -0
  269. package/server/dist/routes/health.js +14 -0
  270. package/server/dist/routes/lint.d.ts +10 -0
  271. package/server/dist/routes/lint.js +28 -0
  272. package/server/dist/routes/render.d.ts +33 -0
  273. package/server/dist/routes/render.js +34 -0
  274. package/server/dist/routes/scene.d.ts +5 -0
  275. package/server/dist/routes/scene.js +48 -0
  276. package/server/dist/routes/screenshot.js +68 -1
  277. package/server/dist/ws-protocol.d.ts +56 -2
  278. package/ui/dist/assets/{index-DMw0OYCF.js → index-BdqrMDRu.js} +30 -30
  279. package/ui/dist/index.html +1 -1
@@ -1,16 +1,53 @@
1
+ import { Face } from "../../common/shapes.js";
2
+ import { Explorer } from "../../oc/explorer.js";
3
+ import { TopologyIndex } from "../../oc/topology-index.js";
1
4
  import { FilterBase } from "../filter-base.js";
2
- export class BelongsToFaceFilter extends FilterBase {
5
+ class BelongsToFaceFilterBase extends FilterBase {
3
6
  faceFilterBuilders;
7
+ scopeSolids = [];
4
8
  scopeFaces = [];
9
+ faceByHash = new Map();
5
10
  constructor(faceFilterBuilders) {
6
11
  super();
7
12
  this.faceFilterBuilders = faceFilterBuilders;
8
13
  }
9
- setScopeFaces(faces) {
10
- this.scopeFaces = faces;
14
+ setScopeIndex(solids, extraFaces, faceByHash) {
15
+ this.scopeSolids = solids;
16
+ this.scopeFaces = extraFaces;
17
+ this.faceByHash = faceByHash;
11
18
  }
19
+ findContainingFaces(edge) {
20
+ const edgeShape = edge.getShape();
21
+ const seen = new Set();
22
+ const result = [];
23
+ for (const solid of this.scopeSolids) {
24
+ const index = solid.getEdgeToFacesIndex();
25
+ const rawFaces = TopologyIndex.seekShapes(index, edgeShape);
26
+ for (const raw of rawFaces) {
27
+ const wrapper = resolveFaceWrapper(raw, this.faceByHash);
28
+ if (wrapper && !seen.has(wrapper)) {
29
+ seen.add(wrapper);
30
+ result.push(wrapper);
31
+ }
32
+ }
33
+ }
34
+ if (this.scopeFaces.length > 0) {
35
+ for (const face of this.scopeFaces) {
36
+ if (seen.has(face)) {
37
+ continue;
38
+ }
39
+ if (face.hasEdge(edgeShape) !== null) {
40
+ seen.add(face);
41
+ result.push(face);
42
+ }
43
+ }
44
+ }
45
+ return result;
46
+ }
47
+ }
48
+ export class BelongsToFaceFilter extends BelongsToFaceFilterBase {
12
49
  match(shape) {
13
- const containingFaces = this.scopeFaces.filter(face => face.hasEdge(shape.getShape()) !== null);
50
+ const containingFaces = this.findContainingFaces(shape);
14
51
  return this.faceFilterBuilders.every(builder => {
15
52
  const filters = builder.getFilters();
16
53
  return containingFaces.some(face => filters.every(f => f.match(face)));
@@ -32,18 +69,9 @@ export class BelongsToFaceFilter extends FilterBase {
32
69
  return new BelongsToFaceFilter(transformed);
33
70
  }
34
71
  }
35
- export class NotBelongsToFaceFilter extends FilterBase {
36
- faceFilterBuilders;
37
- scopeFaces = [];
38
- constructor(faceFilterBuilders) {
39
- super();
40
- this.faceFilterBuilders = faceFilterBuilders;
41
- }
42
- setScopeFaces(faces) {
43
- this.scopeFaces = faces;
44
- }
72
+ export class NotBelongsToFaceFilter extends BelongsToFaceFilterBase {
45
73
  match(shape) {
46
- const containingFaces = this.scopeFaces.filter(face => face.hasEdge(shape.getShape()) !== null);
74
+ const containingFaces = this.findContainingFaces(shape);
47
75
  return !this.faceFilterBuilders.every(builder => {
48
76
  const filters = builder.getFilters();
49
77
  return containingFaces.some(face => filters.every(f => f.match(face)));
@@ -65,3 +93,24 @@ export class NotBelongsToFaceFilter extends FilterBase {
65
93
  return new NotBelongsToFaceFilter(transformed);
66
94
  }
67
95
  }
96
+ function resolveFaceWrapper(rawFace, faceByHash) {
97
+ const hash = rawFace.HashCode(2147483647);
98
+ const bucket = faceByHash.get(hash);
99
+ if (bucket) {
100
+ for (const candidate of bucket) {
101
+ if (candidate.getShape().IsSame(rawFace)) {
102
+ return candidate;
103
+ }
104
+ }
105
+ }
106
+ // Not in scope (e.g. neighbor face from another part / out-of-scope solid).
107
+ // Wrap on the fly so the face filters can still evaluate it.
108
+ const wrapped = Face.fromTopoDSFace(Explorer.toFace(rawFace));
109
+ if (!bucket) {
110
+ faceByHash.set(hash, [wrapped]);
111
+ }
112
+ else {
113
+ bucket.push(wrapped);
114
+ }
115
+ return wrapped;
116
+ }
@@ -1,12 +1,37 @@
1
1
  import { Shape } from "../common/shapes.js";
2
2
  import { FilterBase } from "./filter-base.js";
3
+ export type IndexSelector = {
4
+ type: 'first';
5
+ } | {
6
+ type: 'last';
7
+ } | {
8
+ type: 'at';
9
+ index: number;
10
+ };
3
11
  export declare class FilterBuilderBase<TShape extends Shape = Shape> {
4
12
  protected filters: FilterBase<TShape>[];
5
13
  protected _withTangents: boolean;
14
+ protected _indexSelector?: IndexSelector;
6
15
  filter(filter: FilterBase<TShape>): this;
7
16
  /**
8
17
  * Expands the selection to include all shapes transitively connected
9
18
  * by tangency (G1 continuity) to the initially matched shapes.
10
19
  */
11
20
  withTangents(): this;
21
+ /**
22
+ * Selects the first matching shape (in iteration order). If called multiple
23
+ * times on the same builder, the last positional call wins.
24
+ */
25
+ first(): this;
26
+ /**
27
+ * Selects the last matching shape (in iteration order). If called multiple
28
+ * times on the same builder, the last positional call wins.
29
+ */
30
+ last(): this;
31
+ /**
32
+ * Selects the matching shape at the given zero-based index.
33
+ * Out-of-range indices yield no match. Negative indices are not supported;
34
+ * use {@link last} for the final element.
35
+ */
36
+ at(index: number): this;
12
37
  }
@@ -1,6 +1,7 @@
1
1
  export class FilterBuilderBase {
2
2
  filters = [];
3
3
  _withTangents = false;
4
+ _indexSelector;
4
5
  filter(filter) {
5
6
  this.filters.push(filter);
6
7
  return this;
@@ -13,6 +14,34 @@ export class FilterBuilderBase {
13
14
  this._withTangents = true;
14
15
  return this;
15
16
  }
17
+ /**
18
+ * Selects the first matching shape (in iteration order). If called multiple
19
+ * times on the same builder, the last positional call wins.
20
+ */
21
+ first() {
22
+ this._indexSelector = { type: 'first' };
23
+ return this;
24
+ }
25
+ /**
26
+ * Selects the last matching shape (in iteration order). If called multiple
27
+ * times on the same builder, the last positional call wins.
28
+ */
29
+ last() {
30
+ this._indexSelector = { type: 'last' };
31
+ return this;
32
+ }
33
+ /**
34
+ * Selects the matching shape at the given zero-based index.
35
+ * Out-of-range indices yield no match. Negative indices are not supported;
36
+ * use {@link last} for the final element.
37
+ */
38
+ at(index) {
39
+ if (!Number.isInteger(index) || index < 0) {
40
+ throw new Error(`at(index): index must be a non-negative integer (got ${index}). Use last() for the final element.`);
41
+ }
42
+ this._indexSelector = { type: 'at', index };
43
+ return this;
44
+ }
16
45
  /**
17
46
  * @internal
18
47
  */
@@ -22,6 +51,12 @@ export class FilterBuilderBase {
22
51
  /**
23
52
  * @internal
24
53
  */
54
+ getIndexSelector() {
55
+ return this._indexSelector;
56
+ }
57
+ /**
58
+ * @internal
59
+ */
25
60
  getFilters() {
26
61
  return this.filters;
27
62
  }
@@ -34,6 +69,9 @@ export class FilterBuilderBase {
34
69
  transformedBuilder.filter(filter.transform(matrix));
35
70
  }
36
71
  transformedBuilder._withTangents = this._withTangents;
72
+ // Index is invariant under Matrix4: the selector is re-applied against
73
+ // the transformed shape's own ordered match list at evaluation time.
74
+ transformedBuilder._indexSelector = this._indexSelector;
37
75
  return transformedBuilder;
38
76
  }
39
77
  /**
@@ -45,6 +83,7 @@ export class FilterBuilderBase {
45
83
  remappedBuilder.filter(filter.remap(remap));
46
84
  }
47
85
  remappedBuilder._withTangents = this._withTangents;
86
+ remappedBuilder._indexSelector = this._indexSelector;
48
87
  return remappedBuilder;
49
88
  }
50
89
  /**
@@ -54,6 +93,14 @@ export class FilterBuilderBase {
54
93
  if (this._withTangents !== other._withTangents) {
55
94
  return false;
56
95
  }
96
+ const a = this._indexSelector;
97
+ const b = other._indexSelector;
98
+ if ((a?.type) !== (b?.type)) {
99
+ return false;
100
+ }
101
+ if (a?.type === 'at' && b?.type === 'at' && a.index !== b.index) {
102
+ return false;
103
+ }
57
104
  if (this.filters.length !== other.filters.length) {
58
105
  return false;
59
106
  }
@@ -10,30 +10,55 @@ export class ShapeFilter {
10
10
  if (!this.builders?.length) {
11
11
  return this.shapes;
12
12
  }
13
- const result = [];
14
- for (const shape of this.shapes) {
15
- for (const filter of this.builders) {
16
- const filters = filter.getFilters();
17
- if (filters.every(f => {
13
+ const result = new Set();
14
+ for (const builder of this.builders) {
15
+ const filters = builder.getFilters();
16
+ // Per-builder ordered match list — preserves input (OCC iteration) order
17
+ // so positional selectors (.first/.last/.at) are deterministic.
18
+ const matched = [];
19
+ for (const shape of this.shapes) {
20
+ let ok = true;
21
+ for (const f of filters) {
18
22
  try {
19
- return f.match(shape);
23
+ if (!f.match(shape)) {
24
+ ok = false;
25
+ break;
26
+ }
20
27
  }
21
28
  catch (e) {
22
29
  console.error('Error applying filter:', e, f);
23
- return false;
24
- }
25
- })) {
26
- if (!result.includes(shape)) {
27
- result.push(shape);
30
+ ok = false;
31
+ break;
28
32
  }
29
33
  }
34
+ if (ok) {
35
+ matched.push(shape);
36
+ }
37
+ }
38
+ const sel = builder.getIndexSelector();
39
+ let selected;
40
+ if (!sel) {
41
+ selected = matched;
42
+ }
43
+ else if (sel.type === 'first') {
44
+ selected = matched.length > 0 ? [matched[0]] : [];
45
+ }
46
+ else if (sel.type === 'last') {
47
+ selected = matched.length > 0 ? [matched[matched.length - 1]] : [];
48
+ }
49
+ else {
50
+ selected = sel.index < matched.length ? [matched[sel.index]] : [];
51
+ }
52
+ for (const s of selected) {
53
+ result.add(s);
30
54
  }
31
55
  }
56
+ const resultArr = [...result];
32
57
  // Tangent expansion: if any builder requests it, expand result set via BFS
33
58
  const needsExpansion = this.builders.some(b => b.hasTangentExpansion());
34
- if (needsExpansion && result.length > 0) {
35
- return TangentExpander.expand(result, this.shapes);
59
+ if (needsExpansion && resultArr.length > 0) {
60
+ return TangentExpander.expand(resultArr, this.shapes);
36
61
  }
37
- return result;
62
+ return resultArr;
38
63
  }
39
64
  }
@@ -1,3 +1,4 @@
1
+ import type { TopTools_MapOfShape } from "occjs-wrapper";
1
2
  import { Matrix4 } from "../math/matrix4.js";
2
3
  import { Shape } from "../common/shapes.js";
3
4
  import { ShapeType } from "../common/shape-type.js";
@@ -6,8 +7,11 @@ import { FilterBase } from "./filter-base.js";
6
7
  export declare class FromSceneObjectFilter<TShape extends Shape> extends FilterBase<TShape> {
7
8
  private sceneObjects;
8
9
  private shapeType;
10
+ private membershipSet;
9
11
  constructor(sceneObjects: SceneObject[], shapeType: ShapeType);
10
12
  getSceneObjects(): SceneObject[];
13
+ getShapeType(): ShapeType;
14
+ setMembershipSet(set: TopTools_MapOfShape | null): void;
11
15
  match(shape: TShape): boolean;
12
16
  compareTo(other: FromSceneObjectFilter<TShape>): boolean;
13
17
  transform(_matrix: Matrix4): FromSceneObjectFilter<TShape>;
@@ -2,6 +2,7 @@ import { FilterBase } from "./filter-base.js";
2
2
  export class FromSceneObjectFilter extends FilterBase {
3
3
  sceneObjects;
4
4
  shapeType;
5
+ membershipSet = null;
5
6
  constructor(sceneObjects, shapeType) {
6
7
  super();
7
8
  this.sceneObjects = sceneObjects;
@@ -10,7 +11,16 @@ export class FromSceneObjectFilter extends FilterBase {
10
11
  getSceneObjects() {
11
12
  return this.sceneObjects;
12
13
  }
14
+ getShapeType() {
15
+ return this.shapeType;
16
+ }
17
+ setMembershipSet(set) {
18
+ this.membershipSet = set;
19
+ }
13
20
  match(shape) {
21
+ if (this.membershipSet) {
22
+ return this.membershipSet.Contains(shape.getShape());
23
+ }
14
24
  for (const obj of this.sceneObjects) {
15
25
  const subShapes = obj.getShapes().flatMap(s => s.getSubShapes(this.shapeType));
16
26
  if (subShapes.some(sub => sub.isSame(shape))) {
@@ -1,3 +1,4 @@
1
1
  import { Matrix4 } from "../math/matrix4.js";
2
+ import { LazyMatrix } from "../math/lazy-matrix.js";
2
3
  import { SceneObject } from "../common/scene-object.js";
3
- export declare function cloneWithTransform(objects: SceneObject[], transform: Matrix4, container: SceneObject): SceneObject[];
4
+ export declare function cloneWithTransform(objects: SceneObject[], transform: Matrix4 | LazyMatrix, container: SceneObject): SceneObject[];
@@ -12,7 +12,7 @@ export declare function fuseWithSceneObjects(sceneObjects: SceneObject[], extrus
12
12
  modifiedShapes: any[];
13
13
  toolHistory?: undefined;
14
14
  } | {
15
- newShapes: Shape<import("occjs-wrapper").TopoDS_Shape>[];
15
+ newShapes: Shape<any>[];
16
16
  modifiedShapes: {
17
17
  shape: Shape<any>;
18
18
  object: SceneObject;
@@ -42,23 +42,105 @@ export function fuseWithSceneObjects(sceneObjects, extrusions, opts) {
42
42
  // stay on their original owners so we must not duplicate them.
43
43
  const unconsumed = sceneShapes.filter(s => !modifiedShapes.includes(s));
44
44
  const shapesToAdd = result.filter(s => !unconsumed.some(u => u.getShape().IsPartner(s.getShape())));
45
+ // Clean each addition with UnifySameDomain so that coplanar wall pieces
46
+ // split by the boolean fuse merge back into single faces (the visible
47
+ // "artifact seams" on the target solid). Lineage is captured per cleanup
48
+ // so downstream history can be remapped onto post-clean faces.
49
+ const cleanedShapesToAdd = [];
50
+ const cleanups = [];
51
+ const runCleanups = () => {
52
+ for (const shape of shapesToAdd) {
53
+ const cleanup = ShapeOps.cleanShapeWithLineage(shape);
54
+ cleanedShapesToAdd.push(cleanup.shape);
55
+ cleanups.push(cleanup);
56
+ }
57
+ };
58
+ p ? p.record('Clean fuse result', runCleanups) : runCleanups();
45
59
  let toolHistory;
46
60
  if (opts?.recordHistoryFor) {
47
61
  const recordHistory = () => {
48
- recordFusionHistory(opts.recordHistoryFor, sceneShapes, objShapeMap, shapesToAdd, maker, p);
62
+ recordFusionHistory(opts.recordHistoryFor, sceneShapes, objShapeMap, cleanedShapesToAdd, maker, cleanups, p);
49
63
  // Separately track tool-side (extrusion) lineage so callers can remap
50
64
  // pre-fusion categorizations (start/end/side/…) onto the post-fusion
51
- // faces. We don't store these as modifications on any scene object —
52
- // from the user's POV they are additions on the caller already.
53
- // Tool-side history is only consumed by `remapClassifiedFaces`, which
54
- // touches modifiedFaces only — skip the added* output traversal.
65
+ // faces. Tool-side history is only consumed by `remapClassifiedFaces`,
66
+ // which touches modifiedFaces only skip the added* output traversal.
55
67
  const collectTools = () => ShapeHistoryTracker.collect(maker, extrusions, { skipAdded: true });
56
- toolHistory = p ? p.record('Collect tool history', collectTools) : collectTools();
68
+ const rawToolHistory = p ? p.record('Collect tool history', collectTools) : collectTools();
69
+ toolHistory = remapHistoryThroughCleanups(rawToolHistory, cleanups);
57
70
  };
58
71
  p ? p.record('Record fusion history', recordHistory) : recordHistory();
59
72
  }
73
+ for (const cleanup of cleanups) {
74
+ cleanup.dispose();
75
+ }
60
76
  dispose();
61
- return { newShapes: shapesToAdd, modifiedShapes: modified, toolHistory };
77
+ return { newShapes: cleanedShapesToAdd, modifiedShapes: modified, toolHistory };
78
+ }
79
+ // Remap a pre-clean history through a set of cleanup lineages. Modified
80
+ // records get their result faces/edges replaced with the post-clean image;
81
+ // records whose results entirely vanish during cleanup are dropped (the
82
+ // caller will have already recorded removals for the corresponding sources).
83
+ function remapHistoryThroughCleanups(history, cleanups) {
84
+ const remapFaces = (faces) => {
85
+ const out = [];
86
+ for (const f of faces) {
87
+ let mapped = null;
88
+ for (const c of cleanups) {
89
+ const r = c.remapFace(f);
90
+ if (r !== null) {
91
+ mapped = r;
92
+ break;
93
+ }
94
+ }
95
+ if (mapped) {
96
+ out.push(...mapped);
97
+ }
98
+ else {
99
+ out.push(f);
100
+ }
101
+ }
102
+ return out;
103
+ };
104
+ const remapEdges = (edges) => {
105
+ const out = [];
106
+ for (const e of edges) {
107
+ let mapped = null;
108
+ for (const c of cleanups) {
109
+ const r = c.remapEdge(e);
110
+ if (r !== null) {
111
+ mapped = r;
112
+ break;
113
+ }
114
+ }
115
+ if (mapped) {
116
+ out.push(...mapped);
117
+ }
118
+ else {
119
+ out.push(e);
120
+ }
121
+ }
122
+ return out;
123
+ };
124
+ return {
125
+ addedFaces: remapFaces(history.addedFaces),
126
+ modifiedFaces: history.modifiedFaces
127
+ .map(r => ({ sources: r.sources, results: remapFaces(r.results) }))
128
+ .filter(r => r.results.length > 0),
129
+ generatedFaces: history.generatedFaces.map(r => ({
130
+ sources: r.sources,
131
+ results: remapFaces(r.results),
132
+ })),
133
+ removedFaces: history.removedFaces,
134
+ addedEdges: remapEdges(history.addedEdges),
135
+ modifiedEdges: history.modifiedEdges
136
+ .map(r => ({ sources: r.sources, results: remapEdges(r.results) }))
137
+ .filter(r => r.results.length > 0),
138
+ generatedEdges: history.generatedEdges.map(r => ({
139
+ sources: r.sources,
140
+ results: remapEdges(r.results),
141
+ })),
142
+ removedEdges: history.removedEdges,
143
+ };
62
144
  }
63
145
  /**
64
146
  * Record faces/edges from each shape as additions on `caller`. Used when a
@@ -86,12 +168,50 @@ function recordShapesAsAdditions(caller, shapes) {
86
168
  * of a scene-shape modification. This captures both extrusion-derived faces
87
169
  * (which appear in the result via tool-side Modified()) and truly new faces.
88
170
  */
89
- function recordFusionHistory(caller, sceneShapes, owners, newShapes, maker, p) {
171
+ function recordFusionHistory(caller, sceneShapes, owners, newShapes, maker, cleanups, p) {
90
172
  const oc = getOC();
91
173
  const FACE = oc.TopAbs_ShapeEnum.TopAbs_FACE;
92
174
  const EDGE = oc.TopAbs_ShapeEnum.TopAbs_EDGE;
93
175
  const claimedFaces = new oc.TopTools_MapOfShape();
94
176
  const claimedEdges = new oc.TopTools_MapOfShape();
177
+ // Remap pre-clean faces/edges through whichever cleanup handled them,
178
+ // mirroring the cut-side helper. Faces no cleanup knew about pass through.
179
+ const remapFaces = (faces) => {
180
+ const out = [];
181
+ for (const face of faces) {
182
+ let matched = false;
183
+ for (const cleanup of cleanups) {
184
+ const remapped = cleanup.remapFace(face);
185
+ if (remapped !== null) {
186
+ out.push(...remapped);
187
+ matched = true;
188
+ break;
189
+ }
190
+ }
191
+ if (!matched) {
192
+ out.push(face);
193
+ }
194
+ }
195
+ return out;
196
+ };
197
+ const remapEdges = (edges) => {
198
+ const out = [];
199
+ for (const edge of edges) {
200
+ let matched = false;
201
+ for (const cleanup of cleanups) {
202
+ const remapped = cleanup.remapEdge(edge);
203
+ if (remapped !== null) {
204
+ out.push(...remapped);
205
+ matched = true;
206
+ break;
207
+ }
208
+ }
209
+ if (!matched) {
210
+ out.push(edge);
211
+ }
212
+ }
213
+ return out;
214
+ };
95
215
  const collectScene = () => {
96
216
  for (const sceneShape of sceneShapes) {
97
217
  const owner = owners.get(sceneShape);
@@ -103,14 +223,28 @@ function recordFusionHistory(caller, sceneShapes, owners, newShapes, maker, p) {
103
223
  // need to compute its own added* sets.
104
224
  const history = ShapeHistoryTracker.collect(maker, [sceneShape], { skipAdded: true });
105
225
  for (const record of history.modifiedFaces) {
106
- owner.recordModifiedFaces(record.sources, record.results, caller);
107
- for (const r of record.results) {
226
+ const postCleanResults = remapFaces(record.results);
227
+ if (postCleanResults.length === 0) {
228
+ for (const src of record.sources) {
229
+ owner.recordRemovedFace(src, caller);
230
+ }
231
+ continue;
232
+ }
233
+ owner.recordModifiedFaces(record.sources, postCleanResults, caller);
234
+ for (const r of postCleanResults) {
108
235
  claimedFaces.Add(r.getShape());
109
236
  }
110
237
  }
111
238
  for (const record of history.modifiedEdges) {
112
- owner.recordModifiedEdges(record.sources, record.results, caller);
113
- for (const r of record.results) {
239
+ const postCleanResults = remapEdges(record.results);
240
+ if (postCleanResults.length === 0) {
241
+ for (const src of record.sources) {
242
+ owner.recordRemovedEdge(src, caller);
243
+ }
244
+ continue;
245
+ }
246
+ owner.recordModifiedEdges(record.sources, postCleanResults, caller);
247
+ for (const r of postCleanResults) {
114
248
  claimedEdges.Add(r.getShape());
115
249
  }
116
250
  }
@@ -17,7 +17,13 @@ export type SceneParserContext = {
17
17
  getActiveSketch(): Sketch | null;
18
18
  };
19
19
  export declare function registerBuilder<T extends Function>(builder: (context: SceneParserContext) => T): T;
20
- export declare function init(rootPath?: string): Promise<{
20
+ export interface FluidCADOptions {
21
+ mesh?: {
22
+ lineDeflection?: number;
23
+ angularDeflection?: number;
24
+ };
25
+ }
26
+ export declare function init(options?: FluidCADOptions): Promise<{
21
27
  currentScene: Scene;
22
28
  currentFile: string;
23
29
  renderer: import("./rendering/render.js").SceneRenderer;
package/lib/dist/index.js CHANGED
@@ -88,8 +88,8 @@ export function registerBuilder(builder) {
88
88
  return fn;
89
89
  ;
90
90
  }
91
- export async function init(rootPath) {
91
+ export async function init(options) {
92
92
  await loadOC();
93
- const resolvedPath = rootPath || process.env.FLUIDCAD_WORKSPACE_PATH || '';
94
- return createManager(resolvedPath);
93
+ const resolvedPath = process.env.FLUIDCAD_WORKSPACE_PATH || '';
94
+ return createManager(resolvedPath, options);
95
95
  }
@@ -4,5 +4,9 @@ export declare class FileImport {
4
4
  static deserializeShapes(fileName: string): Solid[];
5
5
  static serializeShape(shape: Shape, workspacePath: string, fileName: string): void;
6
6
  static importFile(workspacePath: string, fileName: string, data: Uint8Array): Solid[];
7
- static deserializeShapesWithMetadata(fileName: string): Solid[];
7
+ static deserializeShapesWithMetadata(fileName: string, options?: {
8
+ noColors?: boolean;
9
+ include?: Set<number>;
10
+ exclude?: Set<number>;
11
+ }): Solid[];
8
12
  }
@@ -49,32 +49,43 @@ export class FileImport {
49
49
  console.log(`Imported ${solids.length} solids with color metadata`);
50
50
  return solids;
51
51
  }
52
- static deserializeShapesWithMetadata(fileName) {
52
+ static deserializeShapesWithMetadata(fileName, options) {
53
53
  // Read geometry from .brep
54
54
  const brepFileName = fileName.replace(/\.(step|stp|brep)$/i, '');
55
55
  const shapes = FileImport.deserializeShapes(brepFileName);
56
- // Read color metadata from JSON sidecar
57
- const sceneManager = getSceneManager();
58
- const jsonPath = join(sceneManager.rootPath, 'imports', brepFileName + '.colors.json');
56
+ // Read color metadata from JSON sidecar (skipped when noColors is set)
59
57
  let colorData = [];
60
- if (fs.existsSync(jsonPath)) {
61
- colorData = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
62
- console.log(`Loaded color metadata from ${jsonPath}`);
58
+ if (!options?.noColors) {
59
+ const sceneManager = getSceneManager();
60
+ const jsonPath = join(sceneManager.rootPath, 'imports', brepFileName + '.colors.json');
61
+ if (fs.existsSync(jsonPath)) {
62
+ colorData = JSON.parse(fs.readFileSync(jsonPath, 'utf8'));
63
+ console.log(`Loaded color metadata from ${jsonPath}`);
64
+ }
63
65
  }
64
- // Build Solid objects and apply colors by face index
65
- const solids = shapes.map((shape, solidIndex) => {
66
- const solid = shape;
66
+ const include = options?.include;
67
+ const exclude = options?.exclude;
68
+ // Build Solid objects, filter by original index, and apply colors by face index.
69
+ const solids = [];
70
+ for (let solidIndex = 0; solidIndex < shapes.length; solidIndex++) {
71
+ if (include && !include.has(solidIndex)) {
72
+ continue;
73
+ }
74
+ if (exclude && exclude.has(solidIndex)) {
75
+ continue;
76
+ }
77
+ const solid = shapes[solidIndex];
67
78
  const solidColors = colorData[solidIndex];
68
- if (!solidColors)
69
- return solid;
70
- const faces = OcIO.findFaces(solid);
71
- for (const entry of solidColors.faces) {
72
- if (entry.faceIndex < faces.length) {
73
- solid.setColor(faces[entry.faceIndex].getShape(), entry.color);
79
+ if (solidColors) {
80
+ const faces = OcIO.findFaces(solid);
81
+ for (const entry of solidColors.faces) {
82
+ if (entry.faceIndex < faces.length) {
83
+ solid.setColor(faces[entry.faceIndex].getShape(), entry.color);
84
+ }
74
85
  }
75
86
  }
76
- return solid;
77
- });
87
+ solids.push(solid);
88
+ }
78
89
  console.log(`Deserialized ${solids.length} solids with color metadata`);
79
90
  return solids;
80
91
  }