@cadit-app/manifold-fillet 1.0.0
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.
- package/README.md +45 -0
- package/dist/context.d.ts +6 -0
- package/dist/context.js +1 -0
- package/dist/edgeSelection.d.ts +57 -0
- package/dist/edgeSelection.js +206 -0
- package/dist/example.d.ts +3 -0
- package/dist/example.js +33 -0
- package/dist/fillet.d.ts +32 -0
- package/dist/fillet.js +117 -0
- package/dist/index.d.ts +92 -0
- package/dist/index.js +60 -0
- package/dist/pipeAlongPath.d.ts +27 -0
- package/dist/pipeAlongPath.js +82 -0
- package/dist/wedgeBuilder.d.ts +23 -0
- package/dist/wedgeBuilder.js +108 -0
- package/package.json +53 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# manifold-fillet
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
A fillet/round library for Manifold meshes. Implements the "brute force" CSG approach from elalish/manifold issue #1411 Installation - npm install @cadit-app/brute-force-fillet manifold-3d
|
|
6
|
+
|
|
7
|
+
> 🔧 This is a [CADit](https://cadit.app) script package - a code-based 3D model you can open and modify.
|
|
8
|
+
>
|
|
9
|
+
> Open this design in [CADit](https://cadit.app) to preview in 3D, customize, and export.
|
|
10
|
+
> You can also fork this design and re-publish your own version!
|
|
11
|
+
|
|
12
|
+
## Use in Your Project
|
|
13
|
+
|
|
14
|
+
Install as a dependency in your TypeScript/JavaScript project:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @cadit-app/manifold-fillet
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Then import and use it in your code.
|
|
21
|
+
|
|
22
|
+
## Build Locally
|
|
23
|
+
|
|
24
|
+
Clone this repo and build 3D files offline:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
git clone https://github.com/CADit-app/manifold-fillet.git
|
|
28
|
+
cd manifold-fillet
|
|
29
|
+
npm install
|
|
30
|
+
npm run build:3mf
|
|
31
|
+
npm run build:glb
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## License
|
|
35
|
+
|
|
36
|
+
[CC BY-SA 4.0 (Attribution-ShareAlike)](https://creativecommons.org/licenses/by-sa/4.0/)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<sub>Created with <a href="https://cadit.app">CADit</a> - The open platform for code-based 3D models.</sub>
|
|
42
|
+
</p>
|
|
43
|
+
<p align="center">
|
|
44
|
+
<sub>Use our web-based <a href="https://app.cadit.app">CAD application</a> to create, open and edit designs visually.</sub>
|
|
45
|
+
</p>
|
package/dist/context.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge selection types for fillet operations.
|
|
3
|
+
*
|
|
4
|
+
* Point-based selection: Find edges nearest to a 3D point
|
|
5
|
+
* Angle-based selection: Find all edges sharper than a threshold angle
|
|
6
|
+
*/
|
|
7
|
+
import { Manifold } from 'manifold-3d';
|
|
8
|
+
/** Point-based edge selection - fillet edge nearest to a point */
|
|
9
|
+
export interface PointEdgeSelection {
|
|
10
|
+
type: 'point';
|
|
11
|
+
point: [number, number, number];
|
|
12
|
+
maxDistance?: number;
|
|
13
|
+
}
|
|
14
|
+
/** Angle-based edge selection - fillet all edges sharper than threshold */
|
|
15
|
+
export interface AngleEdgeSelection {
|
|
16
|
+
type: 'angle';
|
|
17
|
+
minAngle: number;
|
|
18
|
+
}
|
|
19
|
+
export type EdgeSelection = PointEdgeSelection | AngleEdgeSelection;
|
|
20
|
+
/**
|
|
21
|
+
* Represents an edge in a mesh as a pair of vertex indices
|
|
22
|
+
* with associated face normals for dihedral angle calculation
|
|
23
|
+
*/
|
|
24
|
+
export interface MeshEdge {
|
|
25
|
+
v0: number;
|
|
26
|
+
v1: number;
|
|
27
|
+
/** Vertex positions */
|
|
28
|
+
p0: [number, number, number];
|
|
29
|
+
p1: [number, number, number];
|
|
30
|
+
/** Face normals of adjacent triangles */
|
|
31
|
+
n0: [number, number, number];
|
|
32
|
+
n1: [number, number, number];
|
|
33
|
+
/** Dihedral angle in degrees (180 = flat, 90 = right angle) */
|
|
34
|
+
dihedralAngle: number;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Extracts edges from a Manifold mesh with dihedral angle information.
|
|
38
|
+
* An edge is defined as a pair of vertices shared by exactly 2 triangles.
|
|
39
|
+
*/
|
|
40
|
+
export declare function extractEdges(mf: Manifold): MeshEdge[];
|
|
41
|
+
/**
|
|
42
|
+
* Selects edges based on selection criteria.
|
|
43
|
+
*/
|
|
44
|
+
export declare function selectEdges(edges: MeshEdge[], selection: EdgeSelection): MeshEdge[];
|
|
45
|
+
/**
|
|
46
|
+
* Sample points along an edge for tube generation.
|
|
47
|
+
*/
|
|
48
|
+
export declare function sampleEdge(edge: MeshEdge, numSamples?: number): [number, number, number][];
|
|
49
|
+
/**
|
|
50
|
+
* Compute the edge direction (normalized).
|
|
51
|
+
*/
|
|
52
|
+
export declare function edgeDirection(edge: MeshEdge): [number, number, number];
|
|
53
|
+
/**
|
|
54
|
+
* Compute the "inward" direction perpendicular to the edge,
|
|
55
|
+
* pointing into the solid (average of face normals, negated).
|
|
56
|
+
*/
|
|
57
|
+
export declare function edgeInwardDirection(edge: MeshEdge): [number, number, number];
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edge selection types for fillet operations.
|
|
3
|
+
*
|
|
4
|
+
* Point-based selection: Find edges nearest to a 3D point
|
|
5
|
+
* Angle-based selection: Find all edges sharper than a threshold angle
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Extracts edges from a Manifold mesh with dihedral angle information.
|
|
9
|
+
* An edge is defined as a pair of vertices shared by exactly 2 triangles.
|
|
10
|
+
*/
|
|
11
|
+
export function extractEdges(mf) {
|
|
12
|
+
const mesh = mf.getMesh();
|
|
13
|
+
const { triVerts, vertProperties, numProp } = mesh;
|
|
14
|
+
const numTris = triVerts.length / 3;
|
|
15
|
+
// Build edge -> triangles map
|
|
16
|
+
// Key: "min_max" of vertex indices
|
|
17
|
+
const edgeToTris = new Map();
|
|
18
|
+
const getVertex = (idx) => {
|
|
19
|
+
const offset = idx * numProp;
|
|
20
|
+
return [
|
|
21
|
+
vertProperties[offset],
|
|
22
|
+
vertProperties[offset + 1],
|
|
23
|
+
vertProperties[offset + 2]
|
|
24
|
+
];
|
|
25
|
+
};
|
|
26
|
+
const computeNormal = (v0, v1, v2) => {
|
|
27
|
+
// Edge vectors
|
|
28
|
+
const e1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]];
|
|
29
|
+
const e2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]];
|
|
30
|
+
// Cross product
|
|
31
|
+
const n = [
|
|
32
|
+
e1[1] * e2[2] - e1[2] * e2[1],
|
|
33
|
+
e1[2] * e2[0] - e1[0] * e2[2],
|
|
34
|
+
e1[0] * e2[1] - e1[1] * e2[0]
|
|
35
|
+
];
|
|
36
|
+
// Normalize
|
|
37
|
+
const len = Math.sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
|
|
38
|
+
if (len > 1e-12) {
|
|
39
|
+
n[0] /= len;
|
|
40
|
+
n[1] /= len;
|
|
41
|
+
n[2] /= len;
|
|
42
|
+
}
|
|
43
|
+
return n;
|
|
44
|
+
};
|
|
45
|
+
// Process each triangle
|
|
46
|
+
for (let t = 0; t < numTris; t++) {
|
|
47
|
+
const i0 = triVerts[t * 3];
|
|
48
|
+
const i1 = triVerts[t * 3 + 1];
|
|
49
|
+
const i2 = triVerts[t * 3 + 2];
|
|
50
|
+
const v0 = getVertex(i0);
|
|
51
|
+
const v1 = getVertex(i1);
|
|
52
|
+
const v2 = getVertex(i2);
|
|
53
|
+
const normal = computeNormal(v0, v1, v2);
|
|
54
|
+
// Add each edge of this triangle
|
|
55
|
+
const edges = [
|
|
56
|
+
[i0, i1],
|
|
57
|
+
[i1, i2],
|
|
58
|
+
[i2, i0]
|
|
59
|
+
];
|
|
60
|
+
for (const [a, b] of edges) {
|
|
61
|
+
const key = a < b ? `${a}_${b}` : `${b}_${a}`;
|
|
62
|
+
if (!edgeToTris.has(key)) {
|
|
63
|
+
edgeToTris.set(key, []);
|
|
64
|
+
}
|
|
65
|
+
edgeToTris.get(key).push({ tri: t, normal });
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Extract edges that have exactly 2 adjacent triangles (manifold edges)
|
|
69
|
+
const meshEdges = [];
|
|
70
|
+
for (const [key, tris] of edgeToTris) {
|
|
71
|
+
if (tris.length !== 2)
|
|
72
|
+
continue; // Skip boundary or non-manifold edges
|
|
73
|
+
const [aStr, bStr] = key.split('_');
|
|
74
|
+
const v0 = parseInt(aStr);
|
|
75
|
+
const v1 = parseInt(bStr);
|
|
76
|
+
const p0 = getVertex(v0);
|
|
77
|
+
const p1 = getVertex(v1);
|
|
78
|
+
const n0 = tris[0].normal;
|
|
79
|
+
const n1 = tris[1].normal;
|
|
80
|
+
// Compute dihedral angle between face normals
|
|
81
|
+
const dot = n0[0] * n1[0] + n0[1] * n1[1] + n0[2] * n1[2];
|
|
82
|
+
const clampedDot = Math.max(-1, Math.min(1, dot));
|
|
83
|
+
const dihedralAngle = Math.acos(clampedDot) * (180 / Math.PI);
|
|
84
|
+
meshEdges.push({
|
|
85
|
+
v0,
|
|
86
|
+
v1,
|
|
87
|
+
p0,
|
|
88
|
+
p1,
|
|
89
|
+
n0,
|
|
90
|
+
n1,
|
|
91
|
+
dihedralAngle
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return meshEdges;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Selects edges based on selection criteria.
|
|
98
|
+
*/
|
|
99
|
+
export function selectEdges(edges, selection) {
|
|
100
|
+
if (selection.type === 'point') {
|
|
101
|
+
return selectByPoint(edges, selection);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
return selectByAngle(edges, selection);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Find the edge closest to a point.
|
|
109
|
+
*/
|
|
110
|
+
function selectByPoint(edges, selection) {
|
|
111
|
+
const [px, py, pz] = selection.point;
|
|
112
|
+
const maxDist = selection.maxDistance ?? Infinity;
|
|
113
|
+
let closestEdge = null;
|
|
114
|
+
let closestDist = Infinity;
|
|
115
|
+
for (const edge of edges) {
|
|
116
|
+
const dist = pointToSegmentDistance([px, py, pz], edge.p0, edge.p1);
|
|
117
|
+
if (dist < closestDist && dist <= maxDist) {
|
|
118
|
+
closestDist = dist;
|
|
119
|
+
closestEdge = edge;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return closestEdge ? [closestEdge] : [];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Find all edges sharper than a threshold angle.
|
|
126
|
+
* A dihedral angle of 180° is flat (faces are coplanar).
|
|
127
|
+
* A dihedral angle of 90° is a right-angle edge.
|
|
128
|
+
*
|
|
129
|
+
* @param minAngle Minimum angle to consider "sharp" (e.g., 80 means edges < 100° dihedral)
|
|
130
|
+
*/
|
|
131
|
+
function selectByAngle(edges, selection) {
|
|
132
|
+
// minAngle represents how sharp the edge is
|
|
133
|
+
// 180 - dihedralAngle gives us the "sharpness" angle
|
|
134
|
+
const threshold = 180 - selection.minAngle;
|
|
135
|
+
return edges.filter(edge => edge.dihedralAngle <= threshold);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Compute distance from point to line segment.
|
|
139
|
+
*/
|
|
140
|
+
function pointToSegmentDistance(p, a, b) {
|
|
141
|
+
const ab = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
|
|
142
|
+
const ap = [p[0] - a[0], p[1] - a[1], p[2] - a[2]];
|
|
143
|
+
const abLenSq = ab[0] * ab[0] + ab[1] * ab[1] + ab[2] * ab[2];
|
|
144
|
+
if (abLenSq < 1e-12) {
|
|
145
|
+
// Degenerate segment
|
|
146
|
+
return Math.sqrt(ap[0] * ap[0] + ap[1] * ap[1] + ap[2] * ap[2]);
|
|
147
|
+
}
|
|
148
|
+
// Project point onto line, clamped to segment
|
|
149
|
+
const t = Math.max(0, Math.min(1, (ap[0] * ab[0] + ap[1] * ab[1] + ap[2] * ab[2]) / abLenSq));
|
|
150
|
+
// Closest point on segment
|
|
151
|
+
const closest = [
|
|
152
|
+
a[0] + t * ab[0],
|
|
153
|
+
a[1] + t * ab[1],
|
|
154
|
+
a[2] + t * ab[2]
|
|
155
|
+
];
|
|
156
|
+
// Distance
|
|
157
|
+
const dx = p[0] - closest[0];
|
|
158
|
+
const dy = p[1] - closest[1];
|
|
159
|
+
const dz = p[2] - closest[2];
|
|
160
|
+
return Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Sample points along an edge for tube generation.
|
|
164
|
+
*/
|
|
165
|
+
export function sampleEdge(edge, numSamples = 10) {
|
|
166
|
+
const samples = [];
|
|
167
|
+
for (let i = 0; i <= numSamples; i++) {
|
|
168
|
+
const t = i / numSamples;
|
|
169
|
+
samples.push([
|
|
170
|
+
edge.p0[0] + t * (edge.p1[0] - edge.p0[0]),
|
|
171
|
+
edge.p0[1] + t * (edge.p1[1] - edge.p0[1]),
|
|
172
|
+
edge.p0[2] + t * (edge.p1[2] - edge.p0[2])
|
|
173
|
+
]);
|
|
174
|
+
}
|
|
175
|
+
return samples;
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Compute the edge direction (normalized).
|
|
179
|
+
*/
|
|
180
|
+
export function edgeDirection(edge) {
|
|
181
|
+
const dx = edge.p1[0] - edge.p0[0];
|
|
182
|
+
const dy = edge.p1[1] - edge.p0[1];
|
|
183
|
+
const dz = edge.p1[2] - edge.p0[2];
|
|
184
|
+
const len = Math.sqrt(dx * dx + dy * dy + dz * dz);
|
|
185
|
+
if (len < 1e-12)
|
|
186
|
+
return [1, 0, 0];
|
|
187
|
+
return [dx / len, dy / len, dz / len];
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Compute the "inward" direction perpendicular to the edge,
|
|
191
|
+
* pointing into the solid (average of face normals, negated).
|
|
192
|
+
*/
|
|
193
|
+
export function edgeInwardDirection(edge) {
|
|
194
|
+
// Average of face normals points "outward" from the edge
|
|
195
|
+
// For fillet, we want direction toward the solid interior
|
|
196
|
+
const n = [
|
|
197
|
+
(edge.n0[0] + edge.n1[0]) / 2,
|
|
198
|
+
(edge.n0[1] + edge.n1[1]) / 2,
|
|
199
|
+
(edge.n0[2] + edge.n1[2]) / 2
|
|
200
|
+
];
|
|
201
|
+
const len = Math.sqrt(n[0] * n[0] + n[1] * n[1] + n[2] * n[2]);
|
|
202
|
+
if (len < 1e-12)
|
|
203
|
+
return [0, 0, 1];
|
|
204
|
+
// Negate to point inward
|
|
205
|
+
return [-n[0] / len, -n[1] / len, -n[2] / len];
|
|
206
|
+
}
|
package/dist/example.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example usage of the fillet library in CADit environment.
|
|
3
|
+
*
|
|
4
|
+
* In CADit, `manifold` is available as a global variable containing
|
|
5
|
+
* the initialized Manifold WASM module.
|
|
6
|
+
*/
|
|
7
|
+
import { Manifold } from 'manifold-3d/manifoldCAD';
|
|
8
|
+
import { createFillet } from './index';
|
|
9
|
+
// Create fillet API bound to the manifold instance
|
|
10
|
+
const { fillet } = createFillet(manifold);
|
|
11
|
+
const box = Manifold.cube([20, 20, 20], true);
|
|
12
|
+
// Fillet the edge at position (10, 10, 0)
|
|
13
|
+
const rounded = fillet(box, {
|
|
14
|
+
radius: 2,
|
|
15
|
+
selection: {
|
|
16
|
+
type: 'point',
|
|
17
|
+
point: [10, 10, 0],
|
|
18
|
+
maxDistance: 5 // optional: limit search radius
|
|
19
|
+
},
|
|
20
|
+
segments: 50
|
|
21
|
+
});
|
|
22
|
+
const box1 = Manifold.cube([20, 20, 20], true);
|
|
23
|
+
// Fillet all sharp edges (dihedral angle < 100°)
|
|
24
|
+
const fullyRounded = fillet(box1, {
|
|
25
|
+
radius: 2,
|
|
26
|
+
selection: {
|
|
27
|
+
type: 'angle',
|
|
28
|
+
minAngle: 80 // edges sharper than 80° from flat
|
|
29
|
+
},
|
|
30
|
+
segments: 50
|
|
31
|
+
})
|
|
32
|
+
.translate([30, 0, 0]);
|
|
33
|
+
export default [rounded, fullyRounded];
|
package/dist/fillet.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fillet/Round operations for Manifold meshes.
|
|
3
|
+
*
|
|
4
|
+
* Implements the "dirty nasty (but working)" CSG approach from:
|
|
5
|
+
* https://github.com/elalish/manifold/discussions/1411
|
|
6
|
+
*
|
|
7
|
+
* Algorithm:
|
|
8
|
+
* 1. Create a tube (cylinder) tangent to the inner faces of the corner
|
|
9
|
+
* 2. Create a wedge (prism) that covers the corner tip but fits inside the tube's "back" side
|
|
10
|
+
* 3. Subtract tube from wedge → cutting tool (just the corner tip)
|
|
11
|
+
* 4. Subtract cutting tool from original → rounded edge
|
|
12
|
+
*/
|
|
13
|
+
import type { Manifold, ManifoldToplevel } from 'manifold-3d';
|
|
14
|
+
import { EdgeSelection } from './edgeSelection';
|
|
15
|
+
export interface FilletOptions {
|
|
16
|
+
/** Fillet radius */
|
|
17
|
+
radius: number;
|
|
18
|
+
/** Edge selection criteria */
|
|
19
|
+
selection: EdgeSelection;
|
|
20
|
+
/** Number of segments for circular profiles (default: 16) */
|
|
21
|
+
segments?: number;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Apply fillet/round to selected edges of a Manifold.
|
|
25
|
+
*
|
|
26
|
+
* @param manifold - The initialized Manifold WASM module
|
|
27
|
+
* @param mf - The Manifold shape to fillet
|
|
28
|
+
* @param options - Fillet options (radius, selection, segments)
|
|
29
|
+
* @returns A new Manifold with filleted edges
|
|
30
|
+
*/
|
|
31
|
+
export declare function filletWithManifold(manifold: ManifoldToplevel, mf: Manifold, options: FilletOptions): Manifold;
|
|
32
|
+
export type { EdgeSelection, PointEdgeSelection, AngleEdgeSelection } from './edgeSelection';
|
package/dist/fillet.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fillet/Round operations for Manifold meshes.
|
|
3
|
+
*
|
|
4
|
+
* Implements the "dirty nasty (but working)" CSG approach from:
|
|
5
|
+
* https://github.com/elalish/manifold/discussions/1411
|
|
6
|
+
*
|
|
7
|
+
* Algorithm:
|
|
8
|
+
* 1. Create a tube (cylinder) tangent to the inner faces of the corner
|
|
9
|
+
* 2. Create a wedge (prism) that covers the corner tip but fits inside the tube's "back" side
|
|
10
|
+
* 3. Subtract tube from wedge → cutting tool (just the corner tip)
|
|
11
|
+
* 4. Subtract cutting tool from original → rounded edge
|
|
12
|
+
*/
|
|
13
|
+
import { extractEdges, selectEdges } from './edgeSelection';
|
|
14
|
+
/**
|
|
15
|
+
* Apply fillet/round to selected edges of a Manifold.
|
|
16
|
+
*
|
|
17
|
+
* @param manifold - The initialized Manifold WASM module
|
|
18
|
+
* @param mf - The Manifold shape to fillet
|
|
19
|
+
* @param options - Fillet options (radius, selection, segments)
|
|
20
|
+
* @returns A new Manifold with filleted edges
|
|
21
|
+
*/
|
|
22
|
+
export function filletWithManifold(manifold, mf, options) {
|
|
23
|
+
const { radius, selection, segments = 16 } = options;
|
|
24
|
+
if (radius <= 0) {
|
|
25
|
+
throw new Error('Fillet radius must be positive');
|
|
26
|
+
}
|
|
27
|
+
// Extract all edges from mesh
|
|
28
|
+
const allEdges = extractEdges(mf);
|
|
29
|
+
if (allEdges.length === 0) {
|
|
30
|
+
console.warn('fillet: No edges found in mesh');
|
|
31
|
+
return mf;
|
|
32
|
+
}
|
|
33
|
+
// Select edges based on criteria
|
|
34
|
+
const selectedEdges = selectEdges(allEdges, selection);
|
|
35
|
+
if (selectedEdges.length === 0) {
|
|
36
|
+
console.warn('fillet: No edges matched selection criteria');
|
|
37
|
+
return mf;
|
|
38
|
+
}
|
|
39
|
+
console.log('fillet: Found', selectedEdges.length, 'edges to fillet');
|
|
40
|
+
let result = mf;
|
|
41
|
+
// Process convex edges
|
|
42
|
+
// We can union all tools for a single subtraction if they don't overlap
|
|
43
|
+
const cuttingTools = [];
|
|
44
|
+
for (const edge of selectedEdges) {
|
|
45
|
+
// Only handling convex edges (standard fillets)
|
|
46
|
+
// Filter out flat edges (dihedral angle approx 0) which are just triangulation artifacts
|
|
47
|
+
if (edge.dihedralAngle > 5 && edge.dihedralAngle < 175) {
|
|
48
|
+
const tool = createFilletCuttingTool(manifold, edge, radius, segments);
|
|
49
|
+
if (tool && tool.volume() > 1e-9) {
|
|
50
|
+
cuttingTools.push(tool);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Subtract tools sequentially to avoid memory spikes from large unions
|
|
55
|
+
if (cuttingTools.length > 0) {
|
|
56
|
+
// Sort by volume or position might help stability, but just sequential is fine for now
|
|
57
|
+
console.log(`fillet: Subtracting ${cuttingTools.length} tools sequentially`);
|
|
58
|
+
for (const tool of cuttingTools) {
|
|
59
|
+
result = result.subtract(tool);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return result;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Create a fillet cutting tool using the wedge-minus-tube approach.
|
|
66
|
+
*/
|
|
67
|
+
function createFilletCuttingTool(manifold, edge, radius, segments) {
|
|
68
|
+
// Edge endpoints
|
|
69
|
+
const [x0, y0, z0] = edge.p0;
|
|
70
|
+
const [x1, y1, z1] = edge.p1;
|
|
71
|
+
const ex = x1 - x0, ey = y1 - y0, ez = z1 - z0;
|
|
72
|
+
const edgeLen = Math.sqrt(ex * ex + ey * ey + ez * ez);
|
|
73
|
+
if (edgeLen < 1e-9)
|
|
74
|
+
return null;
|
|
75
|
+
const edgeDir = [ex / edgeLen, ey / edgeLen, ez / edgeLen];
|
|
76
|
+
const [n0x, n0y, n0z] = edge.n0;
|
|
77
|
+
const [n1x, n1y, n1z] = edge.n1;
|
|
78
|
+
// 1. TUBE: Positioned INWARD (tangent to faces)
|
|
79
|
+
// Center = Edge - radius*n0 - radius*n1 (for 90 degree edges)
|
|
80
|
+
// Note: For non-90 degree, this approximation works for small fillets but
|
|
81
|
+
// true bisector logic should be used. Using the sum offset for now as it handles 90 deg.
|
|
82
|
+
const ext = radius * 0.1;
|
|
83
|
+
const tubeStart = [
|
|
84
|
+
x0 - n0x * radius - n1x * radius - edgeDir[0] * ext,
|
|
85
|
+
y0 - n0y * radius - n1y * radius - edgeDir[1] * ext,
|
|
86
|
+
z0 - n0z * radius - n1z * radius - edgeDir[2] * ext
|
|
87
|
+
];
|
|
88
|
+
const tubeEnd = [
|
|
89
|
+
x1 - n0x * radius - n1x * radius + edgeDir[0] * ext,
|
|
90
|
+
y1 - n0y * radius - n1y * radius + edgeDir[1] * ext,
|
|
91
|
+
z1 - n0z * radius - n1z * radius + edgeDir[2] * ext
|
|
92
|
+
];
|
|
93
|
+
const sphere0 = manifold.Manifold.sphere(radius, segments).translate(tubeStart);
|
|
94
|
+
const sphere1 = manifold.Manifold.sphere(radius, segments).translate(tubeEnd);
|
|
95
|
+
const tube = manifold.Manifold.hull([sphere0, sphere1]);
|
|
96
|
+
// 2. WEDGE: Triangular prism extending INWARD
|
|
97
|
+
// CRITICAL: The wedge depth must be limited so it is completely contained
|
|
98
|
+
// within the tube on the "back" side. If deeper than the tube, subtracting
|
|
99
|
+
// the tube leaves "internal debris" which carves holes in the solid.
|
|
100
|
+
// For 90 degree corners, radius*1.0 is safe (reaches tube center).
|
|
101
|
+
const wedgeDepth = radius * 1.1;
|
|
102
|
+
const wedgePoints = [];
|
|
103
|
+
// At start
|
|
104
|
+
const sX = x0 - edgeDir[0] * ext, sY = y0 - edgeDir[1] * ext, sZ = z0 - edgeDir[2] * ext;
|
|
105
|
+
wedgePoints.push([sX, sY, sZ]); // Edge
|
|
106
|
+
wedgePoints.push([sX - n0x * wedgeDepth, sY - n0y * wedgeDepth, sZ - n0z * wedgeDepth]);
|
|
107
|
+
wedgePoints.push([sX - n1x * wedgeDepth, sY - n1y * wedgeDepth, sZ - n1z * wedgeDepth]);
|
|
108
|
+
// At end
|
|
109
|
+
const eX = x1 + edgeDir[0] * ext, eY = y1 + edgeDir[1] * ext, eZ = z1 + edgeDir[2] * ext;
|
|
110
|
+
wedgePoints.push([eX, eY, eZ]); // Edge
|
|
111
|
+
wedgePoints.push([eX - n0x * wedgeDepth, eY - n0y * wedgeDepth, eZ - n0z * wedgeDepth]);
|
|
112
|
+
wedgePoints.push([eX - n1x * wedgeDepth, eY - n1y * wedgeDepth, eZ - n1z * wedgeDepth]);
|
|
113
|
+
const wedge = manifold.Manifold.hull(wedgePoints);
|
|
114
|
+
// 3. CUTTING TOOL: Wedge - Tube
|
|
115
|
+
// Since wedge is small, wedge - tube leaves only the corner tip.
|
|
116
|
+
return wedge.subtract(tube);
|
|
117
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brute-force fillet library for Manifold meshes.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import ManifoldModule from 'manifold-3d';
|
|
7
|
+
* import { createFillet } from '@cadit-app/brute-force-fillet';
|
|
8
|
+
*
|
|
9
|
+
* const manifold = await ManifoldModule();
|
|
10
|
+
* const { fillet } = createFillet(manifold);
|
|
11
|
+
*
|
|
12
|
+
* const box = manifold.Manifold.cube([10, 10, 10], true);
|
|
13
|
+
* const rounded = fillet(box, { radius: 1, selection: { type: 'angle', minAngle: 80 } });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import type { Manifold, ManifoldToplevel } from 'manifold-3d';
|
|
17
|
+
import { FilletOptions } from './fillet';
|
|
18
|
+
import { extractEdges, selectEdges, edgeDirection, edgeInwardDirection, sampleEdge, MeshEdge } from './edgeSelection';
|
|
19
|
+
export type { ManifoldToplevel } from './context';
|
|
20
|
+
export type { FilletOptions } from './fillet';
|
|
21
|
+
export type { MeshEdge, EdgeSelection, PointEdgeSelection, AngleEdgeSelection } from './edgeSelection';
|
|
22
|
+
/**
|
|
23
|
+
* The fillet API returned by createFillet.
|
|
24
|
+
*/
|
|
25
|
+
export interface FilletAPI {
|
|
26
|
+
/**
|
|
27
|
+
* Apply fillet/round to selected edges of a Manifold.
|
|
28
|
+
*
|
|
29
|
+
* @param mf - The Manifold shape to fillet
|
|
30
|
+
* @param options - Fillet options (radius, selection, segments)
|
|
31
|
+
* @returns A new Manifold with filleted edges
|
|
32
|
+
*/
|
|
33
|
+
fillet: (mf: Manifold, options: FilletOptions) => Manifold;
|
|
34
|
+
/**
|
|
35
|
+
* Creates a tube along a polyline path using convex hulls of spheres.
|
|
36
|
+
*/
|
|
37
|
+
pipeAlongPath: (path: [number, number, number][], radius: number, segments?: number) => Manifold;
|
|
38
|
+
/**
|
|
39
|
+
* Creates a tube along an edge path with extended endpoints.
|
|
40
|
+
*/
|
|
41
|
+
pipeAlongPathExtended: (path: [number, number, number][], radius: number, extensionFactor?: number, segments?: number) => Manifold;
|
|
42
|
+
/**
|
|
43
|
+
* Creates a wedge solid along an edge.
|
|
44
|
+
*/
|
|
45
|
+
buildWedge: (edge: MeshEdge, distance: number, inflate?: number) => Manifold;
|
|
46
|
+
/**
|
|
47
|
+
* Extract edges from a Manifold mesh.
|
|
48
|
+
*/
|
|
49
|
+
extractEdges: typeof extractEdges;
|
|
50
|
+
/**
|
|
51
|
+
* Select edges based on criteria.
|
|
52
|
+
*/
|
|
53
|
+
selectEdges: typeof selectEdges;
|
|
54
|
+
/**
|
|
55
|
+
* Compute edge direction (normalized).
|
|
56
|
+
*/
|
|
57
|
+
edgeDirection: typeof edgeDirection;
|
|
58
|
+
/**
|
|
59
|
+
* Compute inward direction perpendicular to edge.
|
|
60
|
+
*/
|
|
61
|
+
edgeInwardDirection: typeof edgeInwardDirection;
|
|
62
|
+
/**
|
|
63
|
+
* Sample points along an edge.
|
|
64
|
+
*/
|
|
65
|
+
sampleEdge: typeof sampleEdge;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Creates a fillet API bound to a specific Manifold instance.
|
|
69
|
+
*
|
|
70
|
+
* @param manifold - The initialized Manifold WASM module (from `await ManifoldModule()`)
|
|
71
|
+
* @returns An object containing all fillet functions bound to the manifold instance
|
|
72
|
+
*
|
|
73
|
+
* @example
|
|
74
|
+
* ```typescript
|
|
75
|
+
* import ManifoldModule from 'manifold-3d';
|
|
76
|
+
* import { createFillet } from '@cadit-app/brute-force-fillet';
|
|
77
|
+
*
|
|
78
|
+
* const manifold = await ManifoldModule();
|
|
79
|
+
* const { fillet } = createFillet(manifold);
|
|
80
|
+
*
|
|
81
|
+
* const box = manifold.Manifold.cube([10, 10, 10], true);
|
|
82
|
+
* const rounded = fillet(box, {
|
|
83
|
+
* radius: 1,
|
|
84
|
+
* selection: { type: 'angle', minAngle: 80 }
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export declare function createFillet(manifold: ManifoldToplevel): FilletAPI;
|
|
89
|
+
export { extractEdges, selectEdges, edgeDirection, edgeInwardDirection, sampleEdge };
|
|
90
|
+
export { filletWithManifold } from './fillet';
|
|
91
|
+
export { pipeAlongPath, pipeAlongPathExtended } from './pipeAlongPath';
|
|
92
|
+
export { buildWedge } from './wedgeBuilder';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Brute-force fillet library for Manifold meshes.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```typescript
|
|
6
|
+
* import ManifoldModule from 'manifold-3d';
|
|
7
|
+
* import { createFillet } from '@cadit-app/brute-force-fillet';
|
|
8
|
+
*
|
|
9
|
+
* const manifold = await ManifoldModule();
|
|
10
|
+
* const { fillet } = createFillet(manifold);
|
|
11
|
+
*
|
|
12
|
+
* const box = manifold.Manifold.cube([10, 10, 10], true);
|
|
13
|
+
* const rounded = fillet(box, { radius: 1, selection: { type: 'angle', minAngle: 80 } });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
import { filletWithManifold } from './fillet';
|
|
17
|
+
import { pipeAlongPath, pipeAlongPathExtended } from './pipeAlongPath';
|
|
18
|
+
import { buildWedge } from './wedgeBuilder';
|
|
19
|
+
import { extractEdges, selectEdges, edgeDirection, edgeInwardDirection, sampleEdge } from './edgeSelection';
|
|
20
|
+
/**
|
|
21
|
+
* Creates a fillet API bound to a specific Manifold instance.
|
|
22
|
+
*
|
|
23
|
+
* @param manifold - The initialized Manifold WASM module (from `await ManifoldModule()`)
|
|
24
|
+
* @returns An object containing all fillet functions bound to the manifold instance
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```typescript
|
|
28
|
+
* import ManifoldModule from 'manifold-3d';
|
|
29
|
+
* import { createFillet } from '@cadit-app/brute-force-fillet';
|
|
30
|
+
*
|
|
31
|
+
* const manifold = await ManifoldModule();
|
|
32
|
+
* const { fillet } = createFillet(manifold);
|
|
33
|
+
*
|
|
34
|
+
* const box = manifold.Manifold.cube([10, 10, 10], true);
|
|
35
|
+
* const rounded = fillet(box, {
|
|
36
|
+
* radius: 1,
|
|
37
|
+
* selection: { type: 'angle', minAngle: 80 }
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
export function createFillet(manifold) {
|
|
42
|
+
return {
|
|
43
|
+
fillet: (mf, options) => filletWithManifold(manifold, mf, options),
|
|
44
|
+
pipeAlongPath: (path, radius, segments) => pipeAlongPath(manifold, path, radius, segments),
|
|
45
|
+
pipeAlongPathExtended: (path, radius, extensionFactor, segments) => pipeAlongPathExtended(manifold, path, radius, extensionFactor, segments),
|
|
46
|
+
buildWedge: (edge, distance, inflate) => buildWedge(manifold, edge, distance, inflate),
|
|
47
|
+
// These don't need manifold instance
|
|
48
|
+
extractEdges,
|
|
49
|
+
selectEdges,
|
|
50
|
+
edgeDirection,
|
|
51
|
+
edgeInwardDirection,
|
|
52
|
+
sampleEdge,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
// Re-export utility functions that don't need manifold instance
|
|
56
|
+
export { extractEdges, selectEdges, edgeDirection, edgeInwardDirection, sampleEdge };
|
|
57
|
+
// Export the raw function for advanced use cases
|
|
58
|
+
export { filletWithManifold } from './fillet';
|
|
59
|
+
export { pipeAlongPath, pipeAlongPathExtended } from './pipeAlongPath';
|
|
60
|
+
export { buildWedge } from './wedgeBuilder';
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a tube (pipe) along a path using hull of spheres.
|
|
3
|
+
* This approach is reliable for curved paths and produces
|
|
4
|
+
* smooth results with proper manifold topology.
|
|
5
|
+
*/
|
|
6
|
+
import type { Manifold, ManifoldToplevel } from 'manifold-3d';
|
|
7
|
+
/**
|
|
8
|
+
* Creates a tube along a polyline path using convex hulls of spheres.
|
|
9
|
+
*
|
|
10
|
+
* @param manifold - The initialized Manifold WASM module
|
|
11
|
+
* @param path Array of 3D points defining the path
|
|
12
|
+
* @param radius Radius of the tube
|
|
13
|
+
* @param segments Optional circular segments for sphere quality
|
|
14
|
+
* @returns Manifold representing the tube
|
|
15
|
+
*/
|
|
16
|
+
export declare function pipeAlongPath(manifold: ManifoldToplevel, path: [number, number, number][], radius: number, segments?: number): Manifold;
|
|
17
|
+
/**
|
|
18
|
+
* Creates a tube along an edge path that extends slightly beyond
|
|
19
|
+
* the endpoints to ensure proper boolean overlap.
|
|
20
|
+
*
|
|
21
|
+
* @param manifold - The initialized Manifold WASM module
|
|
22
|
+
* @param path Path points
|
|
23
|
+
* @param radius Tube radius
|
|
24
|
+
* @param extensionFactor How much to extend beyond endpoints (as fraction of radius)
|
|
25
|
+
* @param segments Circular segments
|
|
26
|
+
*/
|
|
27
|
+
export declare function pipeAlongPathExtended(manifold: ManifoldToplevel, path: [number, number, number][], radius: number, extensionFactor?: number, segments?: number): Manifold;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generates a tube (pipe) along a path using hull of spheres.
|
|
3
|
+
* This approach is reliable for curved paths and produces
|
|
4
|
+
* smooth results with proper manifold topology.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Creates a tube along a polyline path using convex hulls of spheres.
|
|
8
|
+
*
|
|
9
|
+
* @param manifold - The initialized Manifold WASM module
|
|
10
|
+
* @param path Array of 3D points defining the path
|
|
11
|
+
* @param radius Radius of the tube
|
|
12
|
+
* @param segments Optional circular segments for sphere quality
|
|
13
|
+
* @returns Manifold representing the tube
|
|
14
|
+
*/
|
|
15
|
+
export function pipeAlongPath(manifold, path, radius, segments) {
|
|
16
|
+
if (path.length < 2) {
|
|
17
|
+
throw new Error('Path must have at least 2 points');
|
|
18
|
+
}
|
|
19
|
+
// Create spheres at each path point
|
|
20
|
+
const spheres = [];
|
|
21
|
+
for (const [x, y, z] of path) {
|
|
22
|
+
const sphere = manifold.Manifold.sphere(radius, segments)
|
|
23
|
+
.translate([x, y, z]);
|
|
24
|
+
spheres.push(sphere);
|
|
25
|
+
}
|
|
26
|
+
// Hull adjacent spheres to create tube segments, then union
|
|
27
|
+
const segments_ = [];
|
|
28
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
29
|
+
// Hull two adjacent spheres to form a tube segment
|
|
30
|
+
const segment = manifold.Manifold.hull([spheres[i], spheres[i + 1]]);
|
|
31
|
+
segments_.push(segment);
|
|
32
|
+
}
|
|
33
|
+
// Union all segments
|
|
34
|
+
if (segments_.length === 1) {
|
|
35
|
+
return segments_[0];
|
|
36
|
+
}
|
|
37
|
+
return manifold.Manifold.union(segments_);
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Creates a tube along an edge path that extends slightly beyond
|
|
41
|
+
* the endpoints to ensure proper boolean overlap.
|
|
42
|
+
*
|
|
43
|
+
* @param manifold - The initialized Manifold WASM module
|
|
44
|
+
* @param path Path points
|
|
45
|
+
* @param radius Tube radius
|
|
46
|
+
* @param extensionFactor How much to extend beyond endpoints (as fraction of radius)
|
|
47
|
+
* @param segments Circular segments
|
|
48
|
+
*/
|
|
49
|
+
export function pipeAlongPathExtended(manifold, path, radius, extensionFactor = 0.1, segments) {
|
|
50
|
+
if (path.length < 2) {
|
|
51
|
+
throw new Error('Path must have at least 2 points');
|
|
52
|
+
}
|
|
53
|
+
// Compute extension vectors at start and end
|
|
54
|
+
const start = path[0];
|
|
55
|
+
const afterStart = path[1];
|
|
56
|
+
const startDir = normalize([
|
|
57
|
+
start[0] - afterStart[0],
|
|
58
|
+
start[1] - afterStart[1],
|
|
59
|
+
start[2] - afterStart[2]
|
|
60
|
+
]);
|
|
61
|
+
const end = path[path.length - 1];
|
|
62
|
+
const beforeEnd = path[path.length - 2];
|
|
63
|
+
const endDir = normalize([
|
|
64
|
+
end[0] - beforeEnd[0],
|
|
65
|
+
end[1] - beforeEnd[1],
|
|
66
|
+
end[2] - beforeEnd[2]
|
|
67
|
+
]);
|
|
68
|
+
const ext = radius * extensionFactor;
|
|
69
|
+
// Create extended path
|
|
70
|
+
const extendedPath = [
|
|
71
|
+
[start[0] + startDir[0] * ext, start[1] + startDir[1] * ext, start[2] + startDir[2] * ext],
|
|
72
|
+
...path,
|
|
73
|
+
[end[0] + endDir[0] * ext, end[1] + endDir[1] * ext, end[2] + endDir[2] * ext]
|
|
74
|
+
];
|
|
75
|
+
return pipeAlongPath(manifold, extendedPath, radius, segments);
|
|
76
|
+
}
|
|
77
|
+
function normalize(v) {
|
|
78
|
+
const len = Math.sqrt(v[0] * v[0] + v[1] * v[1] + v[2] * v[2]);
|
|
79
|
+
if (len < 1e-12)
|
|
80
|
+
return [1, 0, 0];
|
|
81
|
+
return [v[0] / len, v[1] / len, v[2] / len];
|
|
82
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a wedge solid along an edge for fillet operations.
|
|
3
|
+
*
|
|
4
|
+
* The wedge is formed by offsetting from the edge along the
|
|
5
|
+
* adjacent face normals. This creates a triangular prism that,
|
|
6
|
+
* when the tube is subtracted, leaves the fillet surface.
|
|
7
|
+
*/
|
|
8
|
+
import type { Manifold, ManifoldToplevel } from 'manifold-3d';
|
|
9
|
+
import { MeshEdge } from './edgeSelection';
|
|
10
|
+
/**
|
|
11
|
+
* Creates a wedge solid along an edge.
|
|
12
|
+
*
|
|
13
|
+
* The wedge spans:
|
|
14
|
+
* - Along the edge direction (with small extension)
|
|
15
|
+
* - From the edge outward along both face normals
|
|
16
|
+
*
|
|
17
|
+
* @param manifold - The initialized Manifold WASM module
|
|
18
|
+
* @param edge The edge to build wedge for
|
|
19
|
+
* @param distance How far to offset along face normals
|
|
20
|
+
* @param inflate Extra inflation for boolean overlap (typically 2× precision)
|
|
21
|
+
* @returns Manifold representing the wedge
|
|
22
|
+
*/
|
|
23
|
+
export declare function buildWedge(manifold: ManifoldToplevel, edge: MeshEdge, distance: number, inflate?: number): Manifold;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds a wedge solid along an edge for fillet operations.
|
|
3
|
+
*
|
|
4
|
+
* The wedge is formed by offsetting from the edge along the
|
|
5
|
+
* adjacent face normals. This creates a triangular prism that,
|
|
6
|
+
* when the tube is subtracted, leaves the fillet surface.
|
|
7
|
+
*/
|
|
8
|
+
import { edgeDirection } from './edgeSelection';
|
|
9
|
+
/**
|
|
10
|
+
* Creates a wedge solid along an edge.
|
|
11
|
+
*
|
|
12
|
+
* The wedge spans:
|
|
13
|
+
* - Along the edge direction (with small extension)
|
|
14
|
+
* - From the edge outward along both face normals
|
|
15
|
+
*
|
|
16
|
+
* @param manifold - The initialized Manifold WASM module
|
|
17
|
+
* @param edge The edge to build wedge for
|
|
18
|
+
* @param distance How far to offset along face normals
|
|
19
|
+
* @param inflate Extra inflation for boolean overlap (typically 2× precision)
|
|
20
|
+
* @returns Manifold representing the wedge
|
|
21
|
+
*/
|
|
22
|
+
export function buildWedge(manifold, edge, distance, inflate = 0.001) {
|
|
23
|
+
// Edge vector and direction
|
|
24
|
+
const edgeVec = [
|
|
25
|
+
edge.p1[0] - edge.p0[0],
|
|
26
|
+
edge.p1[1] - edge.p0[1],
|
|
27
|
+
edge.p1[2] - edge.p0[2]
|
|
28
|
+
];
|
|
29
|
+
const dir = edgeDirection(edge);
|
|
30
|
+
// Compute offset directions perpendicular to edge, along face normals
|
|
31
|
+
// We need vectors in the plane of each face, perpendicular to the edge
|
|
32
|
+
const offsetA = crossAndNormalize(edge.n0, dir);
|
|
33
|
+
const offsetB = crossAndNormalize(edge.n1, dir);
|
|
34
|
+
// Ensure offset directions point "outward" from the edge (same side as normals)
|
|
35
|
+
// This aligns them with how the fillet should remove material
|
|
36
|
+
const dotA = dot(offsetA, edge.n0);
|
|
37
|
+
const dotB = dot(offsetB, edge.n1);
|
|
38
|
+
if (dotA < 0) {
|
|
39
|
+
offsetA[0] = -offsetA[0];
|
|
40
|
+
offsetA[1] = -offsetA[1];
|
|
41
|
+
offsetA[2] = -offsetA[2];
|
|
42
|
+
}
|
|
43
|
+
if (dotB < 0) {
|
|
44
|
+
offsetB[0] = -offsetB[0];
|
|
45
|
+
offsetB[1] = -offsetB[1];
|
|
46
|
+
offsetB[2] = -offsetB[2];
|
|
47
|
+
}
|
|
48
|
+
// Create wedge vertices
|
|
49
|
+
// The wedge is a triangular prism along the edge
|
|
50
|
+
const d = distance + inflate;
|
|
51
|
+
const ext = inflate; // Small extension along edge
|
|
52
|
+
// Start cap vertices (at p0 - ext along edge)
|
|
53
|
+
const s0 = [
|
|
54
|
+
edge.p0[0] - dir[0] * ext,
|
|
55
|
+
edge.p0[1] - dir[1] * ext,
|
|
56
|
+
edge.p0[2] - dir[2] * ext
|
|
57
|
+
];
|
|
58
|
+
const sA = [
|
|
59
|
+
s0[0] + offsetA[0] * d,
|
|
60
|
+
s0[1] + offsetA[1] * d,
|
|
61
|
+
s0[2] + offsetA[2] * d
|
|
62
|
+
];
|
|
63
|
+
const sB = [
|
|
64
|
+
s0[0] + offsetB[0] * d,
|
|
65
|
+
s0[1] + offsetB[1] * d,
|
|
66
|
+
s0[2] + offsetB[2] * d
|
|
67
|
+
];
|
|
68
|
+
// End cap vertices (at p1 + ext along edge)
|
|
69
|
+
const e0 = [
|
|
70
|
+
edge.p1[0] + dir[0] * ext,
|
|
71
|
+
edge.p1[1] + dir[1] * ext,
|
|
72
|
+
edge.p1[2] + dir[2] * ext
|
|
73
|
+
];
|
|
74
|
+
const eA = [
|
|
75
|
+
e0[0] + offsetA[0] * d,
|
|
76
|
+
e0[1] + offsetA[1] * d,
|
|
77
|
+
e0[2] + offsetA[2] * d
|
|
78
|
+
];
|
|
79
|
+
const eB = [
|
|
80
|
+
e0[0] + offsetB[0] * d,
|
|
81
|
+
e0[1] + offsetB[1] * d,
|
|
82
|
+
e0[2] + offsetB[2] * d
|
|
83
|
+
];
|
|
84
|
+
// Build the wedge as a convex hull of these 6 points
|
|
85
|
+
// This guarantees a valid manifold
|
|
86
|
+
const points = [s0, sA, sB, e0, eA, eB];
|
|
87
|
+
return manifold.Manifold.hull(points);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Cross product of two vectors, normalized.
|
|
91
|
+
*/
|
|
92
|
+
function crossAndNormalize(a, b) {
|
|
93
|
+
const c = [
|
|
94
|
+
a[1] * b[2] - a[2] * b[1],
|
|
95
|
+
a[2] * b[0] - a[0] * b[2],
|
|
96
|
+
a[0] * b[1] - a[1] * b[0]
|
|
97
|
+
];
|
|
98
|
+
const len = Math.sqrt(c[0] ** 2 + c[1] ** 2 + c[2] ** 2);
|
|
99
|
+
if (len < 1e-12)
|
|
100
|
+
return [0, 0, 1];
|
|
101
|
+
return [c[0] / len, c[1] / len, c[2] / len];
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Dot product of two vectors.
|
|
105
|
+
*/
|
|
106
|
+
function dot(a, b) {
|
|
107
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
108
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cadit-app/manifold-fillet",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Fillet and round operations for Manifold meshes",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
},
|
|
13
|
+
"./*": {
|
|
14
|
+
"import": "./dist/*.js",
|
|
15
|
+
"types": "./dist/*.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "bash -c 'output=$(tsc --noCheck 2>&1); echo \"$output\"; echo \"$output\" | grep -v \"error TS2742\" | grep -q \"error TS\" && exit 1 || exit 0'",
|
|
20
|
+
"dev": "tsup src/index.ts --format cjs,esm --dts --watch",
|
|
21
|
+
"lint": "eslint src",
|
|
22
|
+
"build:3mf": "manifold-cad index.ts output.3mf",
|
|
23
|
+
"build:glb": "manifold-cad index.ts output.glb"
|
|
24
|
+
},
|
|
25
|
+
"author": "CadIt",
|
|
26
|
+
"license": "cc-by-sa",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"manifold",
|
|
29
|
+
"fillet",
|
|
30
|
+
"cad"
|
|
31
|
+
],
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "https://github.com/CADit-app/manifold-fillet"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/CADit-app/manifold-fillet#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/CADit-app/manifold-fillet/issues"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"manifold-3d": "^2.1.0 || ^3.0.0"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"manifold-3d": "^3.3.2",
|
|
46
|
+
"tsup": "^8.0.0",
|
|
47
|
+
"typescript": "^5.3.3"
|
|
48
|
+
},
|
|
49
|
+
"module": "dist/index.mjs",
|
|
50
|
+
"publishConfig": {
|
|
51
|
+
"access": "public"
|
|
52
|
+
}
|
|
53
|
+
}
|