@cadit-app/fillet-script-with-unique-name 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 +3 -0
- package/dist/context.js +10 -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 +25 -0
- package/dist/fillet.d.ts +27 -0
- package/dist/fillet.js +115 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/pipeAlongPath.d.ts +25 -0
- package/dist/pipeAlongPath.js +82 -0
- package/dist/wedgeBuilder.d.ts +22 -0
- package/dist/wedgeBuilder.js +109 -0
- package/package.json +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Fillet script with unique name
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
An implementation of a fillet algorithm for ManifoldCAD.
|
|
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/fillet-script-with-unique-name
|
|
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/fillet-script-with-unique-name.git
|
|
28
|
+
cd fillet-script-with-unique-name
|
|
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,10 @@
|
|
|
1
|
+
let manifold;
|
|
2
|
+
export const setupFillet = (manifoldInstance) => {
|
|
3
|
+
manifold = manifoldInstance;
|
|
4
|
+
};
|
|
5
|
+
export const getManifold = () => {
|
|
6
|
+
if (!manifold) {
|
|
7
|
+
throw new Error('Manifold instance not set. Call setupFillet(manifold) before using fillet functions.');
|
|
8
|
+
}
|
|
9
|
+
return manifold;
|
|
10
|
+
};
|
|
@@ -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,25 @@
|
|
|
1
|
+
import { Manifold } from 'manifold-3d/manifoldCAD';
|
|
2
|
+
import { fillet } from './fillet';
|
|
3
|
+
const box = Manifold.cube([20, 20, 20], true);
|
|
4
|
+
// Fillet the edge at position (10, 10, 0)
|
|
5
|
+
const rounded = fillet(box, {
|
|
6
|
+
radius: 2,
|
|
7
|
+
selection: {
|
|
8
|
+
type: 'point',
|
|
9
|
+
point: [10, 10, 0],
|
|
10
|
+
maxDistance: 5 // optional: limit search radius
|
|
11
|
+
},
|
|
12
|
+
segments: 50
|
|
13
|
+
});
|
|
14
|
+
const box1 = Manifold.cube([20, 20, 20], true);
|
|
15
|
+
// Fillet all sharp edges (dihedral angle < 100°)
|
|
16
|
+
const fullyRounded = fillet(box1, {
|
|
17
|
+
radius: 2,
|
|
18
|
+
selection: {
|
|
19
|
+
type: 'angle',
|
|
20
|
+
minAngle: 80 // edges sharper than 80° from flat
|
|
21
|
+
},
|
|
22
|
+
segments: 50
|
|
23
|
+
})
|
|
24
|
+
.translate([30, 0, 0]);
|
|
25
|
+
export default [rounded, fullyRounded];
|
package/dist/fillet.d.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
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 { Manifold } 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
|
+
export declare function fillet(mf: Manifold, options: FilletOptions): Manifold;
|
|
27
|
+
export type { EdgeSelection, PointEdgeSelection, AngleEdgeSelection } from './edgeSelection';
|
package/dist/fillet.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
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 { getManifold } from './context';
|
|
14
|
+
import { extractEdges, selectEdges } from './edgeSelection';
|
|
15
|
+
/**
|
|
16
|
+
* Apply fillet/round to selected edges of a Manifold.
|
|
17
|
+
*/
|
|
18
|
+
export function fillet(mf, options) {
|
|
19
|
+
const manifold = getManifold();
|
|
20
|
+
const { radius, selection, segments = 16 } = options;
|
|
21
|
+
if (radius <= 0) {
|
|
22
|
+
throw new Error('Fillet radius must be positive');
|
|
23
|
+
}
|
|
24
|
+
// Extract all edges from mesh
|
|
25
|
+
const allEdges = extractEdges(mf);
|
|
26
|
+
if (allEdges.length === 0) {
|
|
27
|
+
console.warn('fillet: No edges found in mesh');
|
|
28
|
+
return mf;
|
|
29
|
+
}
|
|
30
|
+
// Select edges based on criteria
|
|
31
|
+
const selectedEdges = selectEdges(allEdges, selection);
|
|
32
|
+
if (selectedEdges.length === 0) {
|
|
33
|
+
console.warn('fillet: No edges matched selection criteria');
|
|
34
|
+
return mf;
|
|
35
|
+
}
|
|
36
|
+
console.log('fillet: Found', selectedEdges.length, 'edges to fillet');
|
|
37
|
+
let result = mf;
|
|
38
|
+
// Process convex edges
|
|
39
|
+
// We can union all tools for a single subtraction if they don't overlap
|
|
40
|
+
const cuttingTools = [];
|
|
41
|
+
for (const edge of selectedEdges) {
|
|
42
|
+
// Only handling convex edges (standard fillets)
|
|
43
|
+
// Filter out flat edges (dihedral angle approx 0) which are just triangulation artifacts
|
|
44
|
+
if (edge.dihedralAngle > 5 && edge.dihedralAngle < 175) {
|
|
45
|
+
const tool = createFilletCuttingTool(edge, radius, segments);
|
|
46
|
+
if (tool && tool.volume() > 1e-9) {
|
|
47
|
+
cuttingTools.push(tool);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Subtract tools sequentially to avoid memory spikes from large unions
|
|
52
|
+
if (cuttingTools.length > 0) {
|
|
53
|
+
// Sort by volume or position might help stability, but just sequential is fine for now
|
|
54
|
+
console.log(`fillet: Subtracting ${cuttingTools.length} tools sequentially`);
|
|
55
|
+
for (const tool of cuttingTools) {
|
|
56
|
+
result = result.subtract(tool);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Create a fillet cutting tool using the wedge-minus-tube approach.
|
|
63
|
+
*/
|
|
64
|
+
function createFilletCuttingTool(edge, radius, segments) {
|
|
65
|
+
const manifold = getManifold();
|
|
66
|
+
// Edge endpoints
|
|
67
|
+
const [x0, y0, z0] = edge.p0;
|
|
68
|
+
const [x1, y1, z1] = edge.p1;
|
|
69
|
+
const ex = x1 - x0, ey = y1 - y0, ez = z1 - z0;
|
|
70
|
+
const edgeLen = Math.sqrt(ex * ex + ey * ey + ez * ez);
|
|
71
|
+
if (edgeLen < 1e-9)
|
|
72
|
+
return null;
|
|
73
|
+
const edgeDir = [ex / edgeLen, ey / edgeLen, ez / edgeLen];
|
|
74
|
+
const [n0x, n0y, n0z] = edge.n0;
|
|
75
|
+
const [n1x, n1y, n1z] = edge.n1;
|
|
76
|
+
// 1. TUBE: Positioned INWARD (tangent to faces)
|
|
77
|
+
// Center = Edge - radius*n0 - radius*n1 (for 90 degree edges)
|
|
78
|
+
// Note: For non-90 degree, this approximation works for small fillets but
|
|
79
|
+
// true bisector logic should be used. Using the sum offset for now as it handles 90 deg.
|
|
80
|
+
const ext = radius * 0.1;
|
|
81
|
+
const tubeStart = [
|
|
82
|
+
x0 - n0x * radius - n1x * radius - edgeDir[0] * ext,
|
|
83
|
+
y0 - n0y * radius - n1y * radius - edgeDir[1] * ext,
|
|
84
|
+
z0 - n0z * radius - n1z * radius - edgeDir[2] * ext
|
|
85
|
+
];
|
|
86
|
+
const tubeEnd = [
|
|
87
|
+
x1 - n0x * radius - n1x * radius + edgeDir[0] * ext,
|
|
88
|
+
y1 - n0y * radius - n1y * radius + edgeDir[1] * ext,
|
|
89
|
+
z1 - n0z * radius - n1z * radius + edgeDir[2] * ext
|
|
90
|
+
];
|
|
91
|
+
const sphere0 = manifold.Manifold.sphere(radius, segments).translate(tubeStart);
|
|
92
|
+
const sphere1 = manifold.Manifold.sphere(radius, segments).translate(tubeEnd);
|
|
93
|
+
const tube = manifold.Manifold.hull([sphere0, sphere1]);
|
|
94
|
+
// 2. WEDGE: Triangular prism extending INWARD
|
|
95
|
+
// CRITICAL: The wedge depth must be limited so it is completely contained
|
|
96
|
+
// within the tube on the "back" side. If deeper than the tube, subtracting
|
|
97
|
+
// the tube leaves "internal debris" which carves holes in the solid.
|
|
98
|
+
// For 90 degree corners, radius*1.0 is safe (reaches tube center).
|
|
99
|
+
const wedgeDepth = radius * 1.1;
|
|
100
|
+
const wedgePoints = [];
|
|
101
|
+
// At start
|
|
102
|
+
const sX = x0 - edgeDir[0] * ext, sY = y0 - edgeDir[1] * ext, sZ = z0 - edgeDir[2] * ext;
|
|
103
|
+
wedgePoints.push([sX, sY, sZ]); // Edge
|
|
104
|
+
wedgePoints.push([sX - n0x * wedgeDepth, sY - n0y * wedgeDepth, sZ - n0z * wedgeDepth]);
|
|
105
|
+
wedgePoints.push([sX - n1x * wedgeDepth, sY - n1y * wedgeDepth, sZ - n1z * wedgeDepth]);
|
|
106
|
+
// At end
|
|
107
|
+
const eX = x1 + edgeDir[0] * ext, eY = y1 + edgeDir[1] * ext, eZ = z1 + edgeDir[2] * ext;
|
|
108
|
+
wedgePoints.push([eX, eY, eZ]); // Edge
|
|
109
|
+
wedgePoints.push([eX - n0x * wedgeDepth, eY - n0y * wedgeDepth, eZ - n0z * wedgeDepth]);
|
|
110
|
+
wedgePoints.push([eX - n1x * wedgeDepth, eY - n1y * wedgeDepth, eZ - n1z * wedgeDepth]);
|
|
111
|
+
const wedge = manifold.Manifold.hull(wedgePoints);
|
|
112
|
+
// 3. CUTTING TOOL: Wedge - Tube
|
|
113
|
+
// Since wedge is small, wedge - tube leaves only the corner tip.
|
|
114
|
+
return wedge.subtract(tube);
|
|
115
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
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 { Manifold } from 'manifold-3d';
|
|
7
|
+
/**
|
|
8
|
+
* Creates a tube along a polyline path using convex hulls of spheres.
|
|
9
|
+
*
|
|
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 declare function pipeAlongPath(path: [number, number, number][], radius: number, segments?: number): Manifold;
|
|
16
|
+
/**
|
|
17
|
+
* Creates a tube along an edge path that extends slightly beyond
|
|
18
|
+
* the endpoints to ensure proper boolean overlap.
|
|
19
|
+
*
|
|
20
|
+
* @param path Path points
|
|
21
|
+
* @param radius Tube radius
|
|
22
|
+
* @param extensionFactor How much to extend beyond endpoints (as fraction of radius)
|
|
23
|
+
* @param segments Circular segments
|
|
24
|
+
*/
|
|
25
|
+
export declare function pipeAlongPathExtended(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
|
+
import { getManifold } from './context';
|
|
7
|
+
/**
|
|
8
|
+
* Creates a tube along a polyline path using convex hulls of spheres.
|
|
9
|
+
*
|
|
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(path, radius, segments) {
|
|
16
|
+
const manifold = getManifold();
|
|
17
|
+
if (path.length < 2) {
|
|
18
|
+
throw new Error('Path must have at least 2 points');
|
|
19
|
+
}
|
|
20
|
+
// Create spheres at each path point
|
|
21
|
+
const spheres = [];
|
|
22
|
+
for (const [x, y, z] of path) {
|
|
23
|
+
const sphere = manifold.Manifold.sphere(radius, segments)
|
|
24
|
+
.translate([x, y, z]);
|
|
25
|
+
spheres.push(sphere);
|
|
26
|
+
}
|
|
27
|
+
// Hull adjacent spheres to create tube segments, then union
|
|
28
|
+
const segments_ = [];
|
|
29
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
30
|
+
// Hull two adjacent spheres to form a tube segment
|
|
31
|
+
const segment = manifold.Manifold.hull([spheres[i], spheres[i + 1]]);
|
|
32
|
+
segments_.push(segment);
|
|
33
|
+
}
|
|
34
|
+
// Union all segments
|
|
35
|
+
if (segments_.length === 1) {
|
|
36
|
+
return segments_[0];
|
|
37
|
+
}
|
|
38
|
+
return manifold.Manifold.union(segments_);
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Creates a tube along an edge path that extends slightly beyond
|
|
42
|
+
* the endpoints to ensure proper boolean overlap.
|
|
43
|
+
*
|
|
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(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(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,22 @@
|
|
|
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 { Manifold } 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 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 declare function buildWedge(edge: MeshEdge, distance: number, inflate?: number): Manifold;
|
|
@@ -0,0 +1,109 @@
|
|
|
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 { getManifold } from './context';
|
|
9
|
+
import { edgeDirection } 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 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(edge, distance, inflate = 0.001) {
|
|
23
|
+
const manifold = getManifold();
|
|
24
|
+
// Edge vector and direction
|
|
25
|
+
const edgeVec = [
|
|
26
|
+
edge.p1[0] - edge.p0[0],
|
|
27
|
+
edge.p1[1] - edge.p0[1],
|
|
28
|
+
edge.p1[2] - edge.p0[2]
|
|
29
|
+
];
|
|
30
|
+
const dir = edgeDirection(edge);
|
|
31
|
+
// Compute offset directions perpendicular to edge, along face normals
|
|
32
|
+
// We need vectors in the plane of each face, perpendicular to the edge
|
|
33
|
+
const offsetA = crossAndNormalize(edge.n0, dir);
|
|
34
|
+
const offsetB = crossAndNormalize(edge.n1, dir);
|
|
35
|
+
// Ensure offset directions point "outward" from the edge (same side as normals)
|
|
36
|
+
// This aligns them with how the fillet should remove material
|
|
37
|
+
const dotA = dot(offsetA, edge.n0);
|
|
38
|
+
const dotB = dot(offsetB, edge.n1);
|
|
39
|
+
if (dotA < 0) {
|
|
40
|
+
offsetA[0] = -offsetA[0];
|
|
41
|
+
offsetA[1] = -offsetA[1];
|
|
42
|
+
offsetA[2] = -offsetA[2];
|
|
43
|
+
}
|
|
44
|
+
if (dotB < 0) {
|
|
45
|
+
offsetB[0] = -offsetB[0];
|
|
46
|
+
offsetB[1] = -offsetB[1];
|
|
47
|
+
offsetB[2] = -offsetB[2];
|
|
48
|
+
}
|
|
49
|
+
// Create wedge vertices
|
|
50
|
+
// The wedge is a triangular prism along the edge
|
|
51
|
+
const d = distance + inflate;
|
|
52
|
+
const ext = inflate; // Small extension along edge
|
|
53
|
+
// Start cap vertices (at p0 - ext along edge)
|
|
54
|
+
const s0 = [
|
|
55
|
+
edge.p0[0] - dir[0] * ext,
|
|
56
|
+
edge.p0[1] - dir[1] * ext,
|
|
57
|
+
edge.p0[2] - dir[2] * ext
|
|
58
|
+
];
|
|
59
|
+
const sA = [
|
|
60
|
+
s0[0] + offsetA[0] * d,
|
|
61
|
+
s0[1] + offsetA[1] * d,
|
|
62
|
+
s0[2] + offsetA[2] * d
|
|
63
|
+
];
|
|
64
|
+
const sB = [
|
|
65
|
+
s0[0] + offsetB[0] * d,
|
|
66
|
+
s0[1] + offsetB[1] * d,
|
|
67
|
+
s0[2] + offsetB[2] * d
|
|
68
|
+
];
|
|
69
|
+
// End cap vertices (at p1 + ext along edge)
|
|
70
|
+
const e0 = [
|
|
71
|
+
edge.p1[0] + dir[0] * ext,
|
|
72
|
+
edge.p1[1] + dir[1] * ext,
|
|
73
|
+
edge.p1[2] + dir[2] * ext
|
|
74
|
+
];
|
|
75
|
+
const eA = [
|
|
76
|
+
e0[0] + offsetA[0] * d,
|
|
77
|
+
e0[1] + offsetA[1] * d,
|
|
78
|
+
e0[2] + offsetA[2] * d
|
|
79
|
+
];
|
|
80
|
+
const eB = [
|
|
81
|
+
e0[0] + offsetB[0] * d,
|
|
82
|
+
e0[1] + offsetB[1] * d,
|
|
83
|
+
e0[2] + offsetB[2] * d
|
|
84
|
+
];
|
|
85
|
+
// Build the wedge as a convex hull of these 6 points
|
|
86
|
+
// This guarantees a valid manifold
|
|
87
|
+
const points = [s0, sA, sB, e0, eA, eB];
|
|
88
|
+
return manifold.Manifold.hull(points);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Cross product of two vectors, normalized.
|
|
92
|
+
*/
|
|
93
|
+
function crossAndNormalize(a, b) {
|
|
94
|
+
const c = [
|
|
95
|
+
a[1] * b[2] - a[2] * b[1],
|
|
96
|
+
a[2] * b[0] - a[0] * b[2],
|
|
97
|
+
a[0] * b[1] - a[1] * b[0]
|
|
98
|
+
];
|
|
99
|
+
const len = Math.sqrt(c[0] ** 2 + c[1] ** 2 + c[2] ** 2);
|
|
100
|
+
if (len < 1e-12)
|
|
101
|
+
return [0, 0, 1];
|
|
102
|
+
return [c[0] / len, c[1] / len, c[2] / len];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Dot product of two vectors.
|
|
106
|
+
*/
|
|
107
|
+
function dot(a, b) {
|
|
108
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
109
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@cadit-app/fillet-script-with-unique-name",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "An implementation of a fillet algorithm for ManifoldCAD",
|
|
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/fillet-script-with-unique-name"
|
|
35
|
+
},
|
|
36
|
+
"homepage": "https://github.com/CADit-app/fillet-script-with-unique-name#readme",
|
|
37
|
+
"bugs": {
|
|
38
|
+
"url": "https://github.com/CADit-app/fillet-script-with-unique-name/issues"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"manifold-3d": "^2.1.0 || ^3.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"manifold-3d": "^3.3.2",
|
|
45
|
+
"tsup": "^8.0.0",
|
|
46
|
+
"typescript": "^5.3.3"
|
|
47
|
+
},
|
|
48
|
+
"module": "dist/index.mjs",
|
|
49
|
+
"publishConfig": {
|
|
50
|
+
"access": "public"
|
|
51
|
+
}
|
|
52
|
+
}
|