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.

Key design principle: No 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:

main.js (Bootstrap) ├── Engine Three.js scene, camera, renderer, raycasting ├── GeometryKernel Face / Edge / Group management ├── InferenceEngine Snap: endpoints, midpoints, axes, grid ├── UndoManager Command stack (undo/redo) ├── ToolManager Tool lifecycle & switching │ ├── SelectTool, LineTool, RectangleTool, ... │ ├── PushPullTool, OffsetTool, FollowMeTool, ... │ └── PaintBucketTool, EraseTool, RotateTool, ... └── UI Toolbar, property panel, milestones

Data Flow

Every user action follows the same path:

  1. Input — Mouse/keyboard event dispatched to the active tool
  2. Command — Tool creates a Command object (e.g. ExtrudeFaceCommand)
  3. ExecuteUndoManager.executeCommand(cmd) runs cmd.execute() and pushes to the undo stack
  4. Kernel — Command calls GeometryKernel methods to create/modify/delete geometry
  5. 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

new Engine(canvas: HTMLCanvasElement)

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

MethodReturnsDescription
start()voidBegin the render loop
setOrbitEnabled(on)voidEnable/disable camera controls (tools disable during operations)
getGroundPoint(event)Vector3Raycast mouse to Y=0 ground plane
getPointOnPlane(event, plane)Vector3Raycast mouse to arbitrary THREE.Plane
raycastObjects(event, objects?)Intersection[]Raycast to model group children
screenToNDC(event)Vector2Mouse event → normalized device coordinates
worldToScreen(point)Vector23D point → screen pixels
addToModel(obj)voidAdd object to user geometry group
addHelper(obj)voidAdd to guide/indicator group
clearHelpers()voidDispose all helpers + dimension labels
addDimensionLabel(pos, text)voidHTML 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

new GeometryKernel(engine: Engine)

Properties

PropertyTypeDescription
facesMap<id, Face>All faces in the scene
edgesMap<id, Edge>All edges (standalone + face boundary)
groupsMap<id, Group>Named face collections
importedMeshesMap<id, {mesh, name}>Imported 3D models

Face Operations

createFaceFromPoints(points: Vector3[], normal: Vector3): Face

Create a new face, add it to the kernel, wire boundary edges, add mesh to scene.

splitFace(parent: Face, innerVerts: Vector3[]): {innerFace, outerFace}

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.

extrudeFace(face: Face, distance: number): Face[]

Extrude face along its normal by distance. Creates bottom, top, and side faces as independent Face objects.

sweepProfile(profileVerts, profileNormal, pathPoints, closed, color): Face[]

Sweep a 2D profile along a 3D path using parallel-transport frames. Creates side quads and cap faces. Used by FollowMeTool.

revolveProfile(profileVerts, axisOrigin, axisDir, angle, steps, color): Face[]

Revolve a profile around an axis to create a surface of revolution. Handles on-axis vertices by collapsing quads to triangles. 24 steps default.

offsetFaceVertices(face: Face, distance: number): Vector3[]

Compute polygon offset using angle bisectors. Positive = inward, negative = outward.

Edge Operations

findOrCreateEdge(a: Vector3, b: Vector3): {edge, isNew}

Reuse an existing edge matching endpoints (within eps=0.01) or create a new one. Core deduplication mechanism.

findFacesFromEdge(edge: Edge): {createdFaces, destroyedFaceData}

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

intersectFaceSets(setA: Face[], setB: Face[], skipSplitIds?, skipCoplanar?): Array

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

toJSON(engine: Engine): object

Serialize all faces, edges, groups, and camera state to a JSON-compatible object.

fromJSON(data, engine, undoManager): void

Clear all geometry and restore from JSON. Resets undo history.

Face

The primary geometric primitive. A planar polygon with optional holes.

Constructor

new Face(vertices: Vector3[], normal: Vector3)

Properties

PropertyTypeDescription
idstringUnique identifier (obj_N)
verticesVector3[]Outer boundary vertices
holesVector3[][]Inner hole polygons (wound opposite to outer)
normalVector3Face normal vector
colornumberHex color (e.g. 0xcccccc)
metalnessnumberPBR metalness (0–1)
roughnessnumberPBR roughness (0–1)
opacitynumberTransparency (0–1)
meshTHREE.MeshRendered Three.js geometry
boundaryEdgesEdge[]Edges forming the outer boundary
holeEdgesEdge[][]Edges forming each hole boundary
groupGroup | nullParent group if grouped
visiblebooleanVisibility toggle

Key Methods

MethodReturnsDescription
createMesh()THREE.MeshBuild ShapeGeometry with ear-clipping for holes. Sets explicit normals to avoid triangulation seam artifacts.
addHole(holeVerts)voidAdd a hole polygon. Winding must be opposite to outer boundary.
setColor(color)voidChange material color
setMaterial(color, metalness, roughness, opacity, textureId)voidUpdate all material properties
highlight(on)voidToggle highlight emissive color
getCenter()Vector3Compute face centroid
dispose()voidFree GPU memory
Hole winding: Holes must be wound opposite to the outer boundary in 2D space. The kernel verifies this using the shoelace formula and reverses if needed.

Edge

First-class edge primitive. All edges (standalone lines and face boundaries) are Edge objects stored in the kernel's edges map.

Constructor

new Edge(points: Vector3[])

Properties

