fluidcad 0.0.26 → 0.0.27

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 (58) hide show
  1. package/lib/dist/common/shape-factory.d.ts +1 -1
  2. package/lib/dist/common/shapes.d.ts +0 -1
  3. package/lib/dist/common/shapes.js +0 -1
  4. package/lib/dist/core/extrude.d.ts +12 -13
  5. package/lib/dist/core/extrude.js +19 -1
  6. package/lib/dist/core/part.d.ts +2 -1
  7. package/lib/dist/core/part.js +4 -1
  8. package/lib/dist/core/sketch.d.ts +4 -3
  9. package/lib/dist/core/sketch.js +4 -1
  10. package/lib/dist/features/extrude-base.d.ts +7 -1
  11. package/lib/dist/features/extrude-base.js +36 -3
  12. package/lib/dist/features/extrude-to-face.d.ts +1 -1
  13. package/lib/dist/features/extrude-to-face.js +29 -17
  14. package/lib/dist/features/extrude-two-distances.d.ts +1 -1
  15. package/lib/dist/features/extrude-two-distances.js +23 -12
  16. package/lib/dist/features/extrude.d.ts +1 -1
  17. package/lib/dist/features/extrude.js +47 -15
  18. package/lib/dist/features/mirror-shape.d.ts +1 -3
  19. package/lib/dist/features/mirror-shape.js +2 -1
  20. package/lib/dist/features/revolve.js +4 -2
  21. package/lib/dist/features/rotate.js +1 -0
  22. package/lib/dist/features/simple-extruder.js +5 -0
  23. package/lib/dist/features/translate.js +3 -1
  24. package/lib/dist/filters/face/face-filter.d.ts +12 -0
  25. package/lib/dist/filters/face/face-filter.js +21 -0
  26. package/lib/dist/filters/face/torus-filter.d.ts +19 -0
  27. package/lib/dist/filters/face/torus-filter.js +38 -0
  28. package/lib/dist/helpers/scene-helpers.d.ts +3 -1
  29. package/lib/dist/helpers/scene-helpers.js +6 -3
  30. package/lib/dist/index.d.ts +1 -0
  31. package/lib/dist/oc/boolean-ops.d.ts +5 -3
  32. package/lib/dist/oc/boolean-ops.js +15 -2
  33. package/lib/dist/oc/face-ops.d.ts +0 -1
  34. package/lib/dist/oc/face-ops.js +0 -13
  35. package/lib/dist/oc/face-query.d.ts +2 -0
  36. package/lib/dist/oc/face-query.js +30 -0
  37. package/lib/dist/oc/fillet-ops.js +84 -66
  38. package/lib/dist/oc/mesh.d.ts +25 -2
  39. package/lib/dist/oc/mesh.js +112 -35
  40. package/lib/dist/oc/shape-ops.d.ts +1 -21
  41. package/lib/dist/oc/shape-ops.js +0 -103
  42. package/lib/dist/rendering/mesh-transform.js +17 -1
  43. package/lib/dist/rendering/render-solid.js +19 -6
  44. package/lib/dist/rendering/render-wire.js +2 -0
  45. package/lib/dist/rendering/render.d.ts +12 -2
  46. package/lib/dist/rendering/render.js +195 -220
  47. package/lib/dist/scene-manager.d.ts +2 -0
  48. package/lib/dist/scene-manager.js +4 -3
  49. package/lib/dist/tests/features/extrude.test.js +71 -0
  50. package/lib/dist/tests/features/fillet2d.test.js +16 -1
  51. package/lib/dist/tests/features/select.test.js +50 -0
  52. package/lib/dist/tests/setup.js +3 -2
  53. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +3 -3
  55. package/ui/dist/assets/{index-BeLxRMCv.js → index-55iqIwnj.js} +37 -37
  56. package/ui/dist/index.html +1 -1
  57. package/lib/dist/common/solid-face.d.ts +0 -9
  58. package/lib/dist/common/solid-face.js +0 -22
@@ -8,7 +8,5 @@ export declare class MirrorShape extends SceneObject {
8
8
  compareTo(other: MirrorShape): boolean;
9
9
  getType(): string;
10
10
  getUniqueType(): string;
11
- serialize(): {
12
- plane: PlaneObjectBase;
13
- };
11
+ serialize(): {};
14
12
  }
@@ -47,6 +47,7 @@ export class MirrorShape extends SceneObject {
47
47
  for (const shape of shapes) {
48
48
  const matrix = Matrix4.mirrorPlane(plane.normal, plane.origin);
49
49
  const transformed = ShapeOps.transform(shape, matrix);
50
+ transformed.setMeshSource(shape, matrix);
50
51
  transformedShapes.push(transformed);
51
52
  }
52
53
  }
