A feature-based modeling playground experimenting with BREP-style workflows on top of triangle meshes. It combines robust manifold CSG (via the Manifold library) with a simple face/edge representation, a history pipeline, and Three.js visualization. Import meshes (STL), repair and group them into faces, then perform boolean operations, fillets, chamfers, sweeps, lofts, and more.
This project is actively evolving; expect rough edges while APIs settle.
- Feature history pipeline with a compact UI to add, edit, and re-run features.
- Robust CSG powered by
manifold-3d
with face-label provenance carried through booleans. - Mesh-to-BREP conversion that groups triangles into faces by normal deflection.
- Mesh repair pipeline: weld, T‑junction fix, overlap removal, hole fill, and consistent normals.
- Importers for STL and 3MF (via Three.js loaders).
- Primitive solids (cube, sphere, cylinder, cone, torus, pyramid) and typical CAD features (sketch/extrude, sweep, loft, revolve, fillet, chamfer, mirror, boolean ops).
- Modular main toolbar with: Save, Zoom to Fit, Wireframe toggle, About, and STL export.
- Selection Filter surfaced in the toolbar for quick access.
- Browser test runner captures per-test canvas snapshots (with auto Zoom‑to‑Fit) and shows them in the log dialog.
- Primitive Cube: Implemented
- Primitive Cylinder: Implemented
- Primitive Cone: Implemented
- Primitive Sphere: Implemented
- Primitive Torus: Implemented
- Primitive Pyramid: Implemented
- Plane: Implemented
- Datium: Planned
- Sketch: Implemented
- Extrude: Implemented
- Sweep: Implemented
- Loft: Planned
- Revolve: Implemented
- Mirror: Implemented
- Boolean: Implemented
- Fillet: Implemented
- Chamfer: Implemented
- STL/3MF Import: Implemented
Prereqs: Node.js 18+ and pnpm
installed.
- Install dependencies:
pnpm install
- Run dev server (Vite):
pnpm dev
- Open the printed URL (usually http://localhost:5173). Try
index.html
,sdf.html
, oroffsetSurfaceMeshTest.html
for sandboxes.
- Open the printed URL (usually http://localhost:5173). Try
- Run tests:
pnpm test
- Live testing while editing (Node):
pnpm liveTesting
- Top toolbar (fixed):
- Save: stores the current model to browser localStorage (integrates with File Manager).
- Zoom to Fit: pans and zooms using ArcballControls to frame all visible geometry without changing orientation.
- Wireframe: toggles mesh wireframe rendering for a quick inspection.
- About: opens the third‑party license report.
- Export STL: downloads an ASCII STL of the current part(s). If multiple solids are present, you can export each individually. If you have solids selected, only the selected ones are exported.
- Selection Filter: now lives in the toolbar (right side) for quick changes; Esc clears selection.
Use the “STL Import” feature in the history panel. It now supports both STL and 3MF:
- STL: ASCII or binary. Parsed with
three/examples/jsm/loaders/STLLoader.js
. - 3MF: ZIP-based format. Parsed with
three/examples/jsm/loaders/3MFLoader.js
and merged.
After parsing, an optional centering step runs, followed by the mesh repair pipeline (configurable levels). Finally, triangles are labeled into faces by deflection angle and authored into a Solid
for CSG and visualization.
Programmatic example (from tests):
import { PartHistory } from './src/PartHistory.js';
const ph = new PartHistory();
const importFeature = await ph.newFeature('STL'); // also accepts 3MF
importFeature.inputParams.fileToImport = someStlOr3mfData; // string (ASCII or data URL) or ArrayBuffer
importFeature.inputParams.deflectionAngle = 15; // degrees to group triangles into faces
await ph.runHistory();
Solid
authoring uses arrays (triangles + per‑triangle face labels). Before building a Manifold, triangle windings are made consistent and orientation is fixed by signed volume.manifold-3d
creates a robust manifold and propagates face IDs through CSG, so original face labels remain usable after unions/differences/intersections.- Faces and edges are visualized via Three.js; face names remain accessible for downstream feature logic.
- BREP model: Triangle mesh plus per‑triangle face labels. Labels map to globally unique IDs in Manifold, which propagate through CSG so selections remain stable. Edges are derived at boundaries between distinct face labels and represented as polyline chains.
- Manifoldization: Authoring arrays are cleaned before build: triangle windings are made consistent by adjacency; outward orientation is enforced by signed volume; an optional weld epsilon removes duplicate vertices and degenerates. Results are cached until geometry mutates.
- Visualization:
Solid.visualize()
creates oneFace
mesh per face label andEdge
polylines for label boundaries. Objects include semantic names to support selection and downstream features.
-
Type:
THREE.Group
subclass providing authoring, CSG, queries, and export. -
Geometry storage:
_vertProperties
(flat positions),_triVerts
(triangle indices),_triIDs
(per‑triangle face ID), with name↔ID maps to preserve labels through CSG. -
addTriangle(faceName, v1, v2, v3)
: Adds a labeled triangle; inputsfaceName:string
,v1:[x,y,z]
,v2:[x,y,z]
,v3:[x,y,z]
; returnsSolid
(this). -
setEpsilon(epsilon = 0)
: Sets weld tolerance, welds vertices, drops degenerates, fixes windings; inputsepsilon:number
; returnsSolid
(this). -
mirrorAcrossPlane(point, normal)
: Returns a mirrored copy across a plane; inputspoint:THREE.Vector3|[x,y,z]
,normal:THREE.Vector3|[x,y,z]
; returnsSolid
. -
invertNormals()
: Flips triangle windings to invert normals; inputs none; returnsSolid
(this). -
fixTriangleWindingsByAdjacency()
: Enforces consistent orientation across shared edges; inputs none; returnsSolid
(this). -
removeTinyBoundaryTriangles(areaThreshold, maxIterations = 1)
: Removes sliver triangles along label boundaries via safe 2–2 flips; inputsareaThreshold:number
,maxIterations?:number
; returnsnumber
(flips performed). -
getMesh()
: Gets current Manifold MeshGL; inputs none; returnsMeshGL
({ numProp, vertProperties, triVerts, faceID, ... }
). -
getFace(name)
: Fetches triangles for a face label; inputsname:string
; returnsArray<{ faceName, indices:number[], p1:[x,y,z], p2:[x,y,z], p3:[x,y,z] }>
. -
getFaces(includeEmpty = false)
: Enumerates faces and their triangles; inputsincludeEmpty?:boolean
; returnsArray<{ faceName:string, triangles:Triangle[] }>
. -
getFaceNames()
: Lists known face labels; inputs none; returnsstring[]
. -
getBoundaryEdgePolylines()
: Computes boundary polylines between distinct face labels; inputs none; returnsArray<{ name:string, faceA:string, faceB:string, indices:number[], positions:[x,y,z][], closedLoop?:boolean }>
. -
visualize(options = {})
: Builds per‑face meshes and edge polylines into this group; inputsoptions:{ showEdges?:boolean, materialForFace?:(name)=>Material, name?:string }
; returnsSolid
(this). -
union(other)
: Boolean union; inputsother:Solid
; returnsSolid
. -
subtract(other)
: Boolean difference (A − B); inputsother:Solid
; returnsSolid
. -
intersect(other)
: Boolean intersection; inputsother:Solid
; returnsSolid
. -
difference(other)
: Boolean difference via Manifold API; inputsother:Solid
; returnsSolid
. -
simplify(tolerance?)
: Simplifies mesh preserving label boundaries; inputstolerance?:number
; returnsSolid
. -
setTolerance(tolerance)
: Sets manifold tolerance (may simplify); inputstolerance:number
; returnsSolid
. -
volume()
: Computes enclosed volume; inputs none; returnsnumber
. -
surfaceArea()
: Computes total surface area; inputs none; returnsnumber
. -
toSTL(name = 'solid', precision = 6)
: Exports ASCII STL; inputsname?:string
,precision?:number
; returnsstring
(STL text). -
writeSTL(filePath, name = 'solid', precision = 6)
: Writes ASCII STL to disk (Node only); inputsfilePath:string
,name?:string
,precision?:number
; returnsPromise<string>
(path written).
-
Type:
THREE.Mesh
representing all triangles that share a face label (can be non‑planar or disjoint islands). -
Properties:
name
(label),type
=FACE
,edges
(adjacentEdge
objects),geometry
(per‑face BufferGeometry built byvisualize()
). -
getAverageNormal()
: Computes area‑weighted world‑space average normal; inputs none; returnsTHREE.Vector3
. -
surfaceArea()
: Computes world‑space surface area; inputs none; returnsnumber
.
-
Type:
Line2
polyline representing a boundary chain between two face labels. -
Properties:
name
(boundary name),type
=EDGE
,faces
(the two adjacentFace
objects when present),closedLoop
(boolean),userData.polylineLocal
(polyline points),userData.faceA/faceB
(label names). -
length()
: Computes world‑space polyline length; inputs none; returnsnumber
.
- Label‑driven topology: Faces are semantic groups defined at authoring/import time and tracked per triangle. After booleans, label provenance survives so selections can continue to target the same named faces/edges.
- Edges from labels: Boundary edges are computed between triangles of different labels, then chained into polylines per label pair. This avoids fragile edge reconstruction and remains stable across many operations.
- Manifold contract: Inputs are assumed (or repaired to) be closed, watertight 2‑manifolds. The system corrects orientation and coherency but cannot heal gross self‑intersections or missing surfaces.
Topological naming is about keeping stable references to faces and edges as the model recomputes. This project uses per‑triangle face labels that propagate through CSG so features can reliably refer to geometry across edits.
- Face labels: Triangles are authored with a string face name. Internally each name maps to a globally unique Manifold ID and is stored as
faceID
per triangle. After boolean ops, Manifold preserves these IDs so the original face names remain available on the result. - Edge identification: Edges are computed as polylines along boundaries between pairs of face labels. Each boundary chain is named
<faceA>|<faceB>[i]
, wherei
disambiguates multiple loops between the same two faces. - Selections: The UI stores object names in feature parameters. Because face/edge objects are rebuilt from the propagated labels, references stay stable so long as some triangles of that face survive.
- Primitive conventions: Built‑in primitives assign semantic face names, e.g.
Cube_NX/PX/NY/PY/NZ/PZ
,Cylinder_S
(side),Cylinder_T/B
(top/bottom),Torus_Side/Cap0/Cap1
. Imported meshes useSTL_FACE_<n>
groups derived by normal‑deflection clustering. - Feature‑generated names: Operations derive clear, persistent names. For example, Fillet uses
FILLET_<faceA>|<faceB>_ARC
,_SIDE_A
,_SIDE_B
,_CAP0
,_CAP1
; Chamfer usesCHAMFER_<faceA>|<faceB>_BEVEL
,_SIDE_A
,_SIDE_B
,_CAP0
,_CAP1
.
Guidelines and limitations
- Stability: Names persist through booleans and simplification; a name disappears only if all its triangles are removed by subsequent features.
- Splits/merges: A single face name can represent multiple disjoint islands after CSG. Edge loop indices
[i]
can change when topology changes; avoid hard‑coding the index when possible. - Semantics vs geometry: Faces are label‑based, not re‑fitted analytic surfaces. Prefer selecting faces by their semantic names (from primitives or earlier features) rather than by geometric predicates alone.
- Authoring tips: When creating new solids or tools, choose descriptive face names and reuse source face names in derived outputs. This improves reference stability for downstream features.
Roadmap
- Optional GUIDs for selection sets to further reduce ambiguity when faces split.
- Enhanced matching heuristics (geometric signatures) to map selections across parameter changes that substantially remesh surfaces.
- Three.js (
three
): rendering and core geometry types.- STL loader:
three/examples/jsm/loaders/STLLoader.js
- 3MF loader:
three/examples/jsm/loaders/3MFLoader.js
- Geometry utilities:
three/examples/jsm/utils/BufferGeometryUtils.js
- STL loader:
- Manifold (
manifold-3d
): WASM CSG/mesh library used for manifold construction, boolean operations, and mesh queries. Repo: https://github.com/elalish/manifold/- Loaded via
src/BREP/setupManifold.js
withvite-plugin-wasm
in the browser.
- Loaded via
- Vite (
vite
): dev server and build tooling. - Nodemon (
nodemon
): convenient live testing for Node-based checks.
src/features/
— Implementations of features (primitives, boolean, fillet, chamfer, sketch/extrude, sweep, loft, revolve, STL/3MF import, etc.).src/BREP/
— Core BREP/solid authoring on top of Manifold, mesh repair, mesh-to-BREP conversion.src/UI/
— Minimal UI widgets for the history pipeline and file management.src/FeatureRegistry.js
— Registers features available to the pipeline.src/PartHistory.js
— Orchestrates feature execution and artifact lifecycle.index.html
,sdf.html
,offsetSurfaceMeshTest.html
— Standalone sandboxes and demos.
pnpm dev
— Run Vite dev server.pnpm build
— Build for production.pnpm test
— Run test suite.pnpm liveTesting
— Auto-runs tests on file changes.
The project also includes a simple license report generator (pnpm generateLicenses
) that writes about.html
.
- A lightweight runner UI (mounted in the browser) lists all tests with controls to run individually or in sequence.
- After each test completes, the runner performs Zoom‑to‑Fit and captures a canvas snapshot. Clicking “Show Log” displays the snapshot above any logged output for that test.
- Between tests, an optional popup can show a running gallery of snapshots when auto‑progressing.
- Zoom‑to‑Fit uses ArcballControls only (pan + orthographic zoom) to frame all visible geometry while preserving the current camera orientation.
- It computes a bounding box of scene content (excluding Arcball gizmos), projects to camera space to consider the current view, and determines the required zoom so both width and height fit with a small margin.
- No direct camera frustum or orientation changes are applied — this keeps controls and rendering in sync and avoids “jump” artifacts.
- Mesh repair is heuristic and may need tuning for specific models.
- 3MF: geometry is merged into one mesh; materials/textures are not preserved for editing (visualization only).
- APIs and file formats are subject to change as the project evolves.
See LICENSE.md
. This project uses a dual-licensing strategy managed by Autodrop3d LLC.