fluidcad 0.0.31 → 0.0.32

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 (37) hide show
  1. package/lib/dist/core/extrude.d.ts +17 -4
  2. package/lib/dist/core/extrude.js +8 -6
  3. package/lib/dist/core/interfaces.d.ts +15 -5
  4. package/lib/dist/core/mirror.d.ts +5 -5
  5. package/lib/dist/features/2d/line.d.ts +2 -0
  6. package/lib/dist/features/2d/line.js +4 -0
  7. package/lib/dist/features/extrude-to-face.d.ts +5 -1
  8. package/lib/dist/features/extrude-to-face.js +50 -8
  9. package/lib/dist/features/extrude.js +19 -28
  10. package/lib/dist/features/rotate.js +2 -2
  11. package/lib/dist/features/select.d.ts +1 -0
  12. package/lib/dist/features/select.js +40 -12
  13. package/lib/dist/features/simple-extruder.js +6 -3
  14. package/lib/dist/filters/face/above-below.d.ts +20 -0
  15. package/lib/dist/filters/face/above-below.js +57 -0
  16. package/lib/dist/filters/face/face-filter.d.ts +26 -0
  17. package/lib/dist/filters/face/face-filter.js +64 -0
  18. package/lib/dist/filters/face/planar-filter.d.ts +15 -0
  19. package/lib/dist/filters/face/planar-filter.js +30 -0
  20. package/lib/dist/filters/from-object.d.ts +1 -0
  21. package/lib/dist/filters/from-object.js +3 -0
  22. package/lib/dist/oc/boolean-ops.js +8 -3
  23. package/lib/dist/oc/face-maker2.d.ts +8 -0
  24. package/lib/dist/oc/face-maker2.js +42 -1
  25. package/lib/dist/oc/face-ops.d.ts +6 -1
  26. package/lib/dist/oc/face-ops.js +3 -2
  27. package/lib/dist/oc/face-query.js +2 -2
  28. package/lib/dist/tests/features/cut.test.js +40 -0
  29. package/lib/dist/tests/features/extrude-to-face.test.js +52 -0
  30. package/lib/dist/tests/features/extrude.test.js +46 -8
  31. package/lib/dist/tests/features/select.test.js +141 -0
  32. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  33. package/package.json +1 -1
  34. package/ui/dist/assets/{index-B15vMQZ2.js → index-DMw0OYCF.js} +97 -97
  35. package/ui/dist/index.html +1 -1
  36. package/lib/dist/features/infinite-extrude.d.ts +0 -13
  37. package/lib/dist/features/infinite-extrude.js +0 -79
@@ -1,4 +1,5 @@
1
1
  import { IExtrude, ISceneObject } from "./interfaces.js";
2
+ import { FaceFilterBuilder } from "../filters/face/face-filter.js";
2
3
  interface ExtrudeFunction {
3
4
  /**
4
5
  * Extrudes the last sketch with a default distance.
@@ -33,15 +34,27 @@ interface ExtrudeFunction {
33
34
  /**
34
35
  * Extrudes up to the first intersecting face.
35
36
  * @param face - The literal `'first-face'`
36
- * @param target - The sketch or face-bearing scene object to extrude
37
+ * @param filters - Optional face filters to narrow the candidate set
38
+ */
39
+ (face: 'first-face', ...filters: FaceFilterBuilder[]): IExtrude;
40
+ /**
41
+ * Extrudes up to the first intersecting face.
42
+ * @param face - The literal `'first-face'`
43
+ * @param filtersAndTarget - Optional face filters followed by the target to extrude
37
44
  */
38
- (face: 'first-face', target?: ISceneObject): IExtrude;
45
+ (face: 'first-face', ...filtersAndTarget: [...FaceFilterBuilder[], ISceneObject]): IExtrude;
39
46
  /**
40
47
  * Extrudes up to the last intersecting face.
41
48
  * @param face - The literal `'last-face'`
42
- * @param target - The sketch or face-bearing scene object to extrude
49
+ * @param filters - Optional face filters to narrow the candidate set
50
+ */
51
+ (face: 'last-face', ...filters: FaceFilterBuilder[]): IExtrude;
52
+ /**
53
+ * Extrudes up to the last intersecting face.
54
+ * @param face - The literal `'last-face'`
55
+ * @param filtersAndTarget - Optional face filters followed by the target to extrude
43
56
  */
44
- (face: 'last-face', target?: ISceneObject): IExtrude;
57
+ (face: 'last-face', ...filtersAndTarget: [...FaceFilterBuilder[], ISceneObject]): IExtrude;
45
58
  }
