Operating manual for Claude Code sessions in this repo. Read this in full before making changes.
BarroCode — a web app that turns SVG curves into G-code for clay 3D printers. The SVG paths are sampled, modulated by a Lissajous wave (normal + tangent oscillation along arc length), stacked into layers, and emitted as G-code with clay-specific behaviour (no retract, soft layer joins, optional dwell/priming, concentric skirt travel, parabolic z-hop at self-intersections).
The whole app runs in the browser. There is no backend, no native shell.
- Vite 5 + React 18 + TypeScript (strict mode via
tsc &&beforevite build) - No UI library, no state manager, no router. All styles live in
src/index.css. State is plain ReactuseStateinApp.tsx. - Custom variable-weight font
GSCodeloaded frompublic/fonts/.
| Command | Purpose |
|---|---|
npm install |
Install deps |
npm run dev |
Vite dev server (typically http://localhost:5173) |
npm run build |
tsc type-check then vite build → dist/ |
npm run preview |
Local static preview of dist/ |
There is no test runner, no linter config, no formatter config wired into npm scripts. Don't add one without being asked.
GitHub Pages, one workflow only: .github/workflows/deploy.yml. On push to main: npm install, npm run build, upload dist/ artifact, deploy.
vite.config.ts uses base: './' so the build also works opened directly from file:// or a USB sub-path.
There is no desktop / Electron / portable distribution. It was removed; don't re-introduce it without explicit request.
SVG string
└─ parseSVG() [lib/svgParser.ts]
• inserts raw SVG into a hidden 1000×1000 DOM div
• queries path / polyline / polygon / line / circle / ellipse / rect
• arc-length sampling via getTotalLength + getPointAtLength
• finite-difference tangent/normal per sample
• getScreenCTM() for transforms
→ ParsedSVG { paths: SampledPath[], viewBox, raw }
SampledPath[] + PrintParams + WaveKeyframe[]
└─ generateWaveLayers() [lib/waveGenerator.ts]
• filters enabled paths
• per layer: phaseBase = lissPhaseOffset + li * phaseShiftPerLayer
• per point: getParamsAtT() lerps between keyframes
• lissajousPoint(): offsetN = ampN*sin(2π·s/wlN + δ + phase)
offsetT = ampT*sin(2π·s/wlT + phase)
offsetZ = ampZ*sin(2π·s/wlZ + phaseZ + phase)
• applyScaleSVG: scale around pivot in SVG space
• alternateDirection: reverse every other layer
• closePath: append first point if not closed
→ WaveLayer[] { index, z (mm), paths: WavePoint[][] (SVG units) }
WaveLayer[] + PrintParams + SVGViewBox
└─ generateGcode() [lib/gcodeGenerator.ts]
• svgToMM() at emit time (NOT before)
• reorderPaths(): nearest-neighbour O(n²) per layer
• computeCentroid + skirtArcPoints for inter-path travel
• buildArcPath + findCrossings + hopAtArc for z-hop
• buildTransition for softJoin: smoothstep XY + linear Z, no retract
• E accumulates: E += dist * extrusionMultiplier
→ string
Regeneration in App.tsx is split into two effects: wave layers re-run immediately on [parsedSVG, params, keyframes]; G-code is debounced 400 ms (via gcodeTimerRef) to avoid freezing the UI during slider/keyframe edits. Re-sampling (re-parsing the raw SVG) only happens when params.sampleSpacing changes.
src/
main.tsx React root, StrictMode
App.tsx Master state + layout (≈507 lines)
index.css All styles (≈1086 lines, design tokens in :root)
types/index.ts All shared types
components/
Preview2D.tsx 3D ortho canvas + timeline + kf editor (≈892 lines, LARGEST)
LissajousParams.tsx Right panel: wave sliders + presets
LissajousPreview.tsx Bottom center: animated Lissajous canvas
PathParams.tsx Left panel: layers, speeds, options
PathList.tsx Per-path overrides + collapse
CenterScaleParams.tsx Bottom left: pivot + scale + z-hop
CenterPad.tsx 56×56 canvas pivot picker
GcodeOutput.tsx G-code textarea + copy + download
NumInput.tsx Controlled number input with wheel-to-change
lib/
svgParser.ts DOM-based SVG sampling
waveGenerator.ts Lissajous math + keyframe interp + scale
gcodeGenerator.ts G-code assembly (≈327 lines)
hopUtils.ts Z-hop crossing detection
skirtUtils.ts Concentric arc travel math
PrintParams is a single flat object with ≈50 fields covering sampling, Lissajous wave, layers, soft-join, SVG→mm transform, shape transform, z-hop, travel, speeds, extrusion, path options, and clay-specific behaviour. Do not nest it, do not split it into multiple stores.
Keyframes (WaveKeyframe[]) override Lissajous fields at given t ∈ [0,1]. Between keyframes, all fields are linearly interpolated (wlN/wlT floor-clamped at 0.1). When keyframes.length > 0, the per-path overrides (ampNOverride etc. on SampledPath) are silently ignored by waveGenerator — the UI disables them in this case (see PathList.tsx).
App.tsx state:
params: PrintParams— the print configparsedSVG: ParsedSVG | null— sampled SVGlayers: WaveLayer[]— output ofgenerateWaveLayersgcode: string— output ofgenerateGcodekeyframes: WaveKeyframe[]timelineProgress: number ∈ [0,1]— scrubber onPreview2DgcodeFilename: string— derived from uploaded SVG filenamelastRawRef: { raw, spacing } | null— kept for re-parse on spacing changegcodeTimerRef— debounce handle for G-code regeneration (400 ms)centerTab: 'preview' | 'gcode'— which tab is active in the center panelsampleIndex: number— which inline sample SVG is shown in the sample navigatorselectedKfId: string | null— id of the currently selected keyframe
WaveLayer.pathsstores points in SVG user units.svgToMM()(inwaveGenerator.ts) converts to mm only at G-code emit time.params.lissAmpN/lissAmpTare in mm and divided byscaleFactorbefore use inside the wave math (so the visual amplitude tracks the real-world mm regardless of SVG unit scale).- Transform order:
scaleFactor(SVG→mm) is separate fromscaleX/Y(shape scale around pivot).applyScaleSVGhandlesscaleX/Yin SVG space beforesvgToMMconverts. flipY: most printers want Y growing upward; SVG Y grows downward.
- All print params live in one flat
PrintParams. No nested objects. No context, no zustand, no redux. - Panels receive
params+onChange. Update with the spread pattern:onChange({ ...params, [k]: v }). - Component-local helpers.
Num,Slider,Check,Secare defined inside each panel file. Don't extract them into a shared module. - Canvas rendering lives inside
useEffectwith the full dependency array — the canvas redraws on every relevant change. Don't memoize drawing. - Stable-refs pattern for window-level handlers. Refs are updated in a separate
useEffect([dep]), then read inside auseEffect([])that attacheswindowlisteners once. SeePreview2D.tsxkeyframe drag code for the canonical example. - Spanish UI throughout. Labels, tooltips, section titles, error messages — all Spanish. Code, identifiers, and comments — English. (Existing files mix in some Spanish comments; keep new comments English.)
- No undo/redo. State changes are permanent until the user changes them again.
- Default to writing no comments. Only add a comment when the WHY is non-obvious (a hidden constraint, a workaround, a subtle invariant). Never explain WHAT well-named code already says, and never reference the current task / fix / caller.
- Don't add error handling for impossible scenarios. Trust internal code. Only validate at boundaries (user input, SVG parsing).
- No backwards-compat shims. This project has no public API; just change the code.
These are documented hazards. Fix them when you touch the area, but don't go on cleanup sweeps without being asked.
Preview2D.tsxis ≈892 lines and overdue for a split. The canvas draw code is the obvious extraction (→lib/draw3D.ts). Don't refactor it preemptively; do it when adding a feature that would otherwise inflate the file further.ParsedSVG.rawduplicatesApp.tsx'slastRawRef.current.raw. Either could be removed.resampleSVGinlib/svgParser.tsis dead code.- Z-hop is silently skipped for layer arc paths > 600 points (
hopUtils.tsMAX_PTS). No user-facing warning. CenterPad's drag has no window-level handler — drag stops abruptly at the 56×56 canvas boundary on fast moves.skirtThresholdlives under the "Velocidades" UI section inPathParams.tsxbut conceptually belongs with travel options.
This is the most subtle area. Before changing lib/gcodeGenerator.ts:
- Generate a sample with a 2- or 3-path SVG and read the output. The header comment block lists every parameter — use it to sanity-check.
- Inter-path travel logic depends on three conditions:
isFirstMove,params.softJoin, andpi > 0(path index within the layer). Layer→layer transitions only happen on the last path of a layer whensoftJoinis true. - Coordinate conversion via
svgToMM()happens at emit time. Never pre-convertWaveLayer.pathsto mm; downstream code expects SVG units there. - E accumulates monotonically (
E += dist * extrusionMultiplier). If you add new motion, account for E unlessparams.generateEis false. - Clay printers don't retract. Don't introduce retraction moves.
Preview2Duses orthographic projection:project(x, y, z, azimuth, elevation). Screen coords are[offsetX + px*scale, offsetY + py*scale].- Layer colors are an HSL gradient from cobalt to terracotta:
hsl(218→20, 72%→88%, 50%→62%). LissajousPreviewcancels itsrequestAnimationFramewhen bothampNandampTare ≈0. Don't remove this — it's a battery and CPU optimization.- Auto-fit triggers in
LissajousPrevieware explicit and key off specific param changes (currentlylissAmpN,lissAmpT). Pan/zoom are user-only.
- Design tokens in
:rootat the top ofsrc/index.css. Use them; don't hardcode colours. - Smallest text size is 9px. Don't go below.
--mutedis#6A6762.--accentis#4F46E5(indigo). The art direction is "Swiss warm-paper" — restrained, off-white background, single accent.- Sliders apply a dynamic
linear-gradientfill inline (seeLissajousParams.Slider). Keep this pattern when adding new sliders. body.dragging-h/body.dragging-vare added during resize drags to override the cursor globally.
.gitignore excludes node_modules/, package-lock.json, dist/, release/, .vite/, env files, editor settings, OS junk, and logs.
The lockfile being gitignored is unusual — accept it; don't try to commit it.
release/ was the Electron build output and is gitignored for legacy reasons; nothing writes to it any more.
When you need context, this is where to look:
- README.md — public overview with mermaid diagrams.
- CLAUDE.md — this file. Operating manual for Claude sessions.
- docs/usage.md — user-facing flow and parameter reference.
- docs/architecture.md — pipeline detail (more technical than this file's diagram).
- docs/fabrication-notes.md — clay-printer criteria informing the design.
- docs/research-notes.md — open research notes, unpriorized ideas. Read before suggesting any new feature.
- pendientes.md — actionable backlog, prioritized in waves. Read before starting any new ticket.
- docs/performance-optimization-spec.md — execution spec for preview caching, LOD, G-code debounce, and worker offload (Ola 8).
- Conventional-commit prefixes are welcome but not enforced. The existing log mixes
feat:,fix:,chore:,ci:, and plain prose. - Keep commits scoped to one logical change. If a change spans entangled files (rename + bug fix in the same function, etc.), split it manually — don't lump everything into a "misc" commit.
- The remote is at Cranmellar/barrocode on GitHub.