-
Notifications
You must be signed in to change notification settings - Fork 0
architecture
This document describes the codebase structure, key design decisions, and how the major subsystems fit together.
| Layer | Technology |
|---|---|
| UI framework | Svelte 5 (runes-compatible, but using Svelte 4 reactivity syntax) |
| Build tool | Vite 8 |
| 3D rendering | Three.js 0.183 |
| Testing | Playwright |
| Linting | ESLint 10 + eslint-plugin-svelte |
| Type checking | svelte-check (JSDoc types, no TypeScript source files) |
gcode/
├── src/
│ ├── App.svelte # Root component — tab bar + tab panels
│ ├── stores/
│ │ └── ui.js # Shared Svelte stores (activeTab, activePaths, …)
│ ├── modules/ # One Svelte component per tab
│ │ ├── WaveRecorder.svelte # Wave tab — audio capture + 3D preview
│ │ ├── GcodeHandler.svelte # G-code tab — preview, export, playback
│ │ ├── SerialTransmission.svelte # Serial tab — GRBL connection
│ │ └── GenArt.svelte # Gen Art tab — generative art + plugin system
│ └── lib/ # Shared libraries
│ ├── gcode.js # G-code generation, NDC↔paper mapping, path sort/dedup
│ ├── visualizer.js # Three.js scene + built-in visualization shapes
│ ├── pluginLoader.js # Runtime loader for visualization plugins
│ ├── genartPluginLoader.js # Runtime loader for GenArt plugins
│ ├── recorder.js # Audio capture + frame buffering
│ ├── imageTrace.js # Marching-squares raster-to-vector tracing
│ └── genart/ # Built-in GenArt algorithm modules
│ ├── attractors.js # Strange attractors (Lorenz, Rössler, …)
│ ├── cyclicCA.js # Cyclic cellular automaton
│ ├── harmonograph.js # Damped two-pendulum parametric curves
│ ├── juliaContours.js # Julia set contours
│ ├── lsystem.js # L-system turtle graphics
│ ├── noiseContours.js # Perlin noise iso-contours
│ ├── reactionDiffusion.js # Gray-Scott reaction-diffusion
│ ├── spaceFilling.js # Space-filling L-system curves
│ ├── spirograph.js # Hypo/epi-cycloid curves
│ ├── superformula.js # Gielis superformula
│ └── _marchingSquares.js # Shared marching-squares helper
├── examples/
│ └── voronoi-cells.genart.js # Example GenArt plugin (Voronoi cells)
├── tests/
│ └── *.spec.js # Playwright smoke / visual regression tests
├── docs/ # This documentation
│ ├── user-guide.md
│ ├── genart-plugin-api.md
│ ├── viz-plugin-api.md
│ └── architecture.md # ← you are here
└── style.css # Global styles
The app uses three coordinate spaces. Understanding the mapping between them is critical for G-code generation.
The internal 2D coordinate system used by all GenArt algorithms and the G-code export pipeline:
- Range:
[-1, +1]on both axes - Origin
(0, 0)= centre of the plot area -
nx = -1= left edge,nx = +1= right edge -
ny = -1= bottom edge,ny = +1= top edge (Y-up) - Machine home
(0, 0)maps to approximately NDC(-1.1, -1.07)(just outside the plot area)
Physical coordinates on the paper:
_ndcToPaper(nx, ny, sx, sy, offsetX, offsetY)
→ { px: mm, py: mm }
-
CENTER_X = MARGIN + PLOT_W/2 = 105 mm(A4 horizontal centre) -
CENTER_Y = MARGIN + PLOT_H/2 = 148.5 mm(A4 vertical centre) -
sx, syare half-extents from_ndcScales(aspect)— aspect-ratio preserving
Used by the Wave tab. Coordinates are in abstract "scene units":
- Scene width: 20 units (X: −10 to +10)
- Scene depth: 20 units (Z: 0 to +20, frames stack along Z)
- Y = vertical amplitude
Projected to NDC via perspective projection using the current camera pose.
Core G-code generation. Public exports:
| Export | Description |
|---|---|
framesToGCode(frames, config) |
Waveform / audio visualization G-code |
projectedPathsToGCode(paths, config) |
NDC paths → G-code (GenArt, image trace) |
anaglyphPathsToGCode(left, right, config) |
Two-pass stereo G-code |
imageGCode(contourPaths, shadingPasses, config) |
Multi-pass image tracing G-code |
parseGCodePaths(content) |
Parse G-code text back to {x,y} paths |
sortPaths(paths) |
Nearest-neighbour sort minimising travel |
ndcScales(aspect, plotW, plotH) |
Compute {sx, sy} half-extents |
Path sort: _sortPaths() seeds the nearest-neighbour search from NDC (-1, -1) (machine home corner) so the first pen move is as short as possible. For single-path algorithms, it reverses the path if the tail is closer to home. _deduplicatePaths() removes duplicate segments before sorting.
Three.js scene management. Key points:
- All built-in shapes registered in
BUILTIN_SHAPESarray. - Plugin contract defined in JSDoc at the top of the file (authoritative spec).
-
getProjectedPaths()uses Three.jsraycaster/ camera frustum math to project 3D lines to 2D NDC.
Runtime GenArt plugin installation:
- Validation — checks required fields, param schema, type constraints.
-
Evaluation — loads code via Blob URL (
import(blobUrl)). -
Persistence — stores
{id, code}inlocalStorageas JSON. - Restore — re-evaluates all stored plugins on page load.
The Gen Art tab. Key reactive state:
| Variable | Type | Description |
|---|---|---|
selectedId |
string |
ID of the active algorithm |
algorithms |
object[] |
Merged built-in + user plugin list |
paramsByAlg |
Record<id, Record<paramId, value>> |
Per-algorithm param storage |
currentPaths |
Array<Array<{nx,ny}>> |
Last generated paths (canvas + export) |
Reactive rules:
-
selectedIdis reset toalgorithms[0].idif the selected plugin is uninstalled. - Params are merged (defaults + stored values) whenever the selected algorithm changes or a plugin is reinstalled.
- Canvas re-renders via
_renderPaths(currentPaths)which draws guide paths first (ifselected.guideexists), then main paths.
There are two independent plugin systems:
| System | Manages | Stored in localStorage | API doc |
|---|---|---|---|
| GenArt plugins | genartPluginLoader.js |
gcode-genart-user-plugins |
genart-plugin-api.md |
| Visualization plugins | pluginLoader.js |
gcode-viz-user-plugins |
viz-plugin-api.md |
Both use the same Blob URL evaluation pattern:
const blob = new Blob([code], { type: 'text/javascript' });
const url = URL.createObjectURL(blob);
const mod = await import(url);
URL.revokeObjectURL(url);Shared reactive state between tabs:
| Store | Type | Description |
|---|---|---|
activeTab |
'wave' | 'gcode' | 'serial' | 'genart' |
Currently visible tab |
activePaths |
Array<Array<{nx,ny}>> |
Paths passed from Wave/GenArt to G-code tab |
shadingPasses |
{label, paths}[] |
Multi-pass shading paths from image tracer |
exportParams |
object |
Metadata snapshot for G-code comments and filenames |
cameraAspect |
number |
Camera aspect ratio for aspect-correct NDC mapping |
settings |
object |
Persisted settings (offsets, scale, pen config) |
Generated G-code follows this structure:
; <app name> — <description>
; Paper: <dims>, <margins>
; --- Generation Parameters ---
; Algorithm ID: spirograph
; Algorithm: Spirograph
; R: 5
; r: 3
G21 ; Units: millimetres
G90 ; Absolute positioning
G0 Z5.000 ; Pen up
G0 X14.750 Y16.925 ; Travel to first path
G1 Z0.000 F300 ; Pen down
G1 X… Y… F2000
…
G0 Z5.000 ; Pen up
G0 X0.000 Y0.000 ; Return to machine home
M2 ; End of programnpm test # Playwright end-to-end tests
npm run lint # ESLint
npm run check # svelte-check (type checking via JSDoc)Playwright tests cover smoke tests for all tabs and visual regression tests for visualization shapes.
- Branch from
main. - Run
npm run lint && npm run check— fix any errors before committing. - Commits must be GPG-signed (
git commit -S). - Open a PR targeting
main. Thereleaseskill handles versioning, CHANGELOG, and merge.