fluidcad 0.0.20 → 0.0.22

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 (48) 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/features/2d/rect.d.ts +1 -0
  13. package/lib/dist/features/2d/rect.js +13 -9
  14. package/lib/dist/features/2d/sketch.d.ts +1 -0
  15. package/lib/dist/features/2d/sketch.js +21 -4
  16. package/lib/dist/features/copy-circular.js +1 -0
  17. package/lib/dist/features/copy-circular2d.js +1 -0
  18. package/lib/dist/features/copy-linear.js +4 -5
  19. package/lib/dist/features/copy-linear2d.js +4 -5
  20. package/lib/dist/features/draft.d.ts +15 -0
  21. package/lib/dist/features/draft.js +88 -0
  22. package/lib/dist/filters/edge/edge-filter.d.ts +0 -14
  23. package/lib/dist/filters/edge/edge-filter.js +2 -0
  24. package/lib/dist/filters/face/face-filter.d.ts +0 -14
  25. package/lib/dist/filters/face/face-filter.js +2 -0
  26. package/lib/dist/oc/draft-ops.d.ts +5 -0
  27. package/lib/dist/oc/draft-ops.js +51 -0
  28. package/lib/dist/oc/mesh.d.ts +2 -0
  29. package/lib/dist/oc/mesh.js +14 -6
  30. package/lib/dist/oc/wire-ops.js +4 -1
  31. package/lib/dist/rendering/mesh-transform.d.ts +3 -0
  32. package/lib/dist/rendering/mesh-transform.js +22 -0
  33. package/lib/dist/rendering/render-solid.js +3 -2
  34. package/lib/dist/rendering/render.js +25 -4
  35. package/lib/dist/tests/features/draft.test.d.ts +1 -0
  36. package/lib/dist/tests/features/draft.test.js +147 -0
  37. package/lib/dist/tests/features/part.test.js +69 -114
  38. package/lib/dist/tsconfig.tsbuildinfo +1 -1
  39. package/package.json +1 -1
  40. package/server/dist/fluidcad-server.d.ts +2 -0
  41. package/server/dist/fluidcad-server.js +10 -0
  42. package/server/dist/routes/actions.js +20 -0
  43. package/server/dist/vite-manager.js +7 -1
  44. package/ui/dist/assets/{index-Bz0YoaQD.js → index-C0JwQ8Bk.js} +15 -11
  45. package/ui/dist/assets/{index-BfcNNxXr.css → index-gPoNOiIs.css} +1 -1
  46. package/ui/dist/index.html +2 -2
  47. package/lib/dist/core/use.d.ts +0 -5
  48. package/lib/dist/core/use.js +0 -22
@@ -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
+ }
@@ -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) {
@@ -43,9 +43,12 @@ export class WireOps {
43
43
  static makeWireFromEdgesRaw(edges) {
44
44
  const oc = getOC();
45
45
  const wireMaker = new oc.BRepBuilderAPI_MakeWire();
46
+ const edgeList = new oc.TopTools_ListOfShape();
46
47
  for (const edge of edges) {
47
- wireMaker.Add(oc.TopoDS.Edge(edge));
48
+ edgeList.Append(oc.TopoDS.Edge(edge));
48
49
  }
50
+ wireMaker.Add(edgeList);
51
+ edgeList.delete();
49
52
  if (!wireMaker.IsDone()) {
50
53
  wireMaker.delete();
51
54
  throw new Error("Failed to create wire from edges");
@@ -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({
@@ -219,7 +242,5 @@ export function renderScene(scene) {
219
242
  for (const object of sceneObjects) {
220
243
  renderSceneObject(object, scene, buildDurations.get(object));
221
244
  }
222
- const result = scene.getRenderedObjects();
223
- console.table(result);
224
245
  return scene;
225
246
  }
@@ -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
+ });