@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 ADDED
@@ -0,0 +1,45 @@
1
+ # Fillet script with unique name
2
+
3
+ ![Fillet script with unique name](./images/fillet-image.webp)
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>
@@ -0,0 +1,3 @@
1
+ import { ManifoldToplevel } from 'manifold-3d';
2
+ export declare const setupFillet: (manifoldInstance: ManifoldToplevel) => void;
3
+ export declare const getManifold: () => ManifoldToplevel;
@@ -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
+ }
@@ -0,0 +1,3 @@
1
+ import type { Manifold as ManifoldType } from 'manifold-3d';
2
+ declare const _default: ManifoldType[];
3
+ export default _default;
@@ -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];
@@ -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
+ }
@@ -0,0 +1,6 @@
1
+ export * from './context';
2
+ export * from './fillet';
3
+ export * from './edgeSelection';
4
+ export * from './pipeAlongPath';
5
+ export * from './wedgeBuilder';
6
+ export { default } from './example';
package/dist/index.js ADDED
@@ -0,0 +1,6 @@
1
+ export * from './context';
2
+ export * from './fillet';
3
+ export * from './edgeSelection';
4
+ export * from './pipeAlongPath';
5
+ export * from './wedgeBuilder';
6
+ export { default } from './example';
@@ -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
+ }