46
59
  declare const _default: ExtrudeFunction;
47
60
  export default _default;
@@ -4,6 +4,7 @@ import { Extrude } from "../features/extrude.js";
4
4
  import { ExtrudeTwoDistances } from "../features/extrude-two-distances.js";
5
5
  import { ExtrudeToFace } from "../features/extrude-to-face.js";
6
6
  import { SelectSceneObject } from "../features/select.js";
7
+ import { FaceFilterBuilder } from "../filters/face/face-filter.js";
7
8
  function isExtrudable(obj) {
8
9
  return obj instanceof SceneObject && obj.isExtrudable();
9
10
  }
@@ -25,16 +26,17 @@ function build(context) {
25
26
  if (params.length === 0) {
26
27
  return new Extrude(defaultDistance, extrudable);
27
28
  }
29
+ if (params[0] === 'first-face' || params[0] === 'last-face') {
30
+ const rest = params.slice(1);
31
+ if (!rest.every(a => a instanceof FaceFilterBuilder)) {
32
+ throw new Error("Invalid parameter for extrude function.");
33
+ }
34
+ return new ExtrudeToFace(params[0], extrudable, rest);
35
+ }
28
36
  if (params.length === 1) {
29
37
  if (typeof params[0] === 'number') {
30
38
  return new Extrude(params[0], extrudable);
31
39
  }
32
- else if (params[0] === 'first-face') {
33
- return new ExtrudeToFace('first-face', extrudable);
34
- }
35
- else if (params[0] === 'last-face') {
36
- return new ExtrudeToFace('last-face', extrudable);
37
- }
38
40
  else if (params[0] instanceof SceneObject) {
39
41
  context.addSceneObject(params[0]);
40
42
  return new ExtrudeToFace(params[0], extrudable);
@@ -11,11 +11,6 @@ export interface ISceneObject {
11
11
  * @param value - The display name to assign.
12
12
  */
13
13
  name(value: string): this;
14
- /**
15
- * Marks this object as construction geometry. Guide objects are excluded from
16
- * final geometry output unless explicitly included.
17
- */
18
- guide(): this;
19
14
  /**
20
15
  * Marks this object as reusable. Reusable objects retain their shapes when
21
16
  * consumed by features (e.g., extrude, revolve), allowing multiple features
@@ -107,6 +102,12 @@ export interface IAxis extends ISceneObject {
107
102
  export interface ISelect extends ISceneObject {
108
103
  }
109
104
  export interface IGeometry extends ISceneObject {
105
+ /**
106
+ * Marks this sketch geometry as construction geometry. Guide geometries are
107
+ * excluded from the final sketch output (e.g., extrude, revolve) unless
108
+ * explicitly included.
109
+ */
110
+ guide(): this;
110
111
  /**
111
112
  * Returns a lazy-evaluated vertex at the start point of this geometry element.
112
113
  */
@@ -665,6 +666,15 @@ export interface IMirror extends IBooleanOperation {
665
666
  */
666
667
  exclude(...objects: ISceneObject[]): this;
667
668
  }
669
+ export interface IMirror2D extends IGeometry {
670
+ /**
671
+ * Excludes the given sketch geometries from the mirror operation. Useful
672
+ * when mirroring "everything" but a few specific geometries should be
673
+ * skipped, or when narrowing an explicit target list.
674
+ * @param objects - The sketch geometries to exclude from mirroring.
675
+ */
676
+ exclude(...objects: ISceneObject[]): this;
677
+ }
668
678
  export interface ITranslate extends ISceneObject {
669
679
  /**
670
680
  * Excludes the given objects from the translate operation. Useful when
@@ -1,29 +1,29 @@
1
1
  import { PlaneLike } from "../math/plane.js";
2
2
  import { AxisLike } from "../math/axis.js";
3
- import { IMirror, ISceneObject } from "./interfaces.js";
3
+ import { IMirror, IMirror2D, ISceneObject } from "./interfaces.js";
4
4
  interface MirrorFunction {
5
5
  /**
6
6
  * [2D] Mirror all sketch geometries across a given line.
7
7
  * @param line The line to mirror across
8
8
  */
9
- (line: ISceneObject): IMirror;
9
+ (line: ISceneObject): IMirror2D;
10
10
  /**
11
11
  * [2D] Mirror all sketch geometries across a given axis.
12
12
  * @param axis The local axis to mirror across
13
13
  */
14
- (axis: AxisLike): IMirror;
14
+ (axis: AxisLike): IMirror2D;
15
15
  /**
16
16
  * [2D] Mirror given sketch geometries across a given line.
17
17
  * @param line The line to mirror across
18
18
  * @param geometries The geometries to mirror
19
19
  */
20
- (line: ISceneObject, ...geometries: ISceneObject[]): IMirror;
20
+ (line: ISceneObject, ...geometries: ISceneObject[]): IMirror2D;
21
21
  /**
22
22
  * [2D] Mirror given sketch geometries across a given axis.
23
23
  * @param axis The local axis to mirror across
24
24
  * @param geometries The geometries to mirror
25
25
  */
26
- (axis: AxisLike, ...geometries: ISceneObject[]): IMirror;
26
+ (axis: AxisLike, ...geometries: ISceneObject[]): IMirror2D;
27
27
  /**
28
28
  * [3D] Mirror all scene shapes across a given plane.
29
29
  * @param plane The plane to mirror across
@@ -1,3 +1,4 @@
1
+ import { SceneObject } from "../../common/scene-object.js";
1
2
  import { LazyVertex } from "../lazy-vertex.js";
2
3
  import { PlaneObjectBase } from "../plane-renderable-base.js";
3
4
  import { GeometrySceneObject } from "./geometry.js";
@@ -6,6 +7,7 @@ export declare class LineTo extends GeometrySceneObject {
6
7
  private targetPlane;
7
8
  constructor(endPoint: LazyVertex, targetPlane?: PlaneObjectBase);
8
9
  build(): void;
10
+ createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
9
11
  compareTo(other: LineTo): boolean;
10
12
  getType(): string;
11
13
  getUniqueType(): string;
@@ -30,6 +30,10 @@ export class LineTo extends GeometrySceneObject {
30
30
  this.targetPlane.removeShapes(this);
31
31
  }
32
32
  }
33
+ createCopy(remap) {
34
+ const targetPlane = this.targetPlane ? (remap.get(this.targetPlane) || this.targetPlane) : null;
35
+ return new LineTo(this.endPoint, targetPlane);
36
+ }
33
37
  compareTo(other) {
34
38
  if (!(other instanceof LineTo)) {
35
39
  return false;
@@ -2,9 +2,11 @@ import { BuildSceneObjectContext, SceneObject } from "../common/scene-object.js"
2
2
  import { ExtrudeBase } from "./extrude-base.js";
3
3
  import { Extrudable } from "../helpers/types.js";
4
4
  import { Point } from "../math/point.js";
5
+ import { FaceFilterBuilder } from "../filters/face/face-filter.js";
5
6
  export declare class ExtrudeToFace extends ExtrudeBase {
6
7
  face: SceneObject | 'first-face' | 'last-face';
7
- constructor(face: SceneObject | 'first-face' | 'last-face', source?: Extrudable | SceneObject);
8
+ private faceFilters;
9
+ constructor(face: SceneObject | 'first-face' | 'last-face', source?: Extrudable | SceneObject, faceFilters?: FaceFilterBuilder[]);
8
10
  build(context: BuildSceneObjectContext): void;
9
11
  private createAdvancedExtrude;
10
12
  private resizePlanarFace;
@@ -20,6 +22,7 @@ export declare class ExtrudeToFace extends ExtrudeBase {
20
22
  private createSimpleExtrude;
21
23
  private getFace;
22
24
  private getFirstOrLastFace;
25
+ private collectFromSceneObjects;
23
26
  getDependencies(): SceneObject[];
24
27
  createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
25
28
  compareTo(other: ExtrudeToFace): boolean;
@@ -39,6 +42,7 @@ export declare class ExtrudeToFace extends ExtrudeBase {
39
42
  draft: [number, number];
40
43
  endOffset: number;
41
44
  face: string;
45
+ faceFilterCount: number;
42
46
  thin: [number, number] | [number];
43
47
  };
44
48
  }
@@ -11,11 +11,15 @@ import { Explorer } from "../oc/explorer.js";
11
11
  import { FaceMaker2 } from "../oc/face-maker2.js";
12
12
  import { ThinFaceMaker } from "../oc/thin-face-maker.js";
13
13
  import { Point } from "../math/point.js";
14
+ import { ShapeFilter } from "../filters/filter.js";
15
+ import { FromSceneObjectFilter } from "../filters/from-object.js";
14
16
  export class ExtrudeToFace extends ExtrudeBase {
15
17
  face;
16
- constructor(face, source) {
18
+ faceFilters;
19
+ constructor(face, source, faceFilters = []) {
17
20
  super(source);
18
21
  this.face = face;
22
+ this.faceFilters = faceFilters;
19
23
  }
20
24
  build(context) {
21
25
  const allSceneObjects = context.getSceneObjects();
@@ -252,12 +256,31 @@ export class ExtrudeToFace extends ExtrudeBase {
252
256
  }
253
257
  }
254
258
  }
255
- const result = FaceQuery.findFaceByDistance(allFaces, plane, mode);
259
+ let candidates = allFaces;
260
+ if (this.faceFilters.length > 0) {
261
+ candidates = new ShapeFilter(allFaces, ...this.faceFilters).apply();
262
+ }
263
+ const result = FaceQuery.findFaceByDistance(candidates, plane, mode);
256
264
  if (!result) {
257
265
  throw new Error(`No face found for '${mode}-face' extrusion`);
258
266
  }
259
267
  return result;
260
268
  }
269
+ collectFromSceneObjects() {
270
+ const objects = [];
271
+ for (const builder of this.faceFilters) {
272
+ for (const filter of builder.getFilters()) {
273
+ if (filter instanceof FromSceneObjectFilter) {
274
+ for (const obj of filter.getSceneObjects()) {
275
+ if (!objects.includes(obj)) {
276
+ objects.push(obj);
277
+ }
278
+ }
279
+ }
280
+ }
281
+ }
282
+ return objects;
283
+ }
261
284
  getDependencies() {
262
285
  const deps = [];
263
286
  const source = this.getSource();
@@ -267,6 +290,11 @@ export class ExtrudeToFace extends ExtrudeBase {
267
290
  if (this.face instanceof SceneObject) {
268
291
  deps.push(this.face);
269
292
  }
293
+ for (const obj of this.collectFromSceneObjects()) {
294
+ if (!deps.includes(obj)) {
295
+ deps.push(obj);
296
+ }
297
+ }
270
298
  return deps;
271
299
  }
272
300
  createCopy(remap) {
@@ -275,7 +303,8 @@ export class ExtrudeToFace extends ExtrudeBase {
275
303
  : this.face;
276
304
  const source = this.getSource();
277
305
  const remapped = source ? (remap.get(source) || source) : undefined;
278
- return new ExtrudeToFace(newFace, remapped).syncWith(this);
306
+ const remappedFilters = this.faceFilters.map(f => f.remap(remap));
307
+ return new ExtrudeToFace(newFace, remapped, remappedFilters).syncWith(this);
279
308
  }
280
309
  compareTo(other) {
281
310
  if (!(other instanceof ExtrudeToFace)) {
@@ -292,15 +321,27 @@ export class ExtrudeToFace extends ExtrudeBase {
292
321
  if (thisSource && otherSource && !thisSource.compareTo(otherSource)) {
293
322
  return false;
294
323
  }
295
- if (typeof (this.face) !== typeof (other.face)) {
296
- return false;
324
+ if (this.face instanceof SceneObject) {
325
+ if (!(other.face instanceof SceneObject)) {
326
+ return false;
327
+ }
328
+ if (!this.face.compareTo(other.face)) {
329
+ return false;
330
+ }
297
331
  }
298
- if (this.face instanceof SceneObject && other.face instanceof SceneObject && !this.face.compareTo(other.face)) {
299
- return false;
332
+ else {
333
+ if (this.face !== other.face) {
334
+ return false;
335
+ }
300
336
  }
301
- if (this.face !== other.face) {
337
+ if (this.faceFilters.length !== other.faceFilters.length) {
302
338
  return false;
303
339
  }
340
+ for (let i = 0; i < this.faceFilters.length; i++) {
341
+ if (!this.faceFilters[i].equals(other.faceFilters[i])) {
342
+ return false;
343
+ }
344
+ }
304
345
  return true;
305
346
  }
306
347
  getUniqueType() {
@@ -316,6 +357,7 @@ export class ExtrudeToFace extends ExtrudeBase {
316
357
  draft: this.getDraft(),
317
358
  endOffset: this.getEndOffset(),
318
359
  face: typeof (this.face) === 'string' ? this.face : 'selection',
360
+ faceFilterCount: this.faceFilters.length,
319
361
  thin: this._thin,
320
362
  ...this.serializePickFields(),
321
363
  };
@@ -5,8 +5,11 @@ import { FaceMaker2 } from "../oc/face-maker2.js";
5
5
  import { BooleanOps } from "../oc/boolean-ops.js";
6
6
  import { EdgeOps } from "../oc/edge-ops.js";
7
7
  import { Explorer } from "../oc/explorer.js";
8
- import { ExtrudeThroughAll } from "./infinite-extrude.js";
9
8
  import { ThinFaceMaker } from "../oc/thin-face-maker.js";
9
+ /** Finite stand-in for "infinity" in through-all cuts. Truly infinite prisms
10
+ * (via OC's `Inf=true` flag) silently fail inside `BRepAlgoAPI_Cut` — verified
11
+ * experimentally — so use a large finite extrusion instead. */
12
+ const THROUGH_ALL_LENGTH = 100000;
10
13
  export class Extrude extends ExtrudeBase {
11
14
  distance;
12
15
  constructor(distance, source) {
@@ -257,36 +260,24 @@ export class Extrude extends ExtrudeBase {
257
260
  const scope = this.resolveFusionScope(context.getSceneObjects());
258
261
  let toolShapes;
259
262
  const isThroughAll = this.distance === 0;
260
- if (this._symmetric) {
261
- // Symmetric cut: create tool centered on sketch plane
262
- if (isThroughAll) {
263
- if (this.isFaceSourced()) {
264
- throw new Error("through-all is not supported with a face-sourced extrude");
265
- }
266
- const extrudeThroughAll = new ExtrudeThroughAll(this.extrudable, true, true, faces);
267
- toolShapes = extrudeThroughAll.build();
268
- }
269
- else {
270
- const extruder1 = new Extruder(faces, plane, -this.distance / 2, this.getDraft(), this.getEndOffset());
271
- const extrusions1 = extruder1.extrude();
272
- const extruder2 = new Extruder(faces, plane, this.distance / 2, this.getDraft(), this.getEndOffset());
273
- const extrusions2 = extruder2.extrude();
274
- const all = [...extrusions1, ...extrusions2];
275
- const halvesFuse = BooleanOps.fuse(all);
276
- toolShapes = halvesFuse.result;
277
- halvesFuse.dispose();
278
- }
263
+ const draft = this.getDraft();
264
+ if (isThroughAll && this.isFaceSourced()) {
265
+ throw new Error("through-all is not supported with a face-sourced extrude");
279
266
  }
280
- else if (isThroughAll) {
281
- if (this.isFaceSourced()) {
282
- throw new Error("through-all is not supported with a face-sourced extrude");
283
- }
284
- const extrudeThroughAll = new ExtrudeThroughAll(this.extrudable, false, true, faces);
285
- toolShapes = extrudeThroughAll.build();
267
+ if (this._symmetric) {
268
+ // Symmetric cut: create tool centered on sketch plane, fusing two halves
269
+ // (one on each side). For through-all, each half uses THROUGH_ALL_LENGTH.
270
+ const halfDistance = isThroughAll ? THROUGH_ALL_LENGTH : this.distance / 2;
271
+ const extruder1 = new Extruder(faces, plane, -halfDistance, draft, this.getEndOffset());
272
+ const extruder2 = new Extruder(faces, plane, halfDistance, draft, this.getEndOffset());
273
+ const all = [...extruder1.extrude(), ...extruder2.extrude()];
274
+ const halvesFuse = BooleanOps.fuse(all);
275
+ toolShapes = halvesFuse.result;
276
+ halvesFuse.dispose();
286
277
  }
287
278
  else {
288
- const distance = -this.distance;
289
- const extruder = new Extruder(faces, plane, distance, this.getDraft(), this.getEndOffset());
279
+ const distance = isThroughAll ? -THROUGH_ALL_LENGTH : -this.distance;
280
+ const extruder = new Extruder(faces, plane, distance, draft, this.getEndOffset());
290
281
  toolShapes = extruder.extrude();
291
282
  }
292
283
  this.getSource()?.removeShapes(this);
@@ -90,7 +90,7 @@ export class Rotate extends SceneObject {
90
90
  return false;
91
91
  }
92
92
  for (let i = 0; i < thisTargetObjects.length; i++) {
93
- if (thisTargetObjects[i] !== otherTargetObjects[i]) {
93
+ if (!thisTargetObjects[i].compareTo(otherTargetObjects[i])) {
94
94
  return false;
95
95
  }
96
96
  }
@@ -98,7 +98,7 @@ export class Rotate extends SceneObject {
98
98
  return false;
99
99
  }
100
100
  for (let i = 0; i < this._excludedObjects.length; i++) {
101
- if (this._excludedObjects[i] !== other._excludedObjects[i]) {
101
+ if (!this._excludedObjects[i].compareTo(other._excludedObjects[i])) {
102
102
  return false;
103
103
  }
104
104
  }
@@ -11,6 +11,7 @@ export declare class SelectSceneObject extends SceneObject implements ISelect {
11
11
  private shapes;
12
12
  constructor(filters: FilterBuilderBase<Shape>[], constraintObject?: SceneObject);
13
13
  build(context: BuildSceneObjectContext): void;
14
+ private collectFromSceneObjects;
14
15
  private getAllShapes;
15
16
  getDependencies(): SceneObject[];
16
17
  createCopy(remap: Map<SceneObject, SceneObject>): SceneObject;
@@ -2,6 +2,7 @@ import { FaceFilterBuilder } from "../filters/face/face-filter.js";
2
2
  import { ShapeFilter } from "../filters/filter.js";
3
3
  import { SceneObject } from "../common/scene-object.js";
4
4
  import { BelongsToFaceFilter, NotBelongsToFaceFilter } from "../filters/edge/belongs-to-face.js";
5
+ import { FromSceneObjectFilter } from "../filters/from-object.js";
5
6
  export class SelectSceneObject extends SceneObject {
6
7
  filters;
7
8
  constraintObject;
@@ -32,30 +33,57 @@ export class SelectSceneObject extends SceneObject {
32
33
  sceneObjects = context.getSceneObjectsFromTo(parent, this);
33
34
  }
34
35
  }
36
+ // Objects passed explicitly via `from(...)` bypass the part scope so that
37
+ // cross-part selection works (e.g. select(face().from(p1)) from inside p2).
38
+ if (!this.constraintObject) {
39
+ const fromObjects = this.collectFromSceneObjects(filters);
40
+ if (fromObjects.length > 0) {
41
+ sceneObjects = sceneObjects.slice();
42
+ for (const obj of fromObjects) {
43
+ if (!sceneObjects.includes(obj)) {
44
+ sceneObjects.push(obj);
45
+ }
46
+ }
47
+ }
48
+ }
35
49
  const allShapes = this.constraintObject ? this.constraintObject.getShapes() : this.getAllShapes(sceneObjects, excludedObjects);
36
50
  if (this.type === "edge") {
37
51
  this.injectScopeFaces(filters, sceneObjects);
38
52
  }
39
53
  const filteredShapes = this.applyFilters(allShapes, filters);
40
- console.log(`SelectSceneObject: shapes after filtering: ${filteredShapes[0]}`);
41
54
  this.addShapes(filteredShapes);
42
55
  }
56
+ collectFromSceneObjects(filters) {
57
+ const objects = [];
58
+ for (const builder of filters) {
59
+ for (const filter of builder.getFilters()) {
60
+ if (filter instanceof FromSceneObjectFilter) {
61
+ for (const obj of filter.getSceneObjects()) {
62
+ if (!objects.includes(obj)) {
63
+ objects.push(obj);
64
+ }
65
+ }
66
+ }
67
+ }
68
+ }
69
+ return objects;
70
+ }
43
71
  getAllShapes(scope, exludedShapes) {
44
72
  const scopeShapes = scope.flatMap(obj => obj.getShapes({}, 'solid').map(s => s.getSubShapes(this.type)).flat());
45
73
  exludedShapes = exludedShapes.flatMap(s => s.getSubShapes(this.type));
46
- const finalShapes = scopeShapes.filter(shape => !exludedShapes.some(exShape => exShape.isSame(shape)));
47
- console.log('=== Scope Objects:', scope.length, ' Shapes:', scopeShapes.length);
48
- console.log('=== Excluded Shapes:', exludedShapes.length);
49
- console.log('=== Final Shapes after exclusion:', finalShapes.length);
50
- let allShapes = [];
51
- for (const shape of finalShapes) {
52
- allShapes.push(shape);
53
- }
54
- console.log('SelectSceneObject: total shapes collected for filtering:', allShapes.length);
55
- return allShapes;
74
+ return scopeShapes.filter(shape => !exludedShapes.some(exShape => exShape.isSame(shape)));
56
75
  }
57
76
  getDependencies() {
58
- return this.constraintObject ? [this.constraintObject] : [];
77
+ const deps = [];
78
+ if (this.constraintObject) {
79
+ deps.push(this.constraintObject);
80
+ }
81
+ for (const obj of this.collectFromSceneObjects(this.filters)) {
82
+ if (!deps.includes(obj)) {
83
+ deps.push(obj);
84
+ }
85
+ }
86
+ return deps;
59
87
  }
60
88
  createCopy(remap) {
61
89
  const remappedConstraint = this.constraintObject
@@ -73,13 +73,16 @@ export class Extruder {
73
73
  remainingFaces.push(f);
74
74
  }
75
75
  }
76
- // Use the firstFace from the solid to detect inner wires
77
- // Inner wires (CCW) indicate holesside faces sharing edges with them are internal
76
+ // Use the firstFace from the solid to detect inner wires. Test against
77
+ // the face's actual outward normalfor negative-distance extrudes the
78
+ // firstFace's outward normal flips relative to plane.normal, which would
79
+ // otherwise invert outer/inner detection and swap side/internal faces.
78
80
  const resolvedFirst = firstFace;
81
+ const outwardNormal = resolvedFirst.calculateNormal();
79
82
  const innerWireEdgeShapes = [];
80
83
  const wires = resolvedFirst.getWires();
81
84
  for (const wire of wires) {
82
- if (!wire.isCW(this.plane.normal)) {
85
+ if (wire.isCW(outwardNormal)) {
83
86
  for (const edge of wire.getEdges()) {
84
87
  innerWireEdgeShapes.push(edge);
85
88
  }
@@ -0,0 +1,20 @@
1
+ import { Matrix4 } from "../../math/matrix4.js";
2
+ import { Face } from "../../common/shapes.js";
3
+ import { FilterBase } from "../filter-base.js";
4
+ import { PlaneObjectBase } from "../../features/plane-renderable-base.js";
5
+ export declare class AboveFacePlaneFilter extends FilterBase<Face> {
6
+ private plane;
7
+ private partial;
8
+ constructor(plane: PlaneObjectBase, partial?: boolean);
9
+ match(shape: Face): boolean;
10
+ compareTo(other: AboveFacePlaneFilter): boolean;
11
+ transform(matrix: Matrix4): AboveFacePlaneFilter;
12
+ }
13
+ export declare class BelowFacePlaneFilter extends FilterBase<Face> {
14
+ private plane;
15
+ private partial;
16
+ constructor(plane: PlaneObjectBase, partial?: boolean);
17
+ match(shape: Face): boolean;
18
+ compareTo(other: BelowFacePlaneFilter): boolean;
19
+ transform(matrix: Matrix4): BelowFacePlaneFilter;
20
+ }
@@ -0,0 +1,57 @@
1
+ import { FilterBase } from "../filter-base.js";
2
+ import { EdgeOps } from "../../oc/edge-ops.js";
3
+ import { PlaneObject } from "../../features/plane.js";
4
+ function getBoundaryPoints(face) {
5
+ return face.getEdges().flatMap(edge => [
6
+ EdgeOps.getVertexPoint(EdgeOps.getFirstVertex(edge)),
7
+ EdgeOps.getVertexPoint(EdgeOps.getLastVertex(edge)),
8
+ ]);
9
+ }
10
+ export class AboveFacePlaneFilter extends FilterBase {
11
+ plane;
12
+ partial;
13
+ constructor(plane, partial = false) {
14
+ super();
15
+ this.plane = plane;
16
+ this.partial = partial;
17
+ }
18
+ match(shape) {
19
+ const plane = this.plane.getPlane();
20
+ const flags = getBoundaryPoints(shape).map(p => plane.signedDistanceToPoint(p) > 0);
21
+ if (flags.length === 0) {
22
+ return false;
23
+ }
24
+ return this.partial ? flags.some(Boolean) : flags.every(Boolean);
25
+ }
26
+ compareTo(other) {
27
+ return this.plane.compareTo(other.plane) && this.partial === other.partial;
28
+ }
29
+ transform(matrix) {
30
+ const transformedPlane = this.plane.getPlane().applyMatrix(matrix);
31
+ return new AboveFacePlaneFilter(new PlaneObject(transformedPlane), this.partial);
32
+ }
33
+ }
34
+ export class BelowFacePlaneFilter extends FilterBase {
35
+ plane;
36
+ partial;
37
+ constructor(plane, partial = false) {
38
+ super();
39
+ this.plane = plane;
40
+ this.partial = partial;
41
+ }
42
+ match(shape) {
43
+ const plane = this.plane.getPlane();
44
+ const flags = getBoundaryPoints(shape).map(p => plane.signedDistanceToPoint(p) < 0);
45
+ if (flags.length === 0) {
46
+ return false;
47
+ }
48
+ return this.partial ? flags.some(Boolean) : flags.every(Boolean);
49
+ }
50
+ compareTo(other) {
51
+ return this.plane.compareTo(other.plane) && this.partial === other.partial;
52
+ }
53
+ transform(matrix) {
54
+ const transformedPlane = this.plane.getPlane().applyMatrix(matrix);
55
+ return new BelowFacePlaneFilter(new PlaneObject(transformedPlane), this.partial);
56
+ }
57
+ }
@@ -70,6 +70,14 @@ export declare class FaceFilterBuilder extends FilterBuilderBase<Face> {
70
70
  * @param minorRadius - Optional radius of the tube itself.
71
71
  */
72
72
  notTorus(majorRadius?: number, minorRadius?: number): this;
73
+ /**
74
+ * Selects planar (flat) faces.
75
+ */
76
+ planar(): this;
77
+ /**
78
+ * Excludes planar (flat) faces.
79
+ */
80
+ notPlanar(): this;
73
81
  /**
74
82
  * Selects conical faces.
75
83
  */
@@ -119,6 +127,24 @@ export declare class FaceFilterBuilder extends FilterBuilderBase<Face> {
119
127
  * @param count - The number of edges to exclude.
120
128
  */
121
129
  notEdgeCount(count: number): this;
130
+ /**
131
+ * Selects faces that are entirely above the given plane (in the direction of its normal).
132
+ * @param plane - The reference plane.
133
+ * @param offsetOrOptions - Offset distance, or an options object with `offset` and `partial`.
134
+ */
135
+ above(plane: PlaneLike | PlaneObjectBase, offsetOrOptions?: number | {
136
+ offset?: number;
137
+ partial?: boolean;
138
+ }): this;
139
+ /**
140
+ * Selects faces that are entirely below the given plane (opposite to its normal direction).
141
+ * @param plane - The reference plane.
142
+ * @param offsetOrOptions - Offset distance, or an options object with `offset` and `partial`.
143
+ */
144
+ below(plane: PlaneLike | PlaneObjectBase, offsetOrOptions?: number | {
145
+ offset?: number;
146
+ partial?: boolean;
147
+ }): this;
122
148
  /**
123
149
  * Restricts the selection to faces originating from the given scene objects.
124
150
  * Recursive: passing a container picks up faces from its descendants.