|
| 1 | +# CLAUDE.md |
| 2 | + |
| 3 | +Operating manual for Claude Code sessions in this repo. Read this in full before making changes. |
| 4 | + |
| 5 | +--- |
| 6 | + |
| 7 | +## What this project is |
| 8 | + |
| 9 | +**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). |
| 10 | + |
| 11 | +The whole app runs in the browser. There is no backend, no native shell. |
| 12 | + |
| 13 | +--- |
| 14 | + |
| 15 | +## Stack and commands |
| 16 | + |
| 17 | +- **Vite 5** + **React 18** + **TypeScript** (strict mode via `tsc &&` before `vite build`) |
| 18 | +- **No UI library**, no state manager, no router. All styles live in `src/index.css`. State is plain React `useState` in `App.tsx`. |
| 19 | +- Custom variable-weight font `GSCode` loaded from `public/fonts/`. |
| 20 | + |
| 21 | +| Command | Purpose | |
| 22 | +|---|---| |
| 23 | +| `npm install` | Install deps | |
| 24 | +| `npm run dev` | Vite dev server (typically `http://localhost:5173`) | |
| 25 | +| `npm run build` | `tsc` type-check then `vite build` → `dist/` | |
| 26 | +| `npm run preview` | Local static preview of `dist/` | |
| 27 | + |
| 28 | +There is **no** test runner, no linter config, no formatter config wired into npm scripts. Don't add one without being asked. |
| 29 | + |
| 30 | +--- |
| 31 | + |
| 32 | +## Deployment |
| 33 | + |
| 34 | +GitHub Pages, one workflow only: [.github/workflows/deploy.yml](.github/workflows/deploy.yml). On push to `main`: `npm install`, `npm run build`, upload `dist/` artifact, deploy. |
| 35 | + |
| 36 | +`vite.config.ts` uses `base: './'` so the build also works opened directly from `file://` or a USB sub-path. |
| 37 | + |
| 38 | +There is **no** desktop / Electron / portable distribution. It was removed; don't re-introduce it without explicit request. |
| 39 | + |
| 40 | +--- |
| 41 | + |
| 42 | +## Data pipeline |
| 43 | + |
| 44 | +``` |
| 45 | +SVG string |
| 46 | + └─ parseSVG() [lib/svgParser.ts] |
| 47 | + • inserts raw SVG into a hidden 1000×1000 DOM div |
| 48 | + • queries path / polyline / polygon / line / circle / ellipse / rect |
| 49 | + • arc-length sampling via getTotalLength + getPointAtLength |
| 50 | + • finite-difference tangent/normal per sample |
| 51 | + • getScreenCTM() for transforms |
| 52 | + → ParsedSVG { paths: SampledPath[], viewBox, raw } |
| 53 | +
|
| 54 | +SampledPath[] + PrintParams + WaveKeyframe[] |
| 55 | + └─ generateWaveLayers() [lib/waveGenerator.ts] |
| 56 | + • filters enabled paths |
| 57 | + • per layer: phaseBase = lissPhaseOffset + li * phaseShiftPerLayer |
| 58 | + • per point: getParamsAtT() lerps between keyframes |
| 59 | + • lissajousPoint(): offsetN = ampN*sin(2π·s/wlN + δ + phase) |
| 60 | + offsetT = ampT*sin(2π·s/wlT + phase) |
| 61 | + • applyScaleSVG: scale around pivot in SVG space |
| 62 | + • alternateDirection: reverse every other layer |
| 63 | + • closePath: append first point if not closed |
| 64 | + → WaveLayer[] { index, z (mm), paths: WavePoint[][] (SVG units) } |
| 65 | +
|
| 66 | +WaveLayer[] + PrintParams + SVGViewBox |
| 67 | + └─ generateGcode() [lib/gcodeGenerator.ts] |
| 68 | + • svgToMM() at emit time (NOT before) |
| 69 | + • reorderPaths(): nearest-neighbour O(n²) per layer |
| 70 | + • computeCentroid + skirtArcPoints for inter-path travel |
| 71 | + • buildArcPath + findCrossings + hopAtArc for z-hop |
| 72 | + • buildTransition for softJoin: smoothstep XY + linear Z, no retract |
| 73 | + • E accumulates: E += dist * extrusionMultiplier |
| 74 | + → string |
| 75 | +``` |
| 76 | + |
| 77 | +The master regeneration is a single `useEffect([parsedSVG, params, keyframes])` in `App.tsx` — every param change re-runs the entire pipeline. Re-sampling (re-parsing the raw SVG) only happens when `params.sampleSpacing` changes. |
| 78 | + |
| 79 | +--- |
| 80 | + |
| 81 | +## File map |
| 82 | + |
| 83 | +``` |
| 84 | +src/ |
| 85 | + main.tsx React root, StrictMode |
| 86 | + App.tsx Master state + layout (≈325 lines) |
| 87 | + index.css All styles (≈885 lines, design tokens in :root) |
| 88 | + types/index.ts All shared types |
| 89 | + components/ |
| 90 | + Preview2D.tsx 3D ortho canvas + timeline + kf editor (≈892 lines, LARGEST) |
| 91 | + LissajousParams.tsx Right panel: wave sliders + presets |
| 92 | + LissajousPreview.tsx Bottom center: animated Lissajous canvas |
| 93 | + PathParams.tsx Left panel: layers, speeds, options |
| 94 | + PathList.tsx Per-path overrides + collapse |
| 95 | + CenterScaleParams.tsx Bottom left: pivot + scale + z-hop |
| 96 | + CenterPad.tsx 56×56 canvas pivot picker |
| 97 | + GcodeOutput.tsx G-code textarea + copy + download |
| 98 | + NumInput.tsx Controlled number input with wheel-to-change |
| 99 | + lib/ |
| 100 | + svgParser.ts DOM-based SVG sampling |
| 101 | + waveGenerator.ts Lissajous math + keyframe interp + scale |
| 102 | + gcodeGenerator.ts G-code assembly (≈380 lines) |
| 103 | + hopUtils.ts Z-hop crossing detection |
| 104 | + skirtUtils.ts Concentric arc travel math |
| 105 | +``` |
| 106 | + |
| 107 | +--- |
| 108 | + |
| 109 | +## State and types |
| 110 | + |
| 111 | +`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.** |
| 112 | + |
| 113 | +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`). |
| 114 | + |
| 115 | +`App.tsx` state: |
| 116 | +- `params: PrintParams` — the print config |
| 117 | +- `parsedSVG: ParsedSVG | null` — sampled SVG |
| 118 | +- `layers: WaveLayer[]` — output of `generateWaveLayers` |
| 119 | +- `gcode: string` — output of `generateGcode` |
| 120 | +- `keyframes: WaveKeyframe[]` |
| 121 | +- `timelineProgress: number ∈ [0,1]` — scrubber on `Preview2D` |
| 122 | +- `gcodeFilename: string` — derived from uploaded SVG filename |
| 123 | +- `lastRawRef: { raw, spacing } | null` — kept for re-parse on spacing change |
| 124 | + |
| 125 | +--- |
| 126 | + |
| 127 | +## Coordinate / unit conventions |
| 128 | + |
| 129 | +- `WaveLayer.paths` stores points in **SVG user units**. |
| 130 | +- `svgToMM()` (in `waveGenerator.ts`) converts to **mm** only at G-code emit time. |
| 131 | +- `params.lissAmpN` / `lissAmpT` are in **mm** and divided by `scaleFactor` before use inside the wave math (so the visual amplitude tracks the real-world mm regardless of SVG unit scale). |
| 132 | +- Transform order: `scaleFactor` (SVG→mm) is separate from `scaleX/Y` (shape scale around pivot). `applyScaleSVG` handles `scaleX/Y` in SVG space before `svgToMM` converts. |
| 133 | +- `flipY`: most printers want Y growing upward; SVG Y grows downward. |
| 134 | + |
| 135 | +--- |
| 136 | + |
| 137 | +## Conventions to follow |
| 138 | + |
| 139 | +1. **All print params live in one flat `PrintParams`.** No nested objects. No context, no zustand, no redux. |
| 140 | +2. **Panels receive `params` + `onChange`.** Update with the spread pattern: `onChange({ ...params, [k]: v })`. |
| 141 | +3. **Component-local helpers.** `Num`, `Slider`, `Check`, `Sec` are defined inside each panel file. Don't extract them into a shared module. |
| 142 | +4. **Canvas rendering** lives inside `useEffect` with the full dependency array — the canvas redraws on every relevant change. Don't memoize drawing. |
| 143 | +5. **Stable-refs pattern for window-level handlers.** Refs are updated in a separate `useEffect([dep])`, then read inside a `useEffect([])` that attaches `window` listeners once. See `Preview2D.tsx` keyframe drag code for the canonical example. |
| 144 | +6. **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.) |
| 145 | +7. **No undo/redo.** State changes are permanent until the user changes them again. |
| 146 | +8. **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. |
| 147 | +9. **Don't add error handling for impossible scenarios.** Trust internal code. Only validate at boundaries (user input, SVG parsing). |
| 148 | +10. **No backwards-compat shims.** This project has no public API; just change the code. |
| 149 | + |
| 150 | +--- |
| 151 | + |
| 152 | +## Architectural sharp edges and known issues |
| 153 | + |
| 154 | +These are documented hazards. Fix them when you touch the area, but don't go on cleanup sweeps without being asked. |
| 155 | + |
| 156 | +- **`Preview2D.tsx` is ≈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. |
| 157 | +- **`ParsedSVG.raw` duplicates `App.tsx`'s `lastRawRef.current.raw`.** Either could be removed. |
| 158 | +- **`bottomSplit.size`** is computed via `useResize` in `App.tsx` but never applied to layout. The drag handle is wired; the consumer isn't. |
| 159 | +- **`resampleSVG` in `lib/svgParser.ts`** is dead code. |
| 160 | +- **Z-hop is silently skipped for layer arc paths > 600 points** (`hopUtils.ts` MAX_PTS). No user-facing warning. |
| 161 | +- **`CenterPad`'s drag has no window-level handler** — drag stops abruptly at the 56×56 canvas boundary on fast moves. |
| 162 | +- **`skirtThreshold` lives under the "Velocidades" UI section** in `PathParams.tsx` but conceptually belongs with travel options. |
| 163 | + |
| 164 | +--- |
| 165 | + |
| 166 | +## When working on G-code generation |
| 167 | + |
| 168 | +This is the most subtle area. Before changing `lib/gcodeGenerator.ts`: |
| 169 | + |
| 170 | +- 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. |
| 171 | +- Inter-path travel logic depends on three conditions: `isFirstMove`, `params.softJoin`, and `pi > 0` (path index within the layer). Layer→layer transitions only happen on the **last** path of a layer when `softJoin` is true. |
| 172 | +- Coordinate conversion via `svgToMM()` happens at emit time. Never pre-convert `WaveLayer.paths` to mm; downstream code expects SVG units there. |
| 173 | +- E accumulates monotonically (`E += dist * extrusionMultiplier`). If you add new motion, account for E unless `params.generateE` is false. |
| 174 | +- Clay printers don't retract. Don't introduce retraction moves. |
| 175 | + |
| 176 | +--- |
| 177 | + |
| 178 | +## When working on the canvas previews |
| 179 | + |
| 180 | +- `Preview2D` uses orthographic projection: `project(x, y, z, azimuth, elevation)`. Screen coords are `[offsetX + px*scale, offsetY + py*scale]`. |
| 181 | +- Layer colors are an HSL gradient from cobalt to terracotta: `hsl(218→20, 72%→88%, 50%→62%)`. |
| 182 | +- `LissajousPreview` cancels its `requestAnimationFrame` when both `ampN` and `ampT` are ≈0. Don't remove this — it's a battery and CPU optimization. |
| 183 | +- Auto-fit triggers in `LissajousPreview` are explicit and key off specific param changes (currently `lissAmpN`, `lissAmpT`). Pan/zoom are user-only. |
| 184 | + |
| 185 | +--- |
| 186 | + |
| 187 | +## CSS |
| 188 | + |
| 189 | +- Design tokens in `:root` at the top of `src/index.css`. Use them; don't hardcode colours. |
| 190 | +- Smallest text size is **9px**. Don't go below. |
| 191 | +- `--muted` is `#6A6762`. `--accent` is `#4F46E5` (indigo). The art direction is "Swiss warm-paper" — restrained, off-white background, single accent. |
| 192 | +- Sliders apply a dynamic `linear-gradient` fill inline (see `LissajousParams.Slider`). Keep this pattern when adding new sliders. |
| 193 | +- `body.dragging-h` / `body.dragging-v` are added during resize drags to override the cursor globally. |
| 194 | + |
| 195 | +--- |
| 196 | + |
| 197 | +## What's gitignored (and why it matters) |
| 198 | + |
| 199 | +`.gitignore` excludes `node_modules/`, **`package-lock.json`**, `dist/`, `release/`, `.vite/`, env files, editor settings, OS junk, and logs. |
| 200 | + |
| 201 | +The lockfile being gitignored is unusual — accept it; don't try to commit it. |
| 202 | + |
| 203 | +`release/` was the Electron build output and is gitignored for legacy reasons; nothing writes to it any more. |
| 204 | + |
| 205 | +--- |
| 206 | + |
| 207 | +## Commit and review hygiene |
| 208 | + |
| 209 | +- Conventional-commit prefixes are welcome but not enforced. The existing log mixes `feat:`, `fix:`, `chore:`, `ci:`, and plain prose. |
| 210 | +- 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. |
| 211 | +- The remote is at [Cranmellar/curvabarro](https://github.com/Cranmellar/curvabarro) on GitHub. The repo name on GitHub has not been updated to match the new project name (`barrocode`); leave that for the user to do. |
0 commit comments