fluidcad 0.0.21 → 0.0.23

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 (63) hide show
  1. package/README.md +7 -2
  2. package/lib/dist/common/shape.d.ts +7 -0
  3. package/lib/dist/common/shape.js +7 -0
  4. package/lib/dist/core/copy.js +1 -1
  5. package/lib/dist/core/draft.d.ts +16 -0
  6. package/lib/dist/core/draft.js +29 -0
  7. package/lib/dist/core/index.d.ts +2 -3
  8. package/lib/dist/core/index.js +1 -1
  9. package/lib/dist/core/interfaces.d.ts +2 -0
  10. package/lib/dist/core/part.d.ts +2 -6
  11. package/lib/dist/core/part.js +12 -22
  12. package/lib/dist/core/trim.d.ts +12 -5
  13. package/lib/dist/core/trim.js +7 -2
  14. package/lib/dist/features/2d/sketch.js +23 -17
  15. package/lib/dist/features/copy-circular.js +1 -0
  16. package/lib/dist/features/copy-circular2d.js +1 -0
  17. package/lib/dist/features/copy-linear.js +4 -5
  18. package/lib/dist/features/copy-linear2d.js +4 -5
  19. package/lib/dist/features/draft.d.ts +15 -0
  20. package/lib/dist/features/draft.js +88 -0
  21. package/lib/dist/features/extrude-base.js +1 -0
  22. package/lib/dist/features/remove.d.ts +2 -0
  23. package/lib/dist/features/remove.js +7 -0
  24. package/lib/dist/features/trim2d.d.ts +16 -4
  25. package/lib/dist/features/trim2d.js +80 -29
  26. package/lib/dist/filters/edge/above-below.d.ts +20 -0
  27. package/lib/dist/filters/edge/above-below.js +57 -0
  28. package/lib/dist/filters/edge/edge-filter.d.ts +40 -6
  29. package/lib/dist/filters/edge/edge-filter.js +76 -8
  30. package/lib/dist/filters/edge/intersects-with.d.ts +18 -0
  31. package/lib/dist/filters/edge/intersects-with.js +38 -0
  32. package/lib/dist/filters/edge/on-plane.d.ts +4 -2
  33. package/lib/dist/filters/edge/on-plane.js +37 -12
  34. package/lib/dist/filters/filter-builder-base.d.ts +0 -5
  35. package/lib/dist/filters/filter-builder-base.js +12 -0
  36. package/lib/dist/helpers/clone-transform.js +3 -0
  37. package/lib/dist/oc/draft-ops.d.ts +5 -0
  38. package/lib/dist/oc/draft-ops.js +51 -0
  39. package/lib/dist/oc/edge-query.d.ts +5 -1
  40. package/lib/dist/oc/edge-query.js +40 -0
  41. package/lib/dist/oc/mesh.d.ts +2 -0
  42. package/lib/dist/oc/mesh.js +14 -6
  43. package/lib/dist/rendering/mesh-transform.d.ts +3 -0
  44. package/lib/dist/rendering/mesh-transform.js +22 -0
  45. package/lib/dist/rendering/render-solid.js +3 -2
  46. package/lib/dist/rendering/render.js +28 -6
  47. package/lib/dist/tests/features/chamfer.test.js +1 -1
  48. package/lib/dist/tests/features/draft.test.d.ts +1 -0
  49. package/lib/dist/tests/features/draft.test.js +147 -0
  50. package/lib/dist/tests/features/fillet.test.js +1 -1
  51. package/lib/dist/tests/features/part.test.js +69 -114
  52. package/lib/dist/tests/features/select.test.js +101 -3
  53. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  54. package/package.json +2 -3
  55. package/server/dist/fluidcad-server.d.ts +2 -0
  56. package/server/dist/fluidcad-server.js +10 -0
  57. package/server/dist/routes/actions.js +20 -0
  58. package/server/dist/vite-manager.js +7 -1
  59. package/ui/dist/assets/{index-B1LkrBga.js → index-CqP_mgZk.js} +23 -12
  60. package/ui/dist/assets/{index-BfcNNxXr.css → index-gPoNOiIs.css} +1 -1
  61. package/ui/dist/index.html +2 -2
  62. package/lib/dist/core/use.d.ts +0 -5
  63. package/lib/dist/core/use.js +0 -22