PropertyTypeDescription
idstringUnique identifier
pointsVector3[]Polyline vertices (usually 2 points)
faceRefsSet<Face>Faces that share this edge as boundary
meshTHREE.LineRendered 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

PropertyTypeDescription
idstringUnique identifier
facesSet<Face>Member faces
namestringDisplay 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

setActiveTool(name: string): void

Deactivates the current tool and activates the named tool. Dispatches a toolChanged custom event.

Helper Functions

FunctionDescription
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

MethodDescription
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;
    }
  }
}
Edge snapshot pattern: Face-creating commands track 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

  1. Endpoint (green) — Existing face/edge vertices
  2. Midpoint (blue) — Edge midpoints
  3. Axis alignment (red/green/blue) — Cardinal alignment from reference point
  4. 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.

Exporter.toOBJ(kernel: GeometryKernel): string

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.

Exporter.toSTL(kernel: GeometryKernel): string

ASCII STL format. Extracts triangles from ShapeGeometry for faces with holes, uses fan triangulation for simple faces.

Exporter.download(content: string, filename: string): void

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

  1. startDeviceFlow() → Get user code + verification URL
  2. User visits GitHub URL, enters code
  3. pollForToken() → Receive access token
  4. validateToken() → Fetch username and avatar
  5. createRepo(name) → Private repo for project
  6. saveProject(repo, json, message) → Commit sketch3d.json
  7. listMilestones(repo) → Browse commit history
  8. getProjectAtCommit(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.

CommandDescription
CreateFaceCommandCreate face from vertices + normal
SplitFaceCommandCut hole in face, creating inner + outer
ExtrudeFaceCommandExtrude face along normal (with punch-through)
ResizeExtrusionCommandResize existing extrusion instead of creating new
CreateEdgeCommandCreate standalone edge with auto-face detection
DeleteCommandDelete face only
DeleteFaceCleanCommandDelete face + orphaned edges
DeleteEdgeCommandDelete standalone edge
DeleteEdgeWithFacesCommandDelete edge + all dependent faces
SplitFaceByLineCommandSplit face with line (outer→outer)
SplitHoleCommandSplit hole with line (hole→hole)
AbsorbHoleCommandAbsorb hole into outer boundary via bridge
MergeHolesCommandMerge two holes via connecting line
MoveFaceCommandTranslate face vertices + holes
MoveEdgeCommandMove edge endpoints
RotateFaceCommandRotate face around center/axis
ChangeColorCommandChange face color/material
ApplyMaterialCommandApply full PBR material
OffsetFaceCommandApply polygon offset to face
FillHoleCommandFill hole by removing from holes array
FollowMeCommandExecute sweep operation
RevolveCommandExecute revolve operation
IntersectFacesCommandSplit overlapping face sets
FilletFaceCommandRound face corners
ChamferEdgeCommandBevel face corners
HideFaceCommandToggle face visibility
ShowAllCommandShow all hidden faces
GroupCommandCreate group from faces
UngroupCommandDissolve group
ImportMeshCommandImport external 3D model

Tool Reference

ToolShortcutDescription
SelectToolVClick/box select. Drag to move. Shift+click multi-select. Shows bounding box gizmo.
LineToolLPlace edges. Auto-creates faces from closed loops. Splits faces with 5 boundary cases.
RectangleToolRClick-drag rectangle. Splits existing faces. Dimension labels.
CircleToolCClick center, drag radius. 24-sided polygon approximation.
PolygonToolGClick center, drag radius. Scroll wheel adjusts sides (3-24).
ArcToolA3-click arc: endpoint1, endpoint2, bulge. Sagitta formula for radius.
CurveToolSCatmull-Rom spline. Click control points, Enter to finish.
PushPullToolPExtrude faces. Snap to parallel faces. Punch-through coplanar faces. Resize existing extrusions.
MoveToolMMove faces/edges. Also integrated into SelectTool.
RotateToolQ3-click: face → center → reference. Protractor visual. 15° snap.
OffsetToolFClick face, set inward distance. Bisector polygon offset.
FollowMeToolHSweep profile along edge path. Parallel-transport frames.
RevolveToolJSpin profile around axis. 360°, 24 steps. Handles on-axis vertices.
EraseToolEDelete edges (priority) or faces. Edge deletion removes dependent faces.
PaintBucketToolBApply color. Alt+click eyedropper. Fill holes. Floating palette.
FilletToolKRound corners with arc radius.
ChamferToolD45° bevel at corners.
HideTool\Toggle face visibility.
CommentToolNAdd text annotations to scene.

Keyboard Shortcuts

KeyAction
VSelect tool
LLine tool
RRectangle tool
CCircle tool
GPolygon tool
AArc tool
SCurve tool
PPush/Pull tool
MMove tool
QRotate tool
FOffset tool
HFollow Me tool
JRevolve tool
EEraser tool
BPaint Bucket tool
KFillet tool
DChamfer tool
\Hide tool
NComment tool
IIntersect selected faces
Ctrl/Cmd+ZUndo
Ctrl/Cmd+Shift+ZRedo
Ctrl/Cmd+SSave milestone
Delete / BackspaceDelete selected (face + orphaned edges)
EscapeCancel current operation / Deselect
Space + dragTemporary pan (Photoshop-style)
Middle mousePan
Right mouseOrbit
Scroll wheelZoom