@@ -90,7 +91,7 @@ export class MirrorShape extends SceneObject {
90
91
  }
91
92
  serialize() {
92
93
  return {
93
- plane: this.plane,
94
+ // plane: this.plane,
94
95
  };
95
96
  }
96
97
  }
@@ -9,6 +9,7 @@ import { ExtrudeBase } from "./extrude-base.js";
9
9
  import { BooleanOps } from "../oc/boolean-ops.js";
10
10
  import { FaceOps } from "../oc/face-ops.js";
11
11
  import { ThinFaceMaker } from "../oc/thin-face-maker.js";
12
+ import { Matrix4 } from "../math/matrix4.js";
12
13
  export class Revolve extends ExtrudeBase {
13
14
  axis;
14
15
  angle;
@@ -45,8 +46,9 @@ export class Revolve extends ExtrudeBase {
45
46
  const solid = ExtrudeOps.makeRevol(face, axis, rad(this.angle));
46
47
  let resultSolid;
47
48
  if (this._symmetric) {
48
- const rotated = ShapeOps.rotateShape(solid.getShape(), axis, -rad(this.angle) / 2);
49
- resultSolid = Solid.fromTopoDSSolid(Explorer.toSolid(rotated));
49
+ const matrix = Matrix4.fromRotationAroundAxis(axis.origin, axis.direction, -rad(this.angle) / 2);
50
+ const rotated = ShapeOps.transform(solid, matrix);
51
+ resultSolid = Solid.fromTopoDSSolid(Explorer.toSolid(rotated.getShape()));
50
52
  }
51
53
  else {
52
54
  resultSolid = Solid.fromTopoDSSolid(Explorer.toSolid(solid.getShape()));
@@ -41,6 +41,7 @@ export class Rotate extends SceneObject {
41
41
  const shapes = obj.getShapes();
42
42
  for (const shape of shapes) {
43
43
  const transformed = ShapeOps.transform(shape, matrix);
44
+ transformed.setMeshSource(shape, matrix);
44
45
  this.addShape(transformed);
45
46
  if (!this.copy) {
46
47
  obj.removeShape(shape, this);
@@ -42,9 +42,14 @@ export class Extruder {
42
42
  let lastFaces = [];
43
43
  let sideFaces = [];
44
44
  let internalFaces = [];
45
+ console.log("Fusing faces before extrusion...", this.faces.length);
46
+ const tFuseFaces = performance.now();
45
47
  const fusedFaces = BooleanOps.fuseFaces(this.faces);
48
+ console.log(`[perf] Extruder.fuseFaces (in=${this.faces.length}, out=${fusedFaces.result.length}): ${(performance.now() - tFuseFaces).toFixed(1)} ms`);
46
49
  for (const face of fusedFaces.result) {
50
+ const time = performance.now();
47
51
  let { solid, firstFace, lastFace } = ExtrudeOps.makePrismFromVec(face, vec);
52
+ console.log(`[perf] Extruder.makePrismFromVec: ${(performance.now() - time).toFixed(1)} ms`);
48
53
  if (this.draft) {
49
54
  const draftResult = this.applyDraft(solid, firstFace, lastFace, this.plane);
50
55
  solid = draftResult.solid;
@@ -23,7 +23,9 @@ export class Translate extends SceneObject {
23
23
  continue;
24
24
  }
25
25
  const amount = this.amount.asPoint();
26
- let transformed = ShapeOps.transform(shape, Matrix4.fromTranslation(amount.x, amount.y, amount.z));
26
+ const matrix = Matrix4.fromTranslation(amount.x, amount.y, amount.z);
27
+ const transformed = ShapeOps.transform(shape, matrix);
28
+ transformed.setMeshSource(shape, matrix);
27
29
  this.addShape(transformed);
28
30
  if (!this.copy) {
29
31
  obj.removeShape(shape, this);
@@ -58,6 +58,18 @@ export declare class FaceFilterBuilder extends FilterBuilderBase<Face> {
58
58
  * @param plane - The reference plane.
59
59
  */
60
60
  notParallelTo(plane: PlaneLike | PlaneObjectBase): this;
61
+ /**
62
+ * Selects toroidal faces, optionally matching major and/or minor radius.
63
+ * @param majorRadius - Optional radius from the torus axis to the tube center.
64
+ * @param minorRadius - Optional radius of the tube itself.
65
+ */
66
+ torus(majorRadius?: number, minorRadius?: number): this;
67
+ /**
68
+ * Excludes toroidal faces, optionally matching major and/or minor radius.
69
+ * @param majorRadius - Optional radius from the torus axis to the tube center.
70
+ * @param minorRadius - Optional radius of the tube itself.
71
+ */
72
+ notTorus(majorRadius?: number, minorRadius?: number): this;
61
73
  /**
62
74
  * Selects conical faces.
63
75
  */
@@ -4,6 +4,7 @@ import { CircleFilter, NotCircleFilter } from "./circle-filter.js";
4
4
  import { ConeFilter, NotConeFilter } from "./cone-filter.js";
5
5
  import { CylinderCurveFilter, NotCylinderCurveFilter } from "./cylinder-curve.js";
6
6
  import { CylinderFilter, NotCylinderFilter } from "./cylinder.js";
7
+ import { TorusFilter, NotTorusFilter } from "./torus-filter.js";
7
8
  import { NotOnPlaneFilter, OnPlaneFilter } from "./on-plane.js";
8
9
  import { NotParallelFilter, ParallelFilter } from "./parallel.js";
9
10
  import { PlaneObject } from "../../features/plane.js";
@@ -182,6 +183,26 @@ export class FaceFilterBuilder extends FilterBuilderBase {
182
183
  this.filters.push(filter);
183
184
  return this;
184
185
  }
186
+ /**
187
+ * Selects toroidal faces, optionally matching major and/or minor radius.
188
+ * @param majorRadius - Optional radius from the torus axis to the tube center.
189
+ * @param minorRadius - Optional radius of the tube itself.
190
+ */
191
+ torus(majorRadius, minorRadius) {
192
+ const filter = new TorusFilter(majorRadius, minorRadius);
193
+ this.filters.push(filter);
194
+ return this;
195
+ }
196
+ /**
197
+ * Excludes toroidal faces, optionally matching major and/or minor radius.
198
+ * @param majorRadius - Optional radius from the torus axis to the tube center.
199
+ * @param minorRadius - Optional radius of the tube itself.
200
+ */
201
+ notTorus(majorRadius, minorRadius) {
202
+ const filter = new NotTorusFilter(majorRadius, minorRadius);
203
+ this.filters.push(filter);
204
+ return this;
205
+ }
185
206
  /**
186
207
  * Selects conical faces.
187
208
  */
@@ -0,0 +1,19 @@
1
+ import { Matrix4 } from "../../math/matrix4.js";
2
+ import { Face } from "../../common/shapes.js";
3
+ import { FilterBase } from "../filter-base.js";
4
+ export declare class TorusFilter extends FilterBase<Face> {
5
+ private majorRadius?;
6
+ private minorRadius?;
7
+ constructor(majorRadius?: number, minorRadius?: number);
8
+ match(shape: Face): boolean;
9
+ compareTo(other: TorusFilter): boolean;
10
+ transform(matrix: Matrix4): TorusFilter;
11
+ }
12
+ export declare class NotTorusFilter extends FilterBase<Face> {
13
+ private majorRadius?;
14
+ private minorRadius?;
15
+ constructor(majorRadius?: number, minorRadius?: number);
16
+ match(shape: Face): boolean;
17
+ compareTo(other: NotTorusFilter): boolean;
18
+ transform(matrix: Matrix4): NotTorusFilter;
19
+ }
@@ -0,0 +1,38 @@
1
+ import { FilterBase } from "../filter-base.js";
2
+ import { FaceQuery } from "../../oc/face-query.js";
3
+ export class TorusFilter extends FilterBase {
4
+ majorRadius;
5
+ minorRadius;
6
+ constructor(majorRadius, minorRadius) {
7
+ super();
8
+ this.majorRadius = majorRadius;
9
+ this.minorRadius = minorRadius;
10
+ }
11
+ match(shape) {
12
+ return FaceQuery.isTorusFace(shape, this.majorRadius, this.minorRadius);
13
+ }
14
+ compareTo(other) {
15
+ return this.majorRadius === other.majorRadius && this.minorRadius === other.minorRadius;
16
+ }
17
+ transform(matrix) {
18
+ return new TorusFilter(this.majorRadius, this.minorRadius);
19
+ }
20
+ }
21
+ export class NotTorusFilter extends FilterBase {
22
+ majorRadius;
23
+ minorRadius;
24
+ constructor(majorRadius, minorRadius) {
25
+ super();
26
+ this.majorRadius = majorRadius;
27
+ this.minorRadius = minorRadius;
28
+ }
29
+ match(shape) {
30
+ return !FaceQuery.isTorusFace(shape, this.majorRadius, this.minorRadius);
31
+ }
32
+ compareTo(other) {
33
+ return this.majorRadius === other.majorRadius && this.minorRadius === other.minorRadius;
34
+ }
35
+ transform(matrix) {
36
+ return new NotTorusFilter(this.majorRadius, this.minorRadius);
37
+ }
38
+ }
@@ -1,7 +1,9 @@
1
1
  import { SceneObject } from "../common/scene-object.js";
2
2
  import { Shape } from "../common/shapes.js";
3
3
  import { Plane } from "../math/plane.js";
4
- export declare function fuseWithSceneObjects(sceneObjects: SceneObject[], extrusions: Shape<any>[]): {
4
+ export declare function fuseWithSceneObjects(sceneObjects: SceneObject[], extrusions: Shape<any>[], opts?: {
5
+ glue?: 'full' | 'shift';
6
+ }): {
5
7
  newShapes: Shape<any>[];
6
8
  modifiedShapes: any[];
7
9
  } | {
@@ -1,8 +1,9 @@
1
1
  import { BooleanOps } from "../oc/boolean-ops.js";
2
2
  import { ShapeOps } from "../oc/shape-ops.js";
3
3
  import { classifyCutResult } from "./cut-helpers.js";
4
- export function fuseWithSceneObjects(sceneObjects, extrusions) {
4
+ export function fuseWithSceneObjects(sceneObjects, extrusions, opts) {
5
5
  const modified = [];
6
+ const tCollect = performance.now();
6
7
  const objShapeMap = new Map();
7
8
  for (const obj of sceneObjects) {
8
9
  const shapes = obj.getShapes({}, 'solid');
@@ -11,9 +12,11 @@ export function fuseWithSceneObjects(sceneObjects, extrusions) {
11
12
  }
12
13
  }
13
14
  let sceneShapes = Array.from(objShapeMap.keys());
14
- console.log("Fusing extrusions with scene objects. Extrusions:", extrusions.length, "Scene object shapes:", sceneShapes.length);
15
+ console.log(`[perf] fuseWithSceneObjects.collect (scenes=${sceneShapes.length}, extrusions=${extrusions.length}): ${(performance.now() - tCollect).toFixed(1)} ms`);
15
16
  const all = [...sceneShapes, ...extrusions];
16
- const { result, newShapes, modifiedShapes } = BooleanOps.fuse(all);
17
+ const tFuse = performance.now();
18
+ const { result, newShapes, modifiedShapes } = BooleanOps.fuse(all, opts);
19
+ console.log(`[perf] fuseWithSceneObjects.BooleanOps.fuse (glue=${opts?.glue ?? 'off'}): ${(performance.now() - tFuse).toFixed(1)} ms`);
17
20
  if (newShapes.length === 0 && modifiedShapes.length === 0) {
18
21
  console.log("No fusions were made.");
19
22
  return {
@@ -20,6 +20,7 @@ export declare function registerBuilder<T extends Function>(builder: (context: S
20
20
  export declare function init(rootPath?: string): Promise<{
21
21
  currentScene: Scene;
22
22
  currentFile: string;
23
+ renderer: import("./rendering/render.js").SceneRenderer;
23
24
  rootPath: string;
24
25
  setCurrentFile(filePath: string): void;
25
26
  startScene(): Scene;
@@ -8,15 +8,17 @@ export declare class BooleanOps {
8
8
  static cutShapes(shape: Shape, tool: Shape): Shape;
9
9
  static cutShapesRaw(shape: TopoDS_Shape, tool: TopoDS_Shape): TopoDS_Shape;
10
10
  static cutMultiShape(stocks: Shape[], tools: Shape[], plane?: Plane, cutDistance?: number): {
11
- result: import("../common/wire.js").Wire | Edge | Solid | Face;
12
- modified: (shape: Shape) => (import("../common/wire.js").Wire | Edge | Solid | Face)[];
11
+ result: import("../common/wire.js").Wire | Edge | Face | Solid;
12
+ modified: (shape: Shape) => (import("../common/wire.js").Wire | Edge | Face | Solid)[];
13
13
  sectionEdges: Edge[];
14
14
  startEdges: Edge[];
15
15
  endEdges: Edge[];
16
16
  internalEdges: Edge[];
17
17
  internalFaces: Face[];
18
18
  };
19
- static fuse(args: Shape[]): {
19
+ static fuse(args: Shape[], opts?: {
20
+ glue?: 'full' | 'shift';
21
+ }): {
20
22
  result: Shape[];
21
23
  modifiedShapes: Shape[];
22
24
  newShapes: Shape[];
@@ -118,12 +118,18 @@ export class BooleanOps {
118
118
  toolList.delete();
119
119
  return { result: wrappedResult, modified, sectionEdges, startEdges, endEdges, internalEdges, internalFaces };
120
120
  }
121
- static fuse(args) {
121
+ static fuse(args, opts) {
122
122
  const oc = getOC();
123
123
  const builder = new oc.BRepAlgoAPI_Fuse();
124
124
  builder.SetNonDestructive(true);
125
125
  builder.SetCheckInverted(true);
126
126
  builder.SetRunParallel(true);
127
+ if (opts?.glue === 'full') {
128
+ builder.SetGlue(oc.BOPAlgo_GlueEnum.BOPAlgo_GlueFull);
129
+ }
130
+ else if (opts?.glue === 'shift') {
131
+ builder.SetGlue(oc.BOPAlgo_GlueEnum.BOPAlgo_GlueShift);
132
+ }
127
133
  const argsList = new oc.TopTools_ListOfShape();
128
134
  for (const arg of args) {
129
135
  argsList.Append(arg.getShape());
@@ -134,11 +140,16 @@ export class BooleanOps {
134
140
  builder.SetArguments(list);
135
141
  builder.SetTools(argsList);
136
142
  const progress = new oc.Message_ProgressRange();
143
+ const tBuild = performance.now();
137
144
  builder.Build(progress);
145
+ console.log(`[perf] BooleanOps.fuse.Build (args=${args.length}): ${(performance.now() - tBuild).toFixed(1)} ms`);
146
+ const tSimplify = performance.now();
138
147
  builder.SimplifyResult(false, true, oc.Precision.Angular());
148
+ console.log(`[perf] BooleanOps.fuse.SimplifyResult: ${(performance.now() - tSimplify).toFixed(1)} ms`);
139
149
  const resultShape = builder.Shape();
150
+ const tExplore = performance.now();
140
151
  const rawShapes = Explorer.findAllShapes(resultShape);
141
- console.log('FuseMultiShape: Result shapes count:', rawShapes.length);
152
+ console.log(`[perf] BooleanOps.fuse.findAllShapes (count=${rawShapes.length}): ${(performance.now() - tExplore).toFixed(1)} ms`);
142
153
  const result = rawShapes.map(s => ShapeFactory.fromShape(s));
143
154
  const modifiedShapes = [];
144
155
  for (const shape of args) {
@@ -149,12 +160,14 @@ export class BooleanOps {
149
160
  builder.delete();
150
161
  progress.delete();
151
162
  const newShapes = [];
163
+ const tPartner = performance.now();
152
164
  for (const s of result) {
153
165
  const existsInArgs = args.some(arg => arg.getShape().IsPartner(s.getShape()));
154
166
  if (!existsInArgs) {
155
167
  newShapes.push(s);
156
168
  }
157
169
  }
170
+ console.log(`[perf] BooleanOps.fuse.IsPartner check (result=${result.length} x args=${args.length}): ${(performance.now() - tPartner).toFixed(1)} ms`);
158
171
  return { result, newShapes, modifiedShapes };
159
172
  }
160
173
  static fuseFaces(args) {
@@ -20,7 +20,6 @@ export declare class FaceOps {
20
20
  static fixFaceOrientation(face: Face | TopoDS_Face): Face;
21
21
  static makeFaceWithHoles(outerWire: Wire, holes: Wire[]): Face;
22
22
  static isPointInsideFace(point: Point, face: Face | TopoDS_Face): boolean;
23
- static getFreeBoundsWire(compound: any): TopoDS_Wire | null;
24
23
  static makeFaceFromPlane2(plane: gp_Pln): TopoDS_Face;
25
24
  static makeFaceFromPlane(plane: gp_Pln): TopoDS_Face;
26
25
  static makeFaceFromCylinder(cylinder: gp_Cylinder): TopoDS_Face;
@@ -197,19 +197,6 @@ export class FaceOps {
197
197
  disposePnt();
198
198
  return isInside;
199
199
  }
200
- static getFreeBoundsWire(compound) {
201
- const oc = getOC();
202
- const freeBounds = new oc.ShapeAnalysis_FreeBounds(compound, oc.Precision.Confusion(), true, true);
203
- const closedWiresCompound = freeBounds.GetClosedWires();
204
- const explorer = new oc.TopExp_Explorer(closedWiresCompound, oc.TopAbs_ShapeEnum.TopAbs_WIRE, oc.TopAbs_ShapeEnum.TopAbs_SHAPE);
205
- let result = null;
206
- if (explorer.More()) {
207
- result = oc.TopoDS.Wire(explorer.Current());
208
- }
209
- explorer.delete();
210
- freeBounds.delete();
211
- return result;
212
- }
213
200
  static makeFaceFromPlane2(plane) {
214
201
  const oc = getOC();
215
202
  const faceMaker = new oc.BRepBuilderAPI_MakeFace(plane, -1000, 1000, -1000, 1000);
@@ -8,6 +8,7 @@ export declare class FaceQuery {
8
8
  static isConeFace(face: Shape): boolean;
9
9
  static isCylinderFace(face: Shape, diameter?: number): boolean;
10
10
  static isCylinderCurveFace(face: Shape, diameter?: number): boolean;
11
+ static isTorusFace(face: Shape, majorRadius?: number, minorRadius?: number): boolean;
11
12
  static isFaceOnPlane(face: Shape, plane: Plane): boolean;
12
13
  static doesFaceIntersectPlane(face: Shape, plane: Plane): boolean;
13
14
  static isFaceParallelToPlane(face: Shape, plane: Plane): boolean;
@@ -23,6 +24,7 @@ export declare class FaceQuery {
23
24
  static isConeFaceRaw(face: TopoDS_Shape): boolean;
24
25
  static isCylinderFaceRaw(face: TopoDS_Shape, diameter?: number): boolean;
25
26
  static isCylinderCurveFaceRaw(face: TopoDS_Shape, diameter?: number): boolean;
27
+ static isTorusFaceRaw(face: TopoDS_Shape, majorRadius?: number, minorRadius?: number): boolean;
26
28
  static isFaceOnPlaneRaw(face: TopoDS_Shape, plane: gp_Pln): boolean;
27
29
  static isFaceParallelToPlaneRaw(face: TopoDS_Shape, plane: gp_Pln): boolean;
28
30
  static isPlanarFaceRaw(face: TopoDS_Shape): boolean;
@@ -17,6 +17,9 @@ export class FaceQuery {
17
17
  static isCylinderCurveFace(face, diameter) {
18
18
  return FaceQuery.isCylinderCurveFaceRaw(face.getShape(), diameter);
19
19
  }
20
+ static isTorusFace(face, majorRadius, minorRadius) {
21
+ return FaceQuery.isTorusFaceRaw(face.getShape(), majorRadius, minorRadius);
22
+ }
20
23
  static isFaceOnPlane(face, plane) {
21
24
  const [gpPln, dispose] = Convert.toGpPln(plane);
22
25
  const result = FaceQuery.isFaceOnPlaneRaw(face.getShape(), gpPln);
@@ -198,6 +201,33 @@ export class FaceQuery {
198
201
  }
199
202
  return false;
200
203
  }
204
+ static isTorusFaceRaw(face, majorRadius, minorRadius) {
205
+ const oc = getOC();
206
+ const ocFace = oc.TopoDS.Face(face);
207
+ const faceAdaptor = new oc.BRepAdaptor_Surface(ocFace, true);
208
+ const type = faceAdaptor.GetType();
209
+ if (type !== oc.GeomAbs_SurfaceType.GeomAbs_Torus) {
210
+ faceAdaptor.delete();
211
+ return false;
212
+ }
213
+ if (majorRadius === undefined && minorRadius === undefined) {
214
+ faceAdaptor.delete();
215
+ return true;
216
+ }
217
+ const torus = faceAdaptor.Torus();
218
+ const actualMajor = torus.MajorRadius();
219
+ const actualMinor = torus.MinorRadius();
220
+ torus.delete();
221
+ faceAdaptor.delete();
222
+ const tol = oc.Precision.Confusion();
223
+ if (majorRadius !== undefined && Math.abs(actualMajor - majorRadius) > tol) {
224
+ return false;
225
+ }
226
+ if (minorRadius !== undefined && Math.abs(actualMinor - minorRadius) > tol) {
227
+ return false;
228
+ }
229
+ return true;
230
+ }
201
231
  static isFaceOnPlaneRaw(face, plane) {
202
232
  const oc = getOC();
203
233
  return FaceOps.faceOnPlane(oc.TopoDS.Face(face), plane);
@@ -74,80 +74,98 @@ export class FilletOps {
74
74
  }
75
75
  static fillet2dRaw(wire, plane, radius) {
76
76
  const oc = getOC();
77
- let currentWire = wire;
78
- let cornerIndex = 0;
79
77
  const isClosed = wire.Closed();
80
- try {
81
- while (true) {
82
- const edges = [];
83
- const explorer = new oc.BRepTools_WireExplorer(currentWire);
84
- while (explorer.More()) {
85
- edges.push(oc.TopoDS.Edge(explorer.Current()));
86
- explorer.Next();
78
+ const ownedEdges = [];
79
+ // Extract edges in wire traversal order and canonicalize: for each REVERSED edge,
80
+ // build a new FORWARD edge whose natural parameterization matches the wire traversal.
81
+ // ChFi2d_FilletAPI returns modified edges whose natural direction matches the input's
82
+ // natural direction, so aligning natural direction with wire traversal makes the
83
+ // modEdges directly usable when rebuilding the final wire.
84
+ const wireEdges = [];
85
+ {
86
+ const explorer = new oc.BRepTools_WireExplorer(wire);
87
+ while (explorer.More()) {
88
+ const raw = oc.TopoDS.Edge(explorer.Current());
89
+ const isReversed = raw.Orientation().value === oc.TopAbs_Orientation.TopAbs_REVERSED.value;
90
+ if (!isReversed) {
91
+ wireEdges.push(raw);
92
+ ownedEdges.push(raw);
87
93
  }
88
- explorer.delete();
89
- const maxCorners = isClosed ? edges.length : edges.length - 1;
90
- if (cornerIndex >= maxCorners) {
91
- edges.forEach(e => e.delete());
92
- break;
93
- }
94
- const edge1 = edges[cornerIndex];
95
- const edge2 = edges[(cornerIndex + 1) % edges.length];
96
- const sharedVertex = oc.TopExp.LastVertex(edge1, false);
97
- const sharedPoint = oc.BRep_Tool.Pnt(sharedVertex);
98
- sharedVertex.delete();
99
- const pairWireBuilder = new oc.BRepBuilderAPI_MakeWire(edge1, edge2);
100
- const pairWire = pairWireBuilder.Wire();
101
- const filletAPI = new oc.ChFi2d_FilletAPI(pairWire, plane);
102
- const success = filletAPI.Perform(radius);
103
- if (!success) {
104
- filletAPI.delete();
105
- pairWire.delete();
106
- pairWireBuilder.delete();
107
- edges.forEach(e => e.delete());
108
- cornerIndex++;
109
- continue;
94
+ else {
95
+ const adaptor = new oc.BRepAdaptor_Curve(raw);
96
+ const edgeFirst = adaptor.FirstParameter();
97
+ const edgeLast = adaptor.LastParameter();
98
+ adaptor.delete();
99
+ const curveHandle = oc.BRep_Tool.Curve(raw, 0, 1);
100
+ if (!curveHandle || curveHandle.IsNull()) {
101
+ raw.delete();
102
+ explorer.delete();
103
+ ownedEdges.forEach(e => e.delete());
104
+ throw new Error("fillet2d: edge has no 3D curve");
105
+ }
106
+ const curve = curveHandle.get();
107
+ const reversedHandle = curve.Reversed();
108
+ const newFirst = curve.ReversedParameter(edgeLast);
109
+ const newLast = curve.ReversedParameter(edgeFirst);
110
+ const maker = new oc.BRepBuilderAPI_MakeEdge(reversedHandle, newFirst, newLast);
111
+ const newEdge = oc.TopoDS.Edge(maker.Edge());
112
+ maker.delete();
113
+ reversedHandle.delete();
114
+ curveHandle.delete();
115
+ raw.delete();
116
+ wireEdges.push(newEdge);
117
+ ownedEdges.push(newEdge);
110
118
  }
111
- const modEdge1 = new oc.TopoDS_Edge();
112
- const modEdge2 = new oc.TopoDS_Edge();
113
- const filletEdge = filletAPI.Result(sharedPoint, modEdge1, modEdge2, -1);
119
+ explorer.Next();
120
+ }
121
+ explorer.delete();
122
+ }
123
+ const currentEdges = wireEdges.slice();
124
+ const filletArcs = new Map();
125
+ const maxCorners = isClosed ? currentEdges.length : currentEdges.length - 1;
126
+ for (let cornerIndex = 0; cornerIndex < maxCorners; cornerIndex++) {
127
+ const nextIndex = (cornerIndex + 1) % currentEdges.length;
128
+ const edge1 = currentEdges[cornerIndex];
129
+ const edge2 = currentEdges[nextIndex];
130
+ const sharedVertex = oc.TopExp.LastVertex(edge1, true);
131
+ const sharedPoint = oc.BRep_Tool.Pnt(sharedVertex);
132
+ sharedVertex.delete();
133
+ const filletAPI = new oc.ChFi2d_FilletAPI(edge1, edge2, plane);
134
+ const success = filletAPI.Perform(radius);
135
+ if (!success || filletAPI.NbResults(sharedPoint) === 0) {
114
136
  sharedPoint.delete();
115
137
  filletAPI.delete();
116
- pairWire.delete();
117
- pairWireBuilder.delete();
118
- const newWireBuilder = new oc.BRepBuilderAPI_MakeWire();
119
- const nextIndex = (cornerIndex + 1) % edges.length;
120
- for (let i = 0; i < edges.length; i++) {
121
- if (i === cornerIndex) {
122
- newWireBuilder.Add(modEdge1);
123
- newWireBuilder.Add(filletEdge);
124
- }
125
- else if (i === nextIndex) {
126
- newWireBuilder.Add(modEdge2);
127
- }
128
- else {
129
- newWireBuilder.Add(edges[i]);
130
- }
131
- }
132
- const prevWire = currentWire;
133
- currentWire = newWireBuilder.Wire();
134
- newWireBuilder.delete();
135
- if (prevWire !== wire) {
136
- prevWire.delete();
137
- }
138
- modEdge1.delete();
139
- modEdge2.delete();
140
- filletEdge.delete();
141
- edges.forEach(e => e.delete());
142
- cornerIndex += 2;
138
+ continue;
143
139
  }
144
- return currentWire;
140
+ const modEdge1 = new oc.TopoDS_Edge();
141
+ const modEdge2 = new oc.TopoDS_Edge();
142
+ const filletEdge = filletAPI.Result(sharedPoint, modEdge1, modEdge2, -1);
143
+ sharedPoint.delete();
144
+ filletAPI.delete();
145
+ currentEdges[cornerIndex] = modEdge1;
146
+ currentEdges[nextIndex] = modEdge2;
147
+ filletArcs.set(cornerIndex, filletEdge);
148
+ ownedEdges.push(modEdge1, modEdge2, filletEdge);
145
149
  }
146
- catch (e) {
147
- if (currentWire !== wire) {
148
- currentWire.delete();
150
+ const edgeList = new oc.TopTools_ListOfShape();
151
+ for (let i = 0; i < currentEdges.length; i++) {
152
+ edgeList.Append(oc.TopoDS.Edge(currentEdges[i]));
153
+ const arc = filletArcs.get(i);
154
+ if (arc) {
155
+ edgeList.Append(oc.TopoDS.Edge(arc));
149
156
  }
150
- throw e;
151
157
  }
158
+ const wireBuilder = new oc.BRepBuilderAPI_MakeWire();
159
+ wireBuilder.Add(edgeList);
160
+ edgeList.delete();
161
+ if (!wireBuilder.IsDone()) {
162
+ wireBuilder.delete();
163
+ ownedEdges.forEach(e => e.delete());
164
+ throw new Error("fillet2d: failed to build filleted wire");
165
+ }
166
+ const result = wireBuilder.Wire();
167
+ wireBuilder.delete();
168
+ ownedEdges.forEach(e => e.delete());
169
+ return result;
152
170
  }
153
171
  }
@@ -1,4 +1,4 @@
1
- import type { TopoDS_Face, TopoDS_Shape } from "occjs-wrapper";
1
+ import type { TopoDS_Edge, TopoDS_Face, TopoDS_Shape } from "occjs-wrapper";
2
2
  import { Face } from "../common/face.js";
3
3
  import { Shape } from "../common/shape.js";
4
4
  export interface MeshData {
@@ -7,11 +7,34 @@ export interface MeshData {
7
7
  indices: number[];
8
8
  count?: number;
9
9
  }
10
+ export interface EnsureTriangulatedOptions {
11
+ linDefl?: number;
12
+ angDefl?: number;
13
+ parallel?: boolean;
14
+ relative?: boolean;
15
+ checkFreeEdges?: boolean;
16
+ }
10
17
  export declare class Mesh {
11
18
  static triangulateFace(face: Face, vertexOffset?: number): MeshData | null;
12
19
  static discretizeEdge(edge: Shape): MeshData;
13
- static premeshShape(shape: TopoDS_Shape): void;
20
+ /**
21
+ * Triangulates `shape` only if it doesn't already have an up-to-date
22
+ * triangulation at the requested deflection. Returns true when a fresh
23
+ * mesh was built, false when the stored one was reused.
24
+ */
25
+ static ensureTriangulated(shape: TopoDS_Shape, opts?: EnsureTriangulatedOptions): boolean;
14
26
  static triangulateFaceRaw(face: TopoDS_Face, vertexOffset?: number): MeshData | null;
15
27
  static extractFaceTriangulationRaw(face: TopoDS_Face, vertexOffset?: number): MeshData | null;
28
+ /**
29
+ * Reads the polyline stored for `edge` as a polygon-on-triangulation of
30
+ * `face`. Node indices point into the face's triangulation, so the edge
31
+ * samples coincide exactly with the face mesh vertices (watertight).
32
+ */
33
+ static discretizeEdgeOnFace(edge: TopoDS_Edge, face: TopoDS_Face): MeshData | null;
34
+ /**
35
+ * Reads the stored 3D polygon for a free edge (one not attached to a
36
+ * meshed face). Caller must have already run `ensureTriangulated` on the
37
+ * edge or its parent wire.
38
+ */
16
39
  static discretizeEdgeRaw(edge: TopoDS_Shape): MeshData;
17
40
  }