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.
- Import/export: STL, OBJ and feature‑aware 3MF (embedded history).
- 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, Import/Export, and About.
- 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.
- Easy to use plugin system lets you import your own plugins from github repos.
- 3D Model Import improvements
- Import button prefers 3MF with embedded feature history; falls back to pure geometry when no history is present. BREP JSON is still supported for history‑only import.
- The Import 3D Model feature auto‑detects STL vs 3MF from strings, data URLs, or ArrayBuffers. 3MFs are merged to a single editable mesh; STLs import as geometry only.
- File Manager save/load now uses compact 3MF with embedded feature history and thumbnail; legacy JSON remains compatible for older saves.
- Image to Face + Image Editor
- “Edit Image” launches an in‑app paint‑like editor. If no image is set, it starts with a 300×300 white canvas by default.
- Editor UI prefers dark mode (image unaffected), renders at true 1:1 pixels with DPR‑aware crispness, and shows immediately on open.
- Resize handle on the bottom‑right lets you expand or crop the working canvas; edits are preserved when growing, cropped when shrinking.
- Brush improvements: live cursor outline, shapes (round/square/diamond), eraser honors brush shape, smooth dabbed strokes.
- Paint Bucket tool with tolerance slider (0–255) fills contiguous regions based on the composited image (background + edits) while applying paint only to the draw layer.
- Finish/Cancel close the editor; Finish updates the feature image and triggers a recompute.
- Example plugin repository: https://github.com/mmiscool/BREPpluginExample
- Example plugin README: https://github.com/mmiscool/BREPpluginExample/blob/master/README.md
- Entrypoint: https://github.com/mmiscool/BREPpluginExample/blob/master/plugin.js
- Feature example: https://github.com/mmiscool/BREPpluginExample/blob/master/exampleFeature.js
- Primitive Cube: Implemented
- Primitive Cylinder: Implemented
- Primitive Cone: Implemented
- Primitive Sphere: Implemented
- Primitive Torus: Implemented
- Primitive Pyramid: Implemented
- Plane: Implemented
- Datum: Implemented
- Sketch: Implemented
- Extrude: Implemented
- Sweep: Implemented
- Loft: Implemented
- Revolve: Implemented
- Mirror: Implemented
- Boolean: Implemented
- Fillet: Implemented
- Chamfer: Implemented
- Remesh: Implemented
- Import 3D Model (STL/3MF): Implemented
- Image to Face (image trace): 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.
- Import…: opens a file picker for 3MF (with optional embedded history) or BREP JSON.
- Export…: opens a dialog to export as 3MF, STL, OBJ, or BREP JSON.
- Selection Filter: now lives in the toolbar (right side) for quick changes; Esc clears selection.
Use the “Import 3D Model” feature in the history panel. It supports both STL and 3MF:
Alternatively, use the top‑toolbar “Import…” button to open 3MF (with or without embedded history) or BREP JSON. If a 3MF includes embedded feature history, BREP restores and runs it; if not, it imports as geometry. For STL files, add an “Import 3D Model” feature to the history (auto‑detects STL vs 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('IMPORT3D');
importFeature.inputParams.fileToImport = someStlOr3mfData; // string (ASCII or data URL) or ArrayBuffer
importFeature.inputParams.deflectionAngle = 15; // degrees to group triangles into faces
await ph.runHistory();
Supported formats and how they round‑trip through BREP:
- 3MF (feature‑aware):
- Export: Generates a valid 3MF container that includes the triangulated geometry plus an embedded copy of your feature history so the file remains editable later in this app. Non‑manifold solids are detected and skipped (you will be notified), and the export still proceeds so you can share the file or fix later. The history is stored as XML at
Metadata/featureHistory.xml
, and a model metadata entryfeatureHistoryPath
points to it. Multiple solids export as separate<object>
items in a single 3MF. Units are configurable (defaultmillimeter
). - Import: If a 3MF contains
Metadata/featureHistory.xml
(or any*featureHistory.xml
), BREP loads that history and rebuilds the model, preserving editable features. If not present, the 3MF is imported as pure geometry (mesh only). - Compatibility: The embedded history is non‑standard metadata; other 3MF viewers will ignore it, but the 3MF remains fully valid and viewable elsewhere.
- Export: Generates a valid 3MF container that includes the triangulated geometry plus an embedded copy of your feature history so the file remains editable later in this app. Non‑manifold solids are detected and skipped (you will be notified), and the export still proceeds so you can share the file or fix later. The history is stored as XML at
The Image to Face feature traces a monochrome image to create planar geometry that can be placed on a selected plane/face and used downstream.
-
Parameters: threshold (0–255), invert, pixelScale, center, simplify options, and an “Edit Image” button.
-
Edit Image opens the paint‑like editor:
- Starts with your current image; if none, uses a 300×300 white canvas.
- Dark‑mode UI, 1:1 pixel rendering by default (DPR‑aware), and immediate display.
- Tools: Brush, Eraser, Pan (Space), Paint Bucket (G) with tolerance slider.
- Brush: size slider, shape selector (round/square/diamond), live outline preview.
- Canvas: bottom‑right resize handle to expand/crop the working area.
- Undo/Redo, Fit view, Finish (saves back to the feature and recomputes), Cancel.
-
STL:
- Export: ASCII STL. If multiple solids are selected, the Export dialog produces a ZIP with one STL per solid. Unit scaling is applied at export time.
- Import: ASCII or binary supported. STL imports as geometry only (no feature history).
-
OBJ:
- Export: ASCII OBJ. If multiple solids are selected, the Export dialog produces a ZIP with one OBJ per solid. Unit scaling is applied at export time.
-
BREP JSON:
- Export: Saves only the feature history as JSON (
.BREP.json
) with no mesh. Useful for versioning or quick backups. - Import: Loads the saved history and recomputes the model. The Import button accepts
.json
files of this shape.
- Export: Saves only the feature history as JSON (
Where this lives in the code:
- 3MF exporter:
src/exporters/threeMF.js
(packages geometry and optional attachments using JSZip). - Export dialog:
src/UI/toolbarButtons/exportButton.js
. - Import logic:
src/UI/toolbarButtons/importButton.js
. - JSON ↔ XML helpers for the embedded history:
src/utils/jsonXml.js
.
Notes and limitations
- 3MF export focuses on geometry and editable history. It also tags each triangle with its originating face label using a per-object BaseMaterials resource (face names appear as material names). No physically-accurate materials or textures are exported.
- 3MF import merges geometry for editing and does not reconstruct materials.
- Embedded feature history is specific to BREP and may change as the project evolves.
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
- JSZip (
jszip
): packages 3MF containers and ZIPs of multiple STLs. - 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). 3MF files exported by BREP include an embedded feature history for round‑tripping in BREP; other apps will ignore this metadata.
- 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.
Todo: Area of face in inspector.