@@ -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)) {
@@ -0,0 +1,5 @@
1
+ import type { TopoDS_Shape } from "occjs-wrapper";
2
+ import { Shape } from "../common/shape.js";
3
+ export declare class DraftOps {
4
+ static applyDraft(solid: Shape, faceRaws: TopoDS_Shape[], angle: number): Shape;
5
+ }
@@ -0,0 +1,51 @@
1
+ import { getOC } from "./init.js";
2
+ import { Convert } from "./convert.js";
3
+ import { ShapeFactory } from "../common/shape-factory.js";
4
+ import { ShapeOps } from "./shape-ops.js";
5
+ import { Vector3d } from "../math/vector3d.js";
6
+ import { Plane } from "../math/plane.js";
7
+ import { Point } from "../math/point.js";
8
+ export class DraftOps {
9
+ static applyDraft(solid, faceRaws, angle) {
10
+ const oc = getOC();
11
+ const solidRaw = solid.getShape();
12
+ const bbox = ShapeOps.getBoundingBox(solid);
13
+ const neutralPlane = new Plane(new Point(0, 0, bbox.minZ), new Vector3d(1, 0, 0), new Vector3d(0, 0, 1));
14
+ const [dir, disposeDir] = Convert.toGpDir(neutralPlane.normal);
15
+ const [pln, disposePln] = Convert.toGpPln(neutralPlane);
16
+ try {
17
+ const draftMaker = new oc.BRepOffsetAPI_DraftAngle(solidRaw);
18
+ let addedCount = 0;
19
+ const explorer = new oc.TopExp_Explorer(solidRaw, oc.TopAbs_ShapeEnum.TopAbs_FACE, oc.TopAbs_ShapeEnum.TopAbs_SHAPE);
20
+ while (explorer.More()) {
21
+ const currentShape = explorer.Current();
22
+ const isSelected = faceRaws.some(sel => sel.IsSame(currentShape));
23
+ if (isSelected) {
24
+ const face = oc.TopoDS.Face(currentShape);
25
+ draftMaker.Add(face, dir, -angle, pln, true);
26
+ addedCount++;
27
+ }
28
+ explorer.Next();
29
+ }
30
+ explorer.delete();
31
+ if (addedCount === 0) {
32
+ draftMaker.delete();
33
+ return null;
34
+ }
35
+ const progress = new oc.Message_ProgressRange();
36
+ draftMaker.Build(progress);
37
+ progress.delete();
38
+ if (!draftMaker.IsDone()) {
39
+ draftMaker.delete();
40
+ throw new Error("Failed to apply draft angle.");
41
+ }
42
+ const result = draftMaker.Shape();
43
+ draftMaker.delete();
44
+ return ShapeFactory.fromShape(result);
45
+ }
46
+ finally {
47
+ disposeDir();
48
+ disposePln();
49
+ }
50
+ }
51
+ }
@@ -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
  }
