Overview
Sketch3D is a browser-based 3D design tool built with Three.js and Vite. It uses a face-and-edge surface modeling approach inspired by SketchUp — there are no solid objects, only independent Face and Edge primitives that can be drawn, split, extruded, swept, and painted.
The entire application runs client-side with no backend required. Projects can be persisted to GitHub via Device Flow OAuth.
Solid class. Everything is an independent Face object, just like SketchUp. Extrusion creates 5+ separate faces (bottom, top, sides) that share edges but have no parent container.
Architecture
The application is organized into six core modules, wired together in main.js:
Data Flow
Every user action follows the same path:
- Input — Mouse/keyboard event dispatched to the active tool
- Command — Tool creates a
Commandobject (e.g.ExtrudeFaceCommand) - Execute —
UndoManager.executeCommand(cmd)runscmd.execute()and pushes to the undo stack - Kernel — Command calls
GeometryKernelmethods to create/modify/delete geometry - Render — Engine's animation loop renders the updated scene
Quick Start
# Clone and run
git clone https://github.com/user/sketch3d.git
cd sketch3d
npm install
npm run dev # http://localhost:3000
# Build for production
npm run build # outputs to dist/
Project Structure
sketch3d/
├── app/index.html # App shell (canvas + toolbar HTML)
├── index.html # Landing page
├── docs/index.html # This documentation
├── src/
│ ├── main.js # Bootstrap & wiring
│ ├── engine.js # Three.js setup
│ ├── geometry.js # Face, Edge, Group, GeometryKernel
│ ├── tools.js # All tool implementations
│ ├── history.js # Command classes & UndoManager
│ ├── inference.js # Snapping engine
│ ├── export.js # OBJ/STL export
│ ├── ui.js # Toolbar, panels, milestones
│ ├── github.js # GitHub OAuth & API client
│ ├── styles.css # All app styles
│ └── examples.js # Demo scene builder
└── vite.config.js # Vite MPA config
Engine
src/engine.js — Three.js wrapper providing scene setup, camera controls, and raycasting utilities.
Constructor
Creates the renderer (WebGL, antialiased), perspective camera (45° FOV), orbit controls (middle=pan, right=orbit, scroll=zoom), lighting (ambient + directional + hemisphere), grid (100×100 at Y=0), and colored axes.
Key Methods
| Method | Returns | Description |
|---|---|---|
| start() | void | Begin the render loop |
| setOrbitEnabled(on) | void | Enable/disable camera controls (tools disable during operations) |
| getGroundPoint(event) | Vector3 | Raycast mouse to Y=0 ground plane |
| getPointOnPlane(event, plane) | Vector3 | Raycast mouse to arbitrary THREE.Plane |
| raycastObjects(event, objects?) | Intersection[] | Raycast to model group children |
| screenToNDC(event) | Vector2 | Mouse event → normalized device coordinates |
| worldToScreen(point) | Vector2 | 3D point → screen pixels |
| addToModel(obj) | void | Add object to user geometry group |
| addHelper(obj) | void | Add to guide/indicator group |
| clearHelpers() | void | Dispose all helpers + dimension labels |
| addDimensionLabel(pos, text) | void | HTML overlay label at world position |
Geometry Kernel
src/geometry.js — Central manager for all geometric data. Contains Face, Edge, Group classes and the GeometryKernel.
Constructor
Properties
| Property | Type | Description |
|---|---|---|
| faces | Map<id, Face> | All faces in the scene |
| edges | Map<id, Edge> | All edges (standalone + face boundary) |
| groups | Map<id, Group> | Named face collections |
| importedMeshes | Map<id, {mesh, name}> | Imported 3D models |
Face Operations
Create a new face, add it to the kernel, wire boundary edges, add mesh to scene.
Split a face by cutting a hole with innerVerts. Returns the inner face (filling the hole) and the outer face (frame with hole). Inner face uses polygonOffsetFactor: 2 to avoid z-fighting.
Extrude face along its normal by distance. Creates bottom, top, and side faces as independent Face objects.
Sweep a 2D profile along a 3D path using parallel-transport frames. Creates side quads and cap faces. Used by FollowMeTool.
Revolve a profile around an axis to create a surface of revolution. Handles on-axis vertices by collapsing quads to triangles. 24 steps default.
Compute polygon offset using angle bisectors. Positive = inward, negative = outward.
Edge Operations
Reuse an existing edge matching endpoints (within eps=0.01) or create a new one. Core deduplication mechanism.
Auto-detect closed loops formed by a new edge. Uses leftmost-turn cycle detection with intersection splitting. Creates faces for all discovered coplanar cycles.
Intersection
Compute all overlaps between two face sets, chain segments into paths/loops, and split faces. Handles both non-coplanar (line-plane intersection + clipping) and coplanar (Sutherland-Hodgman) cases.
Serialization
Serialize all faces, edges, groups, and camera state to a JSON-compatible object.
Clear all geometry and restore from JSON. Resets undo history.
Face
The primary geometric primitive. A planar polygon with optional holes.
Constructor
Properties
| Property | Type | Description |
|---|---|---|
| id | string | Unique identifier (obj_N) |
| vertices | Vector3[] | Outer boundary vertices |
| holes | Vector3[][] | Inner hole polygons (wound opposite to outer) |
| normal | Vector3 | Face normal vector |
| color | number | Hex color (e.g. 0xcccccc) |
| metalness | number | PBR metalness (0–1) |
| roughness | number | PBR roughness (0–1) |
| opacity | number | Transparency (0–1) |
| mesh | THREE.Mesh | Rendered Three.js geometry |
| boundaryEdges | Edge[] | Edges forming the outer boundary |
| holeEdges | Edge[][] | Edges forming each hole boundary |
| group | Group | null | Parent group if grouped |
| visible | boolean | Visibility toggle |
Key Methods
| Method | Returns | Description |
|---|---|---|
| createMesh() | THREE.Mesh | Build ShapeGeometry with ear-clipping for holes. Sets explicit normals to avoid triangulation seam artifacts. |
| addHole(holeVerts) | void | Add a hole polygon. Winding must be opposite to outer boundary. |
| setColor(color) | void | Change material color |
| setMaterial(color, metalness, roughness, opacity, textureId) | void | Update all material properties |
| highlight(on) | void | Toggle highlight emissive color |
| getCenter() | Vector3 | Compute face centroid |
| dispose() | void | Free GPU memory |
Edge
First-class edge primitive. All edges (standalone lines and face boundaries) are Edge objects stored in the kernel's edges map.
Constructor
Properties
| Property | Type | Description |
|---|---|---|
| id | string | Unique identifier |
| points | Vector3[] | Polyline vertices (usually 2 points) |
| faceRefs | Set<Face> | Faces that share this edge as boundary |
| mesh | THREE.Line | Rendered line (linewidth: 2, color: 0x000000) |
Edges are deduplicated via findOrCreateEdge(a, b) with epsilon 0.01. When a face is created, _createBoundaryEdges(face) wires bidirectional references between the face and its edges.
Group
Named collection of faces. Groups are excluded from auto-intersection and can be moved/rotated as a unit.
Properties
| Property | Type | Description |
|---|---|---|
| id | string | Unique identifier |
| faces | Set<Face> | Member faces |
| name | string | Display name |
Tool System
src/tools.js — All user-facing tools extend the base Tool class. ToolManager handles activation and switching.
Base Class
class Tool {
constructor(engine, geometry, inference, history) { ... }
activate() {} // Called when tool becomes active
deactivate() {} // Called when switching away
onMouseDown(event) {}
onMouseMove(event) {}
onMouseUp(event) {}
onKeyDown(event) {}
}
ToolManager
Deactivates the current tool and activates the named tool. Dispatches a toolChanged custom event.
Helper Functions
| Function | Description |
|---|---|
| detectWorkingPlane(engine, geometry, event) | Returns {point, normal, u, v, plane, face} — the working plane under the cursor. Face is the hit Face or null for ground. |
| snapForDrawing(inference, rawPoint, isOnFace) | Snap a point for drawing operations with face vertex/edge/midpoint priority. |
| showFaceHints(engine, geometry, event) | Show visual hints for snapping targets on faces. |
| pickEdgeRaycast(engine, geometry, event) | Raycast to find edges under cursor. Returns Edge array sorted by distance. |
Undo / Redo
src/history.js — Command pattern. Every user action creates a Command with execute() and undo() methods.
UndoManager
| Method | Description |
|---|---|
| executeCommand(cmd) | Run cmd.execute(), push to undo stack, clear redo stack |
| undo() | Pop last command, call cmd.undo(), push to redo stack |
| redo() | Pop from redo stack, call cmd.execute(), push to undo stack |
| clear() | Reset both stacks |
| canUndo() / canRedo() | Boolean state queries |
Creating a Custom Command
class MyCommand extends Command {
constructor(kernel, params) {
super();
this.kernel = kernel;
this.params = params;
this._createdFace = null;
}
execute() {
this._createdFace = this.kernel.createFaceFromPoints(
this.params.points, this.params.normal
);
}
undo() {
if (this._createdFace) {
this.kernel.deleteFace(this._createdFace);
this._createdFace = null;
}
}
}
createdEdges for clean undo. On undo, only edges created by that command are removed — shared/pre-existing edges are preserved.
Snapping (Inference Engine)
src/inference.js — SketchUp-style snapping with visual feedback indicators.
Snap Priority
- Endpoint (green) — Existing face/edge vertices
- Midpoint (blue) — Edge midpoints
- Axis alignment (red/green/blue) — Cardinal alignment from reference point
- Grid (orange) — 0.5-unit grid fallback
Usage
const result = inference.snap(worldPoint);
// result = { point: Vector3, type: 'endpoint'|'midpoint'|'axis'|'grid', color: hex }
inference.setReferencePoint(lastClickPoint); // for axis alignment
inference.showIndicator(result); // display snap sphere
inference.hideIndicator(); // clear feedback
Export (OBJ / STL)
src/export.js — Static export methods with vertex deduplication and proper triangulation.
Wavefront OBJ format. Handles faces with holes by extracting pre-triangulated geometry from ShapeGeometry. Simple faces exported as n-gons. Vertex deduplication with eps=0.0001.
ASCII STL format. Extracts triangles from ShapeGeometry for faces with holes, uses fan triangulation for simple faces.
Trigger browser file download.
Serialization
Projects are serialized as a single JSON object stored in sketch3d.json.
JSON Format
{
"faces": [
{
"vertices": [[x, y, z], ...],
"normal": [nx, ny, nz],
"holes": [[[x, y, z], ...], ...],
"color": 13421772,
"metalness": 0,
"roughness": 0.8,
"opacity": 1,
"textureId": null,
"visible": true,
"groupId": null
}
],
"edges": [
{ "points": [[x, y, z], ...], "color": 0 }
],
"groups": [
{ "id": "group_1", "name": "Box", "faceIndices": [0, 1, 2, 3, 4, 5] }
],
"camera": {
"position": [15, 20, 15],
"target": [0, 0, 0]
}
}
GitHub Integration
src/github.js — Device Flow OAuth for GitHub authentication. Projects are stored as sketch3d.json in private repos. Each save creates a commit — commits serve as "milestones" that can be browsed and restored.
Flow
startDeviceFlow()→ Get user code + verification URL- User visits GitHub URL, enters code
pollForToken()→ Receive access tokenvalidateToken()→ Fetch username and avatarcreateRepo(name)→ Private repo for projectsaveProject(repo, json, message)→ Commit sketch3d.jsonlistMilestones(repo)→ Browse commit historygetProjectAtCommit(repo, sha)→ Restore any version
Sharing
Public repos can be loaded via /app/?gh=owner/repo. The fetchProject() and listPublicMilestones() methods work without authentication for public repos.
Command Reference
All commands in src/history.js extend the base Command class.
| Command | Description |
|---|---|
| CreateFaceCommand | Create face from vertices + normal |
| SplitFaceCommand | Cut hole in face, creating inner + outer |
| ExtrudeFaceCommand | Extrude face along normal (with punch-through) |
| ResizeExtrusionCommand | Resize existing extrusion instead of creating new |
| CreateEdgeCommand | Create standalone edge with auto-face detection |
| DeleteCommand | Delete face only |
| DeleteFaceCleanCommand | Delete face + orphaned edges |
| DeleteEdgeCommand | Delete standalone edge |
| DeleteEdgeWithFacesCommand | Delete edge + all dependent faces |
| SplitFaceByLineCommand | Split face with line (outer→outer) |
| SplitHoleCommand | Split hole with line (hole→hole) |
| AbsorbHoleCommand | Absorb hole into outer boundary via bridge |
| MergeHolesCommand | Merge two holes via connecting line |
| MoveFaceCommand | Translate face vertices + holes |
| MoveEdgeCommand | Move edge endpoints |
| RotateFaceCommand | Rotate face around center/axis |
| ChangeColorCommand | Change face color/material |
| ApplyMaterialCommand | Apply full PBR material |
| OffsetFaceCommand | Apply polygon offset to face |
| FillHoleCommand | Fill hole by removing from holes array |
| FollowMeCommand | Execute sweep operation |
| RevolveCommand | Execute revolve operation |
| IntersectFacesCommand | Split overlapping face sets |
| FilletFaceCommand | Round face corners |
| ChamferEdgeCommand | Bevel face corners |
| HideFaceCommand | Toggle face visibility |
| ShowAllCommand | Show all hidden faces |
| GroupCommand | Create group from faces |
| UngroupCommand | Dissolve group |
| ImportMeshCommand | Import external 3D model |
Tool Reference
| Tool | Shortcut | Description |
|---|---|---|
| SelectTool | V | Click/box select. Drag to move. Shift+click multi-select. Shows bounding box gizmo. |
| LineTool | L | Place edges. Auto-creates faces from closed loops. Splits faces with 5 boundary cases. |
| RectangleTool | R | Click-drag rectangle. Splits existing faces. Dimension labels. |
| CircleTool | C | Click center, drag radius. 24-sided polygon approximation. |
| PolygonTool | G | Click center, drag radius. Scroll wheel adjusts sides (3-24). |
| ArcTool | A | 3-click arc: endpoint1, endpoint2, bulge. Sagitta formula for radius. |
| CurveTool | S | Catmull-Rom spline. Click control points, Enter to finish. |
| PushPullTool | P | Extrude faces. Snap to parallel faces. Punch-through coplanar faces. Resize existing extrusions. |
| MoveTool | M | Move faces/edges. Also integrated into SelectTool. |
| RotateTool | Q | 3-click: face → center → reference. Protractor visual. 15° snap. |
| OffsetTool | F | Click face, set inward distance. Bisector polygon offset. |
| FollowMeTool | H | Sweep profile along edge path. Parallel-transport frames. |
| RevolveTool | J | Spin profile around axis. 360°, 24 steps. Handles on-axis vertices. |
| EraseTool | E | Delete edges (priority) or faces. Edge deletion removes dependent faces. |
| PaintBucketTool | B | Apply color. Alt+click eyedropper. Fill holes. Floating palette. |
| FilletTool | K | Round corners with arc radius. |
| ChamferTool | D | 45° bevel at corners. |
| HideTool | \ | Toggle face visibility. |
| CommentTool | N | Add text annotations to scene. |
Keyboard Shortcuts
| Key | Action |
|---|---|
| V | Select tool |
| L | Line tool |
| R | Rectangle tool |
| C | Circle tool |
| G | Polygon tool |
| A | Arc tool |
| S | Curve tool |
| P | Push/Pull tool |
| M | Move tool |
| Q | Rotate tool |
| F | Offset tool |
| H | Follow Me tool |
| J | Revolve tool |
| E | Eraser tool |
| B | Paint Bucket tool |
| K | Fillet tool |
| D | Chamfer tool |
| \ | Hide tool |
| N | Comment tool |
| I | Intersect selected faces |
| Ctrl/Cmd+Z | Undo |
| Ctrl/Cmd+Shift+Z | Redo |
| Ctrl/Cmd+S | Save milestone |
| Delete / Backspace | Delete selected (face + orphaned edges) |
| Escape | Cancel current operation / Deselect |
| Space + drag | Temporary pan (Photoshop-style) |
| Middle mouse | Pan |
| Right mouse | Orbit |
| Scroll wheel | Zoom |