architecture - rswebdev/gcode GitHub Wiki
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.