@@ -10,6 +10,8 @@ export interface MeshData {
10
10
  export declare class Mesh {
11
11
  static triangulateFace(face: Face, vertexOffset?: number): MeshData | null;
12
12
  static discretizeEdge(edge: Shape): MeshData;
13
+ static premeshShape(shape: TopoDS_Shape): void;
13
14
  static triangulateFaceRaw(face: TopoDS_Face, vertexOffset?: number): MeshData | null;
15
+ static extractFaceTriangulationRaw(face: TopoDS_Face, vertexOffset?: number): MeshData | null;
14
16
  static discretizeEdgeRaw(edge: TopoDS_Shape): MeshData;
15
17
  }
@@ -7,12 +7,14 @@ export class Mesh {
7
7
  static discretizeEdge(edge) {
8
8
  return Mesh.discretizeEdgeRaw(edge.getShape());
9
9
  }
10
+ static premeshShape(shape) {
11
+ const oc = getOC();
12
+ const inc = new oc.BRepMesh_IncrementalMesh(shape, 0.3, false, 0.3, true);
13
+ inc.delete();
14
+ }
10
15
  // Raw methods (for oc-internal use)
11
16
  static triangulateFaceRaw(face, vertexOffset = 0) {
12
17
  const oc = getOC();
13
- const vertices = [];
14
- const normals = [];
15
- const indices = [];
16
18
  let inc;
17
19
  try {
18
20
  inc = new oc.BRepMesh_IncrementalMesh(face, 0.3, false, 0.3, true);
@@ -21,12 +23,19 @@ export class Mesh {
21
23
  console.error("Face mesh failed", e);
22
24
  return null;
23
25
  }
26
+ inc.delete();
27
+ return Mesh.extractFaceTriangulationRaw(face, vertexOffset);
28
+ }
29
+ static extractFaceTriangulationRaw(face, vertexOffset = 0) {
30
+ const oc = getOC();
31
+ const vertices = [];
32
+ const normals = [];
33
+ const indices = [];
24
34
  const aLocation = new oc.TopLoc_Location();
25
35
  const myT = oc.BRep_Tool.Triangulation(face, aLocation, 0);
26
36
  if (myT.IsNull()) {
27
37
  aLocation.delete();
28
- inc.delete();
29
- throw new Error("No triangulation for face");
38
+ return null;
30
39
  }
31
40
  const pc = new oc.Poly_Connect(myT);
32
41
  const triangulation = myT.get();
@@ -69,7 +78,6 @@ export class Mesh {
69
78
  triangles.delete();
70
79
  myT.delete();
71
80
  aLocation.delete();
72
- inc.delete();
73
81
  return { vertices, normals, indices, count: nbNodes };
74
82
  }
75
83
  static discretizeEdgeRaw(edge) {
@@ -0,0 +1,3 @@
1
+ import { SceneObjectMesh } from "./scene.js";
2
+ import { Matrix4 } from "../math/matrix4.js";
3
+ export declare function transformMeshes(meshes: SceneObjectMesh[], matrix: Matrix4): SceneObjectMesh[];
@@ -0,0 +1,22 @@
1
+ export function transformMeshes(meshes, matrix) {
2
+ const m = matrix.elements;
3
+ return meshes.map(mesh => {
4
+ const srcV = mesh.vertices;
5
+ const srcN = mesh.normals;
6
+ const newV = new Array(srcV.length);
7
+ const newN = new Array(srcN.length);
8
+ for (let i = 0; i < srcV.length; i += 3) {
9
+ const x = srcV[i], y = srcV[i + 1], z = srcV[i + 2];
10
+ newV[i] = m[0] * x + m[4] * y + m[8] * z + m[12];
11
+ newV[i + 1] = m[1] * x + m[5] * y + m[9] * z + m[13];
12
+ newV[i + 2] = m[2] * x + m[6] * y + m[10] * z + m[14];
13
+ }
14
+ for (let i = 0; i < srcN.length; i += 3) {
15
+ const nx = srcN[i], ny = srcN[i + 1], nz = srcN[i + 2];
16
+ newN[i] = m[0] * nx + m[4] * ny + m[8] * nz;
17
+ newN[i + 1] = m[1] * nx + m[5] * ny + m[9] * nz;
18
+ newN[i + 2] = m[2] * nx + m[6] * ny + m[10] * nz;
19
+ }
20
+ return { ...mesh, vertices: newV, normals: newN };
21
+ });
22
+ }
@@ -1,7 +1,8 @@
1
- import { renderFace } from "./render-face.js";
2
1
  import { renderEdge } from "./render-edge.js";
3
2
  import { Explorer } from "../oc/explorer.js";
3
+ import { Mesh } from "../oc/mesh.js";
4
4
  export function renderSolid(shapeObj) {
5
+ Mesh.premeshShape(shapeObj.getShape());
5
6
  const facesMeshes = getFacesMesh(shapeObj);
6
7
  const edgesMesh = getEdgesMesh(shapeObj);
7
8
  return [...facesMeshes, ...edgesMesh];
@@ -25,7 +26,7 @@ function getFacesMesh(shapeObj) {
25
26
  for (let faceIdx = 0; faceIdx < faces.length; faceIdx++) {
26
27
  const face = faces[faceIdx];
27
28
  const color = shapeObj.getColor(face.getShape());
28
- const faceResult = renderFace(face, 0);
29
+ const faceResult = Mesh.extractFaceTriangulationRaw(face.getShape(), 0);
29
30
  if (faceResult) {
30
31
  if (!groups.has(color)) {
31
32
  groups.set(color, { vertices: [], normals: [], indices: [], faceMapping: [], vertexOffset: 0 });
@@ -2,6 +2,7 @@ import { MeshBuilder } from "./mesh-builder.js";
2
2
  import { PlaneObjectBase } from "../features/plane-renderable-base.js";
3
3
  import { AxisObjectBase } from "../features/axis-renderable-base.js";
4
4
  import { Sketch } from "../features/2d/sketch.js";
5
+ import { transformMeshes } from "./mesh-transform.js";
5
6
  const meshBuilder = new MeshBuilder();
6
7
  function renderSceneObject(obj, scene, buildDurationMs) {
7
8
  const hasError = !!obj.getError();
@@ -12,7 +13,18 @@ function renderSceneObject(obj, scene, buildDurationMs) {
12
13
  for (const shape of sceneShapes) {
13
14
  let meshes = shape.getMeshes();
14
15
  if (!meshes) {
15
- meshes = meshBuilder.build(shape);
16
+ const meshSource = shape.getMeshSource();
17
+ if (meshSource) {
18
+ let sourceMeshes = meshSource.shape.getMeshes();
19
+ if (!sourceMeshes) {
20
+ sourceMeshes = meshBuilder.build(meshSource.shape);
21
+ meshSource.shape.setMeshes(sourceMeshes);
22
+ }
23
+ meshes = sourceMeshes ? transformMeshes(sourceMeshes, meshSource.matrix) : meshBuilder.build(shape);
24
+ }
25
+ else {
26
+ meshes = meshBuilder.build(shape);
27
+ }
16
28
  shape.setMeshes(meshes);
17
29
  }
18
30
  const shapeType = shape.getType();
@@ -71,7 +83,18 @@ export function renderSceneRollback(scene, rollbackIndex) {
71
83
  for (const shape of sceneShapes) {
72
84
  let meshes = shape.getMeshes();
73
85
  if (!meshes) {
74
- meshes = meshBuilder.build(shape);
86
+ const meshSource = shape.getMeshSource();
87
+ if (meshSource) {
88
+ let sourceMeshes = meshSource.shape.getMeshes();
89
+ if (!sourceMeshes) {
90
+ sourceMeshes = meshBuilder.build(meshSource.shape);
91
+ meshSource.shape.setMeshes(sourceMeshes);
92
+ }
93
+ meshes = sourceMeshes ? transformMeshes(sourceMeshes, meshSource.matrix) : meshBuilder.build(shape);
94
+ }
95
+ else {
96
+ meshes = meshBuilder.build(shape);
97
+ }
75
98
  shape.setMeshes(meshes);
76
99
  }
77
100
  renderedSceneShapes.push({
@@ -184,8 +207,9 @@ export function renderScene(scene) {
184
207
  }
185
208
  buildDurations.set(object, performance.now() - buildStart);
186
209
  }
187
- // After building, mark cloned sketches so their children are skipped
188
- 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()) {
189
213
  skippedContainers.add(object);
190
214
  }
191
215
  }
@@ -219,7 +243,5 @@ export function renderScene(scene) {
219
243
  for (const object of sceneObjects) {
220
244
  renderSceneObject(object, scene, buildDurations.get(object));
221
245
  }
222
- const result = scene.getRenderedObjects();
223
- console.table(result);
224
246
  return scene;
225
247
  }
@@ -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()
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,147 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import { setupOC, render } from "../setup.js";
3
+ import sketch from "../../core/sketch.js";
4
+ import extrude from "../../core/extrude.js";
5
+ import draft from "../../core/draft.js";
6
+ import select from "../../core/select.js";
7
+ import { rect } from "../../core/2d/index.js";
8
+ import { countShapes } from "../utils.js";
9
+ import { ShapeOps } from "../../oc/shape-ops.js";
10
+ import { ShapeProps } from "../../oc/props.js";
11
+ import { face } from "../../filters/index.js";
12
+ describe("draft", () => {
13
+ setupOC();
14
+ describe("draft with implicit selection", () => {
15
+ it("should apply draft to the last selected face", () => {
16
+ sketch("xy", () => {
17
+ rect(100, 100);
18
+ });
19
+ extrude(80);
20
+ select(face().onPlane("front"));
21
+ draft(5);
22
+ const scene = render();
23
+ expect(countShapes(scene)).toBe(1);
24
+ const solid = scene.getAllSceneObjects()
25
+ .flatMap(o => o.getShapes())
26
+ .find(s => s.getType() === "solid");
27
+ expect(solid).toBeDefined();
28
+ });
29
+ it("should change the bounding box when drafting a side face", () => {
30
+ sketch("xy", () => {
31
+ rect(100, 100);
32
+ });
33
+ extrude(80);
34
+ select(face().onPlane("front"));
35
+ draft(5);
36
+ const scene = render();
37
+ const solid = scene.getAllSceneObjects()
38
+ .flatMap(o => o.getShapes())
39
+ .find(s => s.getType() === "solid");
40
+ const bbox = ShapeOps.getBoundingBox(solid);
41
+ // 5 deg draft over 80mm height: tan(5°) * 80 ≈ 7mm extension
42
+ expect(bbox.maxY - bbox.minY).toBeGreaterThan(103);
43
+ });
44
+ it("should change volume compared to original box", () => {
45
+ sketch("xy", () => {
46
+ rect(100, 100);
47
+ });
48
+ extrude(80);
49
+ select(face().onPlane("front"));
50
+ draft(5);
51
+ const scene = render();
52
+ const solid = scene.getAllSceneObjects()
53
+ .flatMap(o => o.getShapes())
54
+ .find(s => s.getType() === "solid");
55
+ const props = ShapeProps.getProperties(solid.getShape());
56
+ const originalVolume = 100 * 100 * 80;
57
+ expect(Math.abs(props.volumeMm3 - originalVolume)).toBeGreaterThan(1000);
58
+ });
59
+ });
60
+ describe("draft with explicit selection", () => {
61
+ it("should apply draft using e.sideFaces()", () => {
62
+ sketch("xy", () => {
63
+ rect(100, 100);
64
+ });
65
+ const e = extrude(80);
66
+ draft(5, e.sideFaces());
67
+ const scene = render();
68
+ expect(countShapes(scene)).toBe(1);
69
+ const solid = scene.getAllSceneObjects()
70
+ .flatMap(o => o.getShapes())
71
+ .find(s => s.getType() === "solid");
72
+ expect(solid).toBeDefined();
73
+ });
74
+ it("should apply draft using explicit select()", () => {
75
+ sketch("xy", () => {
76
+ rect(100, 100);
77
+ });
78
+ extrude(80);
79
+ const sel = select(face().onPlane("front"));
80
+ draft(5, sel);
81
+ const scene = render();
82
+ expect(countShapes(scene)).toBe(1);
83
+ });
84
+ });
85
+ describe("draft angle effect", () => {
86
+ it("should produce larger extension with 10 degrees than 3 degrees", () => {
87
+ sketch("xy", () => {
88
+ rect(100, 100);
89
+ });
90
+ extrude(80);
91
+ select(face().onPlane("front"));
92
+ draft(10);
93
+ const scene = render();
94
+ const solid = scene.getAllSceneObjects()
95
+ .flatMap(o => o.getShapes())
96
+ .find(s => s.getType() === "solid");
97
+ const bbox = ShapeOps.getBoundingBox(solid);
98
+ // tan(10°) * 80 ≈ 14.1mm
99
+ expect(bbox.maxY - bbox.minY).toBeGreaterThan(110);
100
+ });
101
+ it("should produce smaller extension with 3 degrees", () => {
102
+ sketch("xy", () => {
103
+ rect(100, 100);
104
+ });
105
+ extrude(80);
106
+ select(face().onPlane("front"));
107
+ draft(3);
108
+ const scene = render();
109
+ const solid = scene.getAllSceneObjects()
110
+ .flatMap(o => o.getShapes())
111
+ .find(s => s.getType() === "solid");
112
+ const bbox = ShapeOps.getBoundingBox(solid);
113
+ // tan(3°) * 80 ≈ 4.2mm
114
+ expect(bbox.maxY - bbox.minY).toBeGreaterThan(103);
115
+ expect(bbox.maxY - bbox.minY).toBeLessThan(110);
116
+ });
117
+ });
118
+ describe("draft removes selection shapes", () => {
119
+ it("should remove the face selection after drafting", () => {
120
+ sketch("xy", () => {
121
+ rect(100, 100);
122
+ });
123
+ extrude(80);
124
+ const sel = select(face().onPlane("front"));
125
+ draft(5, sel);
126
+ render();
127
+ expect(sel.getShapes()).toHaveLength(0);
128
+ });
129
+ });
130
+ describe("draft on multiple faces", () => {
131
+ it("should draft all four side faces via sideFaces()", () => {
132
+ sketch("xy", () => {
133
+ rect(100, 100);
134
+ });
135
+ const e = extrude(80);
136
+ draft(5, e.sideFaces());
137
+ const scene = render();
138
+ const solid = scene.getAllSceneObjects()
139
+ .flatMap(o => o.getShapes())
140
+ .find(s => s.getType() === "solid");
141
+ const bbox = ShapeOps.getBoundingBox(solid);
142
+ // All four sides drafted — bounding box should extend in both X and Y
143
+ expect(bbox.maxX - bbox.minX).toBeGreaterThan(103);
144
+ expect(bbox.maxY - bbox.minY).toBeGreaterThan(103);
145
+ });
146
+ });
147
+ });
@@ -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()