fluidcad 0.0.22 → 0.0.24

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.
@@ -0,0 +1,18 @@
1
+ import { Matrix4 } from "../../math/matrix4.js";
2
+ import { Edge } from "../../common/shapes.js";
3
+ import { FilterBase } from "../filter-base.js";
4
+ import { SceneObject } from "../../common/scene-object.js";
5
+ export declare class IntersectsWithFilter extends FilterBase<Edge> {
6
+ private sceneObject;
7
+ constructor(sceneObject: SceneObject);
8
+ match(shape: Edge): boolean;
9
+ compareTo(other: IntersectsWithFilter): boolean;
10
+ transform(_matrix: Matrix4): IntersectsWithFilter;
11
+ }
12
+ export declare class NotIntersectsWithFilter extends FilterBase<Edge> {
13
+ private sceneObject;
14
+ constructor(sceneObject: SceneObject);
15
+ match(shape: Edge): boolean;
16
+ compareTo(other: NotIntersectsWithFilter): boolean;
17
+ transform(_matrix: Matrix4): NotIntersectsWithFilter;
18
+ }
@@ -0,0 +1,38 @@
1
+ import { FilterBase } from "../filter-base.js";
2
+ import { EdgeQuery } from "../../oc/edge-query.js";
3
+ export class IntersectsWithFilter extends FilterBase {
4
+ sceneObject;
5
+ constructor(sceneObject) {
6
+ super();
7
+ this.sceneObject = sceneObject;
8
+ }
9
+ match(shape) {
10
+ const objectEdges = this.sceneObject.getShapes({ excludeGuide: false })
11
+ .flatMap(s => s.getSubShapes("edge"));
12
+ return objectEdges.some(objEdge => EdgeQuery.doEdgesIntersect(shape, objEdge));
13
+ }
14
+ compareTo(other) {
15
+ return this.sceneObject.compareTo(other.sceneObject);
16
+ }
17
+ transform(_matrix) {
18
+ return new IntersectsWithFilter(this.sceneObject);
19
+ }
20
+ }
21
+ export class NotIntersectsWithFilter extends FilterBase {
22
+ sceneObject;
23
+ constructor(sceneObject) {
24
+ super();
25
+ this.sceneObject = sceneObject;
26
+ }
27
+ match(shape) {
28
+ const objectEdges = this.sceneObject.getShapes({ excludeGuide: false })
29
+ .flatMap(s => s.getSubShapes("edge"));
30
+ return !objectEdges.some(objEdge => EdgeQuery.doEdgesIntersect(shape, objEdge));
31
+ }
32
+ compareTo(other) {
33
+ return this.sceneObject.compareTo(other.sceneObject);
34
+ }
35
+ transform(_matrix) {
36
+ return new NotIntersectsWithFilter(this.sceneObject);
37
+ }
38
+ }
@@ -5,7 +5,8 @@ import { PlaneObjectBase } from "../../features/plane-renderable-base.js";
5
5
  export declare class OnPlaneFilter extends FilterBase<Edge> {
6
6
  private plane;
7
7
  private plane2?;
8
- constructor(plane: PlaneObjectBase, plane2?: PlaneObjectBase);
8
+ private partial;
9
+ constructor(plane: PlaneObjectBase, plane2?: PlaneObjectBase, partial?: boolean);
9
10
  match(shape: Edge): boolean;
10
11
  compareTo(other: OnPlaneFilter): boolean;
11
12
  transform(matrix: Matrix4): OnPlaneFilter;
@@ -13,7 +14,8 @@ export declare class OnPlaneFilter extends FilterBase<Edge> {
13
14
  export declare class NotOnPlaneFilter extends FilterBase<Edge> {
14
15
  private plane;
15
16
  private plane2?;
16
- constructor(plane: PlaneObjectBase, plane2?: PlaneObjectBase);
17
+ private partial;
18
+ constructor(plane: PlaneObjectBase, plane2?: PlaneObjectBase, partial?: boolean);
17
19
  match(shape: Edge): boolean;
18
20
  compareTo(other: NotOnPlaneFilter): boolean;
19
21
  transform(matrix: Matrix4): NotOnPlaneFilter;
@@ -1,16 +1,31 @@
1
1
  import { FilterBase } from "../filter-base.js";
2
2
  import { EdgeQuery } from "../../oc/edge-query.js";
3
+ import { EdgeOps } from "../../oc/edge-ops.js";
3
4
  import { PlaneObject } from "../../features/plane.js";
4
5
  export class OnPlaneFilter extends FilterBase {
5
6
  plane;
6
7
  plane2;
7
- constructor(plane, plane2) {
8
+ partial;
9
+ constructor(plane, plane2, partial = false) {
8
10
  super();
9
11
  this.plane = plane;
10
12
  this.plane2 = plane2;
13
+ this.partial = partial;
11
14
  }
12
15
  match(shape) {
13
16
  const plane = this.plane.getPlane();
17
+ if (this.partial) {
18
+ const firstPoint = EdgeOps.getVertexPoint(EdgeOps.getFirstVertex(shape));
19
+ const lastPoint = EdgeOps.getVertexPoint(EdgeOps.getLastVertex(shape));
20
+ if (plane.containsPoint(firstPoint) || plane.containsPoint(lastPoint)) {
21
+ return true;
22
+ }
23
+ if (this.plane2) {
24
+ const plane2 = this.plane2.getPlane();
25
+ return plane2.containsPoint(firstPoint) || plane2.containsPoint(lastPoint);
26
+ }
27
+ return false;
28
+ }
14
29
  if (EdgeQuery.isEdgeOnPlane(shape, plane)) {
15
30
  return true;
16
31
  }
@@ -20,7 +35,7 @@ export class OnPlaneFilter extends FilterBase {
20
35
  return false;
21
36
  }
22
37
  compareTo(other) {
23
- if (!this.plane.compareTo(other.plane)) {
38
+ if (!this.plane.compareTo(other.plane) || this.partial !== other.partial) {
24
39
  return false;
25
40
  }
26
41
  if (this.plane2 && other.plane2) {
@@ -29,31 +44,42 @@ export class OnPlaneFilter extends FilterBase {
29
44
  return this.plane2 === other.plane2;
30
45
  }
31
46
  transform(matrix) {
32
- const plane = this.plane.getPlane();
33
- const transformedPlane = plane.applyMatrix(matrix);
34
- console.log('Plane', plane.normal, 'Origin:', plane.origin, ' Transformed plane:', transformedPlane.normal, ' Origin:', transformedPlane.origin);
35
- const planeObj = new PlaneObject(transformedPlane);
47
+ const transformedPlane = this.plane.getPlane().applyMatrix(matrix);
36
48
  const planeObj2 = this.plane2 ? new PlaneObject(this.plane2.getPlane().applyMatrix(matrix)) : undefined;
37
- return new OnPlaneFilter(planeObj, planeObj2);
49
+ return new OnPlaneFilter(new PlaneObject(transformedPlane), planeObj2, this.partial);
38
50
  }
39
51
  }
40
52
  export class NotOnPlaneFilter extends FilterBase {
41
53
  plane;
42
54
  plane2;
43
- constructor(plane, plane2) {
55
+ partial;
56
+ constructor(plane, plane2, partial = false) {
44
57
  super();
45
58
  this.plane = plane;
46
59
  this.plane2 = plane2;
60
+ this.partial = partial;
47
61
  }
48
62
  match(shape) {
49
63
  const plane = this.plane.getPlane();
64
+ if (this.partial) {
65
+ const firstPoint = EdgeOps.getVertexPoint(EdgeOps.getFirstVertex(shape));
66
+ const lastPoint = EdgeOps.getVertexPoint(EdgeOps.getLastVertex(shape));
67
+ if (plane.containsPoint(firstPoint) || plane.containsPoint(lastPoint)) {
68
+ return false;
69
+ }
70
+ if (this.plane2) {
71
+ const plane2 = this.plane2.getPlane();
72
+ return !plane2.containsPoint(firstPoint) && !plane2.containsPoint(lastPoint);
73
+ }
74
+ return true;
75
+ }
50
76
  if (this.plane2) {
51
77
  return !EdgeQuery.isEdgeOnPlane(shape, plane) && !EdgeQuery.isEdgeOnPlane(shape, this.plane2.getPlane());
52
78
  }
53
79
  return !EdgeQuery.isEdgeOnPlane(shape, plane);
54
80
  }
55
81
  compareTo(other) {
56
- if (!this.plane.compareTo(other.plane)) {
82
+ if (!this.plane.compareTo(other.plane) || this.partial !== other.partial) {
57
83
  return false;
58
84
  }
59
85
  if (this.plane2 && other.plane2) {
@@ -62,9 +88,8 @@ export class NotOnPlaneFilter extends FilterBase {
62
88
  return this.plane2 === other.plane2;
63
89
  }
64
90
  transform(matrix) {
65
- const plane = this.plane.getPlane();
66
- const planeObj = new PlaneObject(plane.applyMatrix(matrix));
91
+ const transformedPlane = this.plane.getPlane().applyMatrix(matrix);
67
92
  const planeObj2 = this.plane2 ? new PlaneObject(this.plane2.getPlane().applyMatrix(matrix)) : undefined;
68
- return new NotOnPlaneFilter(planeObj, planeObj2);
93
+ return new NotOnPlaneFilter(new PlaneObject(transformedPlane), planeObj2, this.partial);
69
94
  }
70
95
  }
@@ -1,4 +1,3 @@
1
- import { Matrix4 } from "../math/matrix4.js";
2
1
  import { Shape } from "../common/shapes.js";
3
2
  import { FilterBase } from "./filter-base.js";
4
3
  export declare class FilterBuilderBase<TShape extends Shape = Shape> {
@@ -10,8 +9,4 @@ export declare class FilterBuilderBase<TShape extends Shape = Shape> {
10
9
  * by tangency (G1 continuity) to the initially matched shapes.
11
10
  */
12
11
  withTangents(): this;
13
- hasTangentExpansion(): boolean;
14
- getFilters(): FilterBase<TShape>[];
15
- transform(matrix: Matrix4): FilterBuilderBase<TShape>;
16
- equals(other: FilterBuilderBase<TShape>): boolean;
17
12
  }
@@ -13,12 +13,21 @@ export class FilterBuilderBase {
13
13
  this._withTangents = true;
14
14
  return this;
15
15
  }
16
+ /**
17
+ * @internal
18
+ */
16
19
  hasTangentExpansion() {
17
20
  return this._withTangents;
18
21
  }
22
+ /**
23
+ * @internal
24
+ */
19
25
  getFilters() {
20
26
  return this.filters;
21
27
  }
28
+ /**
29
+ * @internal
30
+ */
22
31
  transform(matrix) {
23
32
  const transformedBuilder = new FilterBuilderBase();
24
33
  for (const filter of this.filters) {
@@ -27,6 +36,9 @@ export class FilterBuilderBase {
27
36
  transformedBuilder._withTangents = this._withTangents;
28
37
  return transformedBuilder;
29
38
  }
39
+ /**
40
+ * @internal
41
+ */
30
42
  equals(other) {
31
43
  if (this._withTangents !== other._withTangents) {
32
44
  return false;
@@ -34,6 +34,9 @@ export function cloneWithTransform(objects, transform, container) {
34
34
  remap.set(obj, copy);
35
35
  copy.setTransform(transform);
36
36
  copy.setCloneSource(obj);
37
+ if (obj.isReusable()) {
38
+ copy.reusable();
39
+ }
37
40
  allCloned.push(copy);
38
41
  const parent = obj.getParent();
39
42
  if (parent && remap.has(parent)) {
@@ -1,4 +1,4 @@
1
- import type { gp_Pln, gp_Vec, TopoDS_Edge, TopoDS_Shape } from "occjs-wrapper";
1
+ import type { gp_Pln, gp_Vec, TopoDS_Edge, TopoDS_Face, TopoDS_Shape } from "occjs-wrapper";
2
2
  import { Point } from "../math/point.js";
3
3
  import { Vector3d } from "../math/vector3d.js";
4
4
  import { Plane } from "../math/plane.js";
@@ -23,6 +23,8 @@ export declare class EdgeQuery {
23
23
  radius: number;
24
24
  axisDirection: Vector3d;
25
25
  };
26
+ static doEdgesIntersect(edge1: Edge, edge2: Edge): boolean;
27
+ static doesEdgeIntersectPlane(edge: Edge, plane: Plane): boolean;
26
28
  static isCircleEdgeRaw(edge: TopoDS_Shape, diameter?: number): boolean;
27
29
  static isArcEdgeRaw(edge: TopoDS_Shape, radius?: number): boolean;
28
30
  static isLineEdgeRaw(edge: TopoDS_Shape, length?: number): boolean;
@@ -41,4 +43,6 @@ export declare class EdgeQuery {
41
43
  radius: number;
42
44
  axisDirection: Vector3d;
43
45
  };
46
+ static doEdgesIntersectRaw(edge1: TopoDS_Edge, edge2: TopoDS_Edge): boolean;
47
+ static doesEdgeIntersectPlaneRaw(edge: TopoDS_Edge, face: TopoDS_Face): boolean;
44
48
  }
@@ -1,5 +1,6 @@
1
1
  import { getOC } from "./init.js";
2
2
  import { Convert } from "./convert.js";
3
+ import { FaceOps } from "./face-ops.js";
3
4
  import { Point } from "../math/point.js";
4
5
  import { Vector3d } from "../math/vector3d.js";
5
6
  export class EdgeQuery {
@@ -46,6 +47,17 @@ export class EdgeQuery {
46
47
  static getCircleDataFromEdge(edge) {
47
48
  return EdgeQuery.getCircleDataFromEdgeRaw(edge.getShape());
48
49
  }
50
+ static doEdgesIntersect(edge1, edge2) {
51
+ return EdgeQuery.doEdgesIntersectRaw(edge1.getShape(), edge2.getShape());
52
+ }
53
+ static doesEdgeIntersectPlane(edge, plane) {
54
+ const [gpPln, dispose] = Convert.toGpPln(plane);
55
+ const face = FaceOps.makeFaceFromPlane2(gpPln);
56
+ const result = EdgeQuery.doesEdgeIntersectPlaneRaw(edge.getShape(), face);
57
+ face.delete();
58
+ dispose();
59
+ return result;
60
+ }
49
61
  // Raw methods (for oc-internal and common/ use)
50
62
  static isCircleEdgeRaw(edge, diameter) {
51
63
  const oc = getOC();
@@ -241,4 +253,32 @@ export class EdgeQuery {
241
253
  curve.delete();
242
254
  return result;
243
255
  }
256
+ static doEdgesIntersectRaw(edge1, edge2) {
257
+ const oc = getOC();
258
+ const tool = new oc.IntTools_EdgeEdge(edge1, edge2);
259
+ tool.Perform();
260
+ let intersects = false;
261
+ if (tool.IsDone()) {
262
+ const parts = tool.CommonParts();
263
+ intersects = parts.Length() > 0;
264
+ parts.delete();
265
+ }
266
+ tool.delete();
267
+ return intersects;
268
+ }
269
+ static doesEdgeIntersectPlaneRaw(edge, face) {
270
+ const oc = getOC();
271
+ const tool = new oc.IntTools_EdgeFace();
272
+ tool.SetEdge(edge);
273
+ tool.SetFace(face);
274
+ tool.Perform();
275
+ let intersects = false;
276
+ if (tool.IsDone()) {
277
+ const parts = tool.CommonParts();
278
+ intersects = parts.Length() > 0;
279
+ parts.delete();
280
+ }
281
+ tool.delete();
282
+ return intersects;
283
+ }
244
284
  }
@@ -207,8 +207,9 @@ export function renderScene(scene) {
207
207
  }
208
208
  buildDurations.set(object, performance.now() - buildStart);
209
209
  }
210
- // After building, mark cloned sketches so their children are skipped
211
- if (object instanceof Sketch && object.getState('cloned-edges')) {
210
+ // After building, mark cloned sketches so their children are skipped
211
+ // the sketch's build() already populated them with transformed shapes.
212
+ if (object instanceof Sketch && object.getCloneSource()) {
212
213
  skippedContainers.add(object);
213
214
  }
214
215
  }
@@ -104,7 +104,7 @@ describe("chamfer", () => {
104
104
  rect(100, 50);
105
105
  });
106
106
  extrude(30);
107
- const sel = select(edge().onPlane("xy", 30));
107
+ const sel = select(edge().onPlane("xy", { offset: 30 }));
108
108
  chamfer(3, sel);
109
109
  render();
110
110
  const solid = render().getAllSceneObjects()
@@ -106,7 +106,7 @@ describe("fillet", () => {
106
106
  });
107
107
  extrude(30);
108
108
  // Fillet only the top horizontal edges (4 edges)
109
- const sel = select(edge().onPlane("xy", 30));
109
+ const sel = select(edge().onPlane("xy", { offset: 30 }));
110
110
  fillet(3, sel);
111
111
  render();
112
112
  const solid = render().getAllSceneObjects()
@@ -222,7 +222,7 @@ describe("select", () => {
222
222
  rect(100, 50);
223
223
  });
224
224
  extrude(30);
225
- const sel = select(edge().onPlane("xy", 30));
225
+ const sel = select(edge().onPlane("xy", { offset: 30 }));
226
226
  render();
227
227
  expect(sel.getShapes()).toHaveLength(4);
228
228
  });
@@ -236,6 +236,104 @@ describe("select", () => {
236
236
  render();
237
237
  expect(sel.getShapes()).toHaveLength(8);
238
238
  });
239
+ it("should select edges on offset plane with bothDirections", () => {
240
+ sketch("xy", () => {
241
+ rect(100, 50);
242
+ });
243
+ extrude(30);
244
+ // Edges on z=15 or z=-15 — none exist on an extruded box
245
+ const sel = select(edge().onPlane("xy", { offset: 15, bothDirections: true }));
246
+ render();
247
+ expect(sel.getShapes()).toHaveLength(0);
248
+ });
249
+ it("should select edges with partial match", () => {
250
+ sketch("xy", () => {
251
+ rect(100, 50);
252
+ });
253
+ extrude(30);
254
+ // partial: true matches edges with at least one vertex on the plane
255
+ // Bottom 4 edges are fully on XY, plus the 4 vertical edges each have one vertex on XY
256
+ const sel = select(edge().onPlane("xy", { partial: true }));
257
+ render();
258
+ expect(sel.getShapes()).toHaveLength(8);
259
+ });
260
+ it("should exclude edges with partial notOnPlane", () => {
261
+ sketch("xy", () => {
262
+ rect(100, 50);
263
+ });
264
+ extrude(30);
265
+ // notOnPlane partial: true excludes edges where any vertex touches XY
266
+ // Only the 4 top edges (z=30) have no vertex on XY
267
+ const sel = select(edge().notOnPlane("xy", { partial: true }));
268
+ render();
269
+ expect(sel.getShapes()).toHaveLength(4);
270
+ });
271
+ });
272
+ describe("above / below", () => {
273
+ it("should select edges entirely above a plane", () => {
274
+ sketch("xy", () => {
275
+ rect(100, 50);
276
+ });
277
+ extrude(30);
278
+ // Edges above z=10: the 4 top edges (z=30) have both vertices above
279
+ // The 4 vertical edges straddle z=10, so they don't match
280
+ const sel = select(edge().above("xy", { offset: 10 }));
281
+ render();
282
+ expect(sel.getShapes()).toHaveLength(4);
283
+ });
284
+ it("should select edges entirely below a plane", () => {
285
+ sketch("xy", () => {
286
+ rect(100, 50);
287
+ });
288
+ extrude(30);
289
+ // Edges below z=10: the 4 bottom edges (z=0) have both vertices below
290
+ const sel = select(edge().below("xy", { offset: 10 }));
291
+ render();
292
+ expect(sel.getShapes()).toHaveLength(4);
293
+ });
294
+ it("should not match edges on the plane itself", () => {
295
+ sketch("xy", () => {
296
+ rect(100, 50);
297
+ });
298
+ extrude(30);
299
+ // Edges above z=0: bottom edges are ON the plane (dist=0), not above
300
+ // Only top 4 edges are above, vertical edges straddle
301
+ const sel = select(edge().above("xy"));
302
+ render();
303
+ expect(sel.getShapes()).toHaveLength(4);
304
+ });
305
+ it("should select partially above edges", () => {
306
+ sketch("xy", () => {
307
+ rect(100, 50);
308
+ });
309
+ extrude(30);
310
+ // partial: true — match edges where at least one vertex is above z=0
311
+ // Top 4 edges (both vertices above) + 4 vertical edges (one vertex above)
312
+ const sel = select(edge().above("xy", { partial: true }));
313
+ render();
314
+ expect(sel.getShapes()).toHaveLength(8);
315
+ });
316
+ it("should select partially below edges", () => {
317
+ sketch("xy", () => {
318
+ rect(100, 50);
319
+ });
320
+ extrude(30);
321
+ // partial: true — match edges where at least one vertex is below z=30
322
+ // Bottom 4 edges (both vertices below) + 4 vertical edges (one vertex below)
323
+ const sel = select(edge().below("xy", { offset: 30, partial: true }));
324
+ render();
325
+ expect(sel.getShapes()).toHaveLength(8);
326
+ });
327
+ it("should return empty when no edges match", () => {
328
+ sketch("xy", () => {
329
+ rect(100, 50);
330
+ });
331
+ extrude(30);
332
+ // Nothing is below z=0 on a box sitting on XY
333
+ const sel = select(edge().below("xy"));
334
+ render();
335
+ expect(sel.getShapes()).toHaveLength(0);
336
+ });
239
337
  });
240
338
  describe("parallelTo / notParallelTo", () => {
241
339
  it("should select edges parallel to a plane", () => {
@@ -369,7 +467,7 @@ describe("select", () => {
369
467
  extrude(30);
370
468
  // Faces that have both a line edge on XY AND a line edge on XY offset 30
371
469
  // The 4 side faces each have edges on both bottom and top planes
372
- const sel = select(face().hasEdge(edge().onPlane("xy"), edge().onPlane("xy", 30)));
470
+ const sel = select(face().hasEdge(edge().onPlane("xy"), edge().onPlane("xy", { offset: 30 })));
373
471
  render();
374
472
  expect(sel.getShapes()).toHaveLength(4);
375
473
  });
@@ -459,7 +557,7 @@ describe("select", () => {
459
557
  });
460
558
  extrude(30);
461
559
  // Edges on bottom OR edges on top
462
- const sel = select(edge().onPlane("xy"), edge().onPlane("xy", 30));
560
+ const sel = select(edge().onPlane("xy"), edge().onPlane("xy", { offset: 30 }));
463
561
  render();
464
562
  expect(sel.getShapes()).toHaveLength(8);
465
563
  });