test: Added CORSMiddleware to engine, scaffolded Vite+React+TS app with…
- "engine/main.py" - "app/vite.config.ts" - "app/src/types/engine.ts" - "app/src/api/engine.ts" - "app/src/api/__tests__/engine.test.ts" - "app/src/App.tsx" - "app/src/test-setup.ts" - "app/tsconfig.app.json" GSD-Task: S01/T01
This commit is contained in:
parent
c3c5a9b082
commit
9be90a4494
36 changed files with 7102 additions and 7 deletions
|
|
@ -10,3 +10,4 @@
|
|||
| D002 | | architecture | Build order and gating strategy | Build Engine first (M001), then App canvas (M002), then Export+Deploy+Embed (M003). Human checkpoints gate each transition. | Brief explicitly mandates: validate engine output quality before building canvas UI, validate canvas before export/deploy. Engine is the hardest and most valuable piece. | No | human |
|
||||
| D003 | | engine | Post-processing replaces regex metadata extraction | postprocess_svg() pipeline replaces old _extract_svg_metadata() regex approach; all SVG paths are parsed into structured PathInfo objects with coordinates, area, island status | Structured path data is needed for DXF/JSON output generation, RDP simplification, and island detection. XML parsing is now necessary since we need actual coordinate data, not just counts. This supersedes D003's regex approach. | No | agent |
|
||||
| D004 | | architecture | Engine Docker image build strategy | Multi-stage build: builder stage compiles pypotrace with build-essential/libagg-dev/libpotrace-dev, runtime stage uses python:3.11-slim with only runtime libs (libpotrace0, libagg2, curl). Only engine source is copied — no App code. | Multi-stage keeps the runtime image small by excluding build tools. Copying only engine source enforces the Engine/App separation (D001). curl is included for Docker HEALTHCHECK. | Yes | agent |
|
||||
| D005 | | architecture | Frontend technology stack for Kerf App | Vite + React + TypeScript with plain CSS modules, Vitest for testing, raw fetch + FormData for engine API calls, no UI framework for V1 | Minimal dependency surface for a tool that needs fast iteration. The engine API uses multipart/form-data which works naturally with native fetch + FormData. CSS modules avoid global style conflicts without adding a framework. Vitest integrates natively with Vite's transform pipeline. No UI library needed — the app has a focused interface with ~8 components. | Yes | agent |
|
||||
|
|
|
|||
|
|
@ -12,3 +12,5 @@
|
|||
{"cmd":"complete-task","params":{"milestoneId":"M001","sliceId":"S03","taskId":"T02"},"ts":"2026-03-26T04:49:33.566Z","actor":"agent","hash":"fc8b517936769f11","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||
{"cmd":"complete-slice","params":{"milestoneId":"M001","sliceId":"S03"},"ts":"2026-03-26T04:52:09.459Z","actor":"agent","hash":"c346f9b623e5659d","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||
{"cmd":"complete-milestone","params":{"milestoneId":"M001"},"ts":"2026-03-26T04:56:43.004Z","actor":"agent","hash":"c877176040436ab9","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||
{"cmd":"plan-slice","params":{"milestoneId":"M002","sliceId":"S01"},"ts":"2026-03-26T05:01:43.661Z","actor":"agent","hash":"d83541fb49b2737b","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||
{"cmd":"complete-task","params":{"milestoneId":"M002","sliceId":"S01","taskId":"T01"},"ts":"2026-03-26T05:05:22.658Z","actor":"agent","hash":"59aebe24d8f53b7a","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,120 @@
|
|||
# S01: Import & Convert UI (View 1)
|
||||
|
||||
**Goal:** Build View 1 (Import & Convert) with Engine API integration, preset selector, tuning sliders with live preview, and output info bar
|
||||
**Goal:** Build the Import & Convert view (View 1) as a React SPA that integrates with the Engine API — users upload a raster image, select a vectorization preset, tune parameters via sliders, see a live SVG preview updated via debounced re-trace, and click "Use This" to advance to the Design Canvas.
|
||||
**Demo:** After this: Upload a PNG, see live preview with preset selection and slider controls, click 'Use This' to advance to canvas
|
||||
|
||||
## Tasks
|
||||
- [x] **T01: Added CORSMiddleware to engine, scaffolded Vite+React+TS app with dev proxy, and built typed API client with 9 passing tests** — This task unblocks all frontend work by: (1) adding CORSMiddleware to engine/main.py so browser requests succeed, (2) scaffolding the app/ directory as a Vite + React + TypeScript project with dev proxy to the engine, and (3) building a typed API client with TypeScript interfaces matching the engine's request/response shapes.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add `CORSMiddleware` to `engine/main.py` — import from `fastapi.middleware.cors`, allow all origins for dev (restrict in production later). Place it before the router include.
|
||||
2. Scaffold `app/` using `npm create vite@latest app -- --template react-ts`. Run `cd app && npm install`.
|
||||
3. Configure `app/vite.config.ts` with a dev proxy: requests to `/engine/*` forward to `http://localhost:8000`.
|
||||
4. Create `app/src/types/engine.ts` with TypeScript interfaces:
|
||||
- `PresetConfig` — matches the JSON structure in `engine/presets/*.json` (name, description, preprocessing, vectorization, postprocessing sections)
|
||||
- `TraceResponse` — `{ output: string, format: string, metadata: TraceMetadata }`
|
||||
- `TraceMetadata` — `{ format: string, path_count: number, node_count_total: number, open_paths: number, island_count: number, warnings: string[], processing_ms: number }`
|
||||
- `PresetsResponse` — `{ presets: Record<string, PresetConfig> }`
|
||||
5. Create `app/src/api/engine.ts` with typed functions:
|
||||
- `getPresets(): Promise<PresetsResponse>` — calls `GET /engine/presets`
|
||||
- `traceImage(file: File, preset: string, params: Record<string, unknown>, signal?: AbortSignal): Promise<TraceResponse>` — builds `FormData` with file, mode, output_format='svg', preset, and params (JSON.stringify), calls `POST /engine/trace`
|
||||
- `simplifyVector(file: File, epsilon: number, signal?: AbortSignal): Promise<TraceResponse>` — calls `POST /engine/simplify`
|
||||
6. Remove Vite boilerplate: delete `app/src/App.css`, the counter component content from `App.tsx`, and the Vite/React logos.
|
||||
7. Add Vitest as dev dependency: `cd app && npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom`. Configure in `vite.config.ts` with `test: { environment: 'jsdom', globals: true }`.
|
||||
8. Write `app/src/api/__tests__/engine.test.ts` — unit tests for the API client using `vi.fn()` to mock fetch. Test that `traceImage()` builds correct FormData, `getPresets()` calls the right URL, and AbortSignal is passed through.
|
||||
- Estimate: 45m
|
||||
- Files: engine/main.py, app/package.json, app/vite.config.ts, app/tsconfig.json, app/src/types/engine.ts, app/src/api/engine.ts, app/src/api/__tests__/engine.test.ts, app/src/App.tsx
|
||||
- Verify: cd app && npx vitest run --reporter=verbose 2>&1 | tail -20
|
||||
- [ ] **T02: Build app shell with view routing, file upload zone, and preset selector** — Build the first user-facing components: the app shell with view-state routing (Import → Canvas → Export), a drag-and-drop file upload zone, and a preset selector that fetches presets from the engine and populates parameter defaults.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create `app/src/views/ImportConvert.tsx` — the main View 1 container. Layout: left panel (upload + controls) and right panel (preview area, placeholder for now). Use CSS modules or a single `app/src/views/ImportConvert.module.css`.
|
||||
2. Update `app/src/App.tsx` with view-state routing using React useState:
|
||||
- `type ViewState = 'import' | 'canvas' | 'export'`
|
||||
- Render `ImportConvert` when state is 'import', placeholder `<div>View 2: Design Canvas</div>` for 'canvas', placeholder for 'export'
|
||||
- Pass a callback `onUseThis(svgOutput: string, metadata: TraceMetadata)` that transitions to 'canvas'
|
||||
3. Create `app/src/components/FileUpload.tsx`:
|
||||
- Drag-and-drop zone with `onDragOver`/`onDrop` handlers + hidden file `<input>` fallback
|
||||
- Accept: `.png,.jpg,.jpeg,.bmp,.tiff,.webp,.svg` via accept attribute
|
||||
- On file selection: store `File` in parent state, show filename + file size, render thumbnail via `URL.createObjectURL()`
|
||||
- Detect SVG uploads (check file type or extension) and surface this to parent for mode switching
|
||||
4. Create `app/src/components/PresetSelector.tsx`:
|
||||
- On mount: call `getPresets()` from `app/src/api/engine.ts`
|
||||
- Render preset cards — each shows name and description from the preset config
|
||||
- Clicking a card selects it (visual highlight) and calls `onPresetSelect(presetName, presetConfig)` callback
|
||||
- Default selection: 'sign' preset
|
||||
5. Create `app/src/App.css` (or global styles) with minimal layout: flexbox two-column layout for View 1, card styles for presets, drop zone styling with dashed border + hover state.
|
||||
6. Wire components together in `ImportConvert.tsx`: FileUpload at top, PresetSelector below it, both in the left panel. Store selected file and preset in local state.
|
||||
- Estimate: 1h
|
||||
- Files: app/src/App.tsx, app/src/App.css, app/src/views/ImportConvert.tsx, app/src/views/ImportConvert.module.css, app/src/components/FileUpload.tsx, app/src/components/PresetSelector.tsx
|
||||
- Verify: cd app && npx vitest run --reporter=verbose 2>&1 | tail -20 && npx tsc --noEmit 2>&1 | tail -10
|
||||
- [ ] **T03: Implement parameter sliders and debounced live SVG preview with AbortController** — Build the core interaction loop: parameter sliders derived from preset defaults, debounced re-trace on any change, AbortController-based request cancellation, and inline SVG rendering. This is the highest-risk piece of the slice.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create `app/src/hooks/useDebouncedTrace.ts` — a custom hook that encapsulates the debounce + abort logic:
|
||||
- Accepts: `file: File | null`, `preset: string`, `params: Record<string, unknown>`, `debounceMs: number` (default 300)
|
||||
- Maintains an `AbortController` ref. On each trigger: clear previous timeout, abort previous request, set new timeout
|
||||
- After debounce: call `traceImage(file, preset, params, signal)` from the API client
|
||||
- Returns: `{ svgOutput: string | null, metadata: TraceMetadata | null, isLoading: boolean, error: string | null }`
|
||||
- On unmount: abort any in-flight request and clear timeout
|
||||
- For SVG file uploads: call `simplifyVector()` instead of `traceImage()`
|
||||
2. Create `app/src/components/ParameterSliders.tsx`:
|
||||
- Accepts current preset config and an `onChange(params)` callback
|
||||
- Renders labeled range inputs for key parameters:
|
||||
- **Detail Level** (epsilon): range 0.5–10, step 0.5, from postprocessing.epsilon
|
||||
- **Noise Filter** (filter_speckle/turdsize): range 0–50, step 1, from vectorization params
|
||||
- **Smooth Curves** (alphamax): range 0–1.334, step 0.1, from vectorization.potrace.alphamax
|
||||
- **Corner Threshold** (corner_threshold): range 0–180, step 1, from vectorization.vtracer.corner_threshold (only shown for vtracer mode)
|
||||
- Show/hide sliders based on vectorization mode (potrace vs vtracer) from preset
|
||||
- Each slider change calls `onChange` with the full current params object
|
||||
- Show current numeric value next to each slider
|
||||
3. Create `app/src/components/SvgPreview.tsx`:
|
||||
- Accepts `svgOutput: string | null`, `isLoading: boolean`, `error: string | null`
|
||||
- When loading: show a spinner/overlay
|
||||
- When error: show error message with retry suggestion
|
||||
- When SVG available: render via `dangerouslySetInnerHTML` inside a container div
|
||||
- Strip `width` and `height` attributes from the SVG string (keep viewBox) for responsive scaling
|
||||
- Apply CSS: `width: 100%; height: auto; max-height: 70vh; object-fit: contain` on the container
|
||||
4. Wire everything in `ImportConvert.tsx`:
|
||||
- Track `currentParams` state, initialized from selected preset defaults
|
||||
- When preset changes: reset params to new preset defaults, trigger re-trace
|
||||
- When slider changes: update params, trigger re-trace via the debounced hook
|
||||
- When file changes: trigger immediate trace (no debounce for initial upload)
|
||||
- Pass svgOutput/isLoading/error to SvgPreview
|
||||
5. Write `app/src/hooks/__tests__/useDebouncedTrace.test.ts` — test the hook with mocked fetch:
|
||||
- Verify debounce behavior (multiple rapid calls → only last fires)
|
||||
- Verify abort: mock AbortController, confirm signal is passed and previous request aborted
|
||||
- Verify SVG mode detection routes to simplifyVector
|
||||
- Estimate: 1h30m
|
||||
- Files: app/src/hooks/useDebouncedTrace.ts, app/src/components/ParameterSliders.tsx, app/src/components/SvgPreview.tsx, app/src/views/ImportConvert.tsx, app/src/hooks/__tests__/useDebouncedTrace.test.ts
|
||||
- Verify: cd app && npx vitest run --reporter=verbose 2>&1 | tail -20 && npx tsc --noEmit 2>&1 | tail -10
|
||||
- [ ] **T04: Add output info bar, Use This flow, error states, and verify full integration** — Complete the slice by adding the output info bar with color-coded stats, the 'Use This' button that advances to View 2, error/empty states, and a full-stack integration verification.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create `app/src/components/OutputInfoBar.tsx`:
|
||||
- Accepts `metadata: TraceMetadata | null`
|
||||
- When metadata is null: render nothing or a muted placeholder
|
||||
- Render stats: Path Count, Total Nodes, Open Paths, Processing Time
|
||||
- Color coding: green for normal values, yellow when node_count_total > 5000, red when open_paths > 0
|
||||
- Show warnings from metadata.warnings array if any
|
||||
2. Add 'Use This' button to `ImportConvert.tsx`:
|
||||
- Disabled when no SVG output is available
|
||||
- On click: call `onUseThis(svgOutput, metadata)` prop, which App.tsx uses to transition view state to 'canvas'
|
||||
- Pass the SVG string forward (stored in App.tsx state for S02 to consume)
|
||||
3. Add empty/error states throughout:
|
||||
- No file selected: show upload prompt in preview area
|
||||
- API error: show error message in preview area with status code
|
||||
- Loading: show spinner overlay on preview
|
||||
4. Write `app/src/components/__tests__/OutputInfoBar.test.tsx` — test color coding logic:
|
||||
- Normal metadata → green indicators
|
||||
- High node count → yellow indicator
|
||||
- Open paths > 0 → red indicator
|
||||
- Null metadata → no crash, placeholder shown
|
||||
5. Final verification: ensure `npx tsc --noEmit` passes with zero errors, `npx vitest run` passes all tests, and all components are wired together end-to-end.
|
||||
- Estimate: 45m
|
||||
- Files: app/src/components/OutputInfoBar.tsx, app/src/components/__tests__/OutputInfoBar.test.tsx, app/src/views/ImportConvert.tsx, app/src/App.tsx
|
||||
- Verify: cd app && npx vitest run --reporter=verbose 2>&1 | tail -20 && npx tsc --noEmit 2>&1 | tail -10
|
||||
|
|
|
|||
109
.gsd/milestones/M002/slices/S01/S01-RESEARCH.md
Normal file
109
.gsd/milestones/M002/slices/S01/S01-RESEARCH.md
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
# S01 — Import & Convert UI (View 1) — Research
|
||||
|
||||
**Date:** 2026-03-26
|
||||
|
||||
## Summary
|
||||
|
||||
This slice builds the first user-facing screen of the Kerf App: an Import & Convert view where users upload a raster image (PNG, JPG, etc.), select a vectorization preset, tune parameters via sliders, see a live SVG preview, and click "Use This" to advance to the Design Canvas. The engine API is fully built and validated (M001, 196 tests) — this slice is a pure frontend build that consumes it.
|
||||
|
||||
The work is greenfield — no `app/` directory, no `package.json`, no frontend tooling exists. The slice must scaffold a React + Vite + TypeScript project, establish the app shell with view routing, build the file upload flow, integrate with the Engine API's `multipart/form-data` endpoints, implement debounced re-tracing on parameter changes, and render SVG previews. A secondary prerequisite is adding CORS middleware to the engine so browser requests succeed.
|
||||
|
||||
The approach is straightforward: Vite + React + TypeScript scaffold, Zustand or simple React state for view-level state, raw `fetch` + `FormData` for engine API calls (no need for a heavy HTTP client), and inline SVG rendering via `dangerouslySetInnerHTML` for the preview. The primary risks are the debounced preview update loop (must cancel in-flight requests on param change) and correctly mapping UI slider values to the engine's nested parameter structure.
|
||||
|
||||
## Recommendation
|
||||
|
||||
Scaffold `app/` as a Vite + React + TypeScript project. Keep dependencies minimal — no UI framework needed for V1 (plain CSS or CSS modules). Use a simple view router (React Router or even a state-based conditional render since there are only 3 views). Build the engine API client as a thin typed wrapper around `fetch`. For the live preview loop, use an `AbortController`-based pattern: each parameter change triggers a debounced (300ms) re-trace call that aborts any in-flight request first. Add CORS to the engine's `main.py` as a prerequisite task.
|
||||
|
||||
## Implementation Landscape
|
||||
|
||||
### Key Files
|
||||
|
||||
**Engine (existing — read-only context for this slice):**
|
||||
- `engine/main.py` — FastAPI app; needs `CORSMiddleware` added (only change to engine code)
|
||||
- `engine/api/routes.py` — Defines `POST /engine/trace` (multipart: file, mode, output_format, preset, params), `POST /engine/simplify`, `GET /engine/presets`
|
||||
- `engine/presets/*.json` — 5 preset configs (sign, patch, stencil, detailed, custom); each has `preprocessing`, `vectorization`, and `postprocessing` sections
|
||||
|
||||
**App (to be created):**
|
||||
- `app/package.json` — Vite + React + TypeScript deps
|
||||
- `app/vite.config.ts` — Dev server config with proxy to engine API (avoids CORS in dev, but CORS still needed for production)
|
||||
- `app/src/main.tsx` — React root mount
|
||||
- `app/src/App.tsx` — Top-level view router (View 1 → View 2 → View 3)
|
||||
- `app/src/views/ImportConvert.tsx` — Main View 1 component
|
||||
- `app/src/components/FileUpload.tsx` — Drag-and-drop file upload zone
|
||||
- `app/src/components/PresetSelector.tsx` — Visual preset cards (icon + label + description)
|
||||
- `app/src/components/ParameterSliders.tsx` — Slider controls mapped to engine params
|
||||
- `app/src/components/SvgPreview.tsx` — Live SVG render panel
|
||||
- `app/src/components/OutputInfoBar.tsx` — Stats bar (path count, node count, warnings) with color coding
|
||||
- `app/src/api/engine.ts` — Typed API client: `traceImage()`, `getPresets()`, `simplifyVector()`
|
||||
- `app/src/types/engine.ts` — TypeScript types for API request/response shapes, preset configs
|
||||
|
||||
### Build Order
|
||||
|
||||
1. **CORS on engine** — add `CORSMiddleware` to `engine/main.py`. Without this, no browser-based API call works. Quick, unblocks everything. Verify: `curl -H "Origin: http://localhost:5173" -I http://localhost:8000/engine/health` returns CORS headers.
|
||||
|
||||
2. **Vite + React scaffold** — `npm create vite@latest app -- --template react-ts`, install deps, configure dev proxy. Verify: `npm run dev` shows a React page at `localhost:5173`.
|
||||
|
||||
3. **API client + types** — Build `engine.ts` with typed `traceImage()` and `getPresets()` functions. Uses `FormData` for trace (the engine expects `multipart/form-data`), returns parsed JSON with typed `TraceResponse` interface. Verify: import and call `getPresets()` from a useEffect, log to console.
|
||||
|
||||
4. **View shell + routing** — App.tsx with view state (import → canvas → export). Only View 1 active for this slice; View 2/3 are placeholder divs. The "Use This" button sets view state + passes vector data forward. Verify: view switching works on button click.
|
||||
|
||||
5. **File upload component** — Drag-and-drop zone + file input fallback. Accepts PNG, JPG, BMP, TIFF, WebP for raster; SVG for simplification mode. Shows filename + thumbnail after selection. Verify: selecting a file shows preview.
|
||||
|
||||
6. **Preset selector + parameter sliders** — Fetch presets from `GET /engine/presets` on mount. Render preset cards. When preset selected, populate slider values from preset defaults. Sliders: Detail Level (epsilon), Threshold, Noise Filter, Smooth Curves, plus an Advanced panel that exposes raw params. Verify: selecting different presets changes slider positions.
|
||||
|
||||
7. **Live preview with debounced re-trace** — Core interaction loop. On any parameter change: debounce 300ms → build FormData with file + current params → `POST /engine/trace` → render returned SVG. Use `AbortController` to cancel in-flight request if params change again. Verify: change a slider, see SVG preview update after debounce.
|
||||
|
||||
8. **Output info bar** — Render metadata from trace response: path count, total node count, open path count, warnings. Color code: green (good), yellow (high node count), red (open paths). Verify: info bar updates with each trace response.
|
||||
|
||||
### Verification Approach
|
||||
|
||||
- `cd app && npm run dev` starts Vite dev server at `localhost:5173`
|
||||
- Engine must be running at `localhost:8000` (either via docker or `cd engine && .venv/bin/uvicorn main:app --port 8000`)
|
||||
- Upload a PNG image → preset selector appears → select "sign" → sliders populate → SVG preview renders
|
||||
- Change epsilon slider → preview updates within ~500ms (300ms debounce + API latency)
|
||||
- Switch to "detailed" preset → sliders change → preview updates with higher detail
|
||||
- Info bar shows path count, node count, open paths — values change per preset
|
||||
- Click "Use This" → view state transitions (placeholder View 2 renders)
|
||||
- Upload an SVG file → routes to simplification mode (only epsilon slider, no preprocessing/vectorization controls)
|
||||
|
||||
## Constraints
|
||||
|
||||
- **Engine API uses `multipart/form-data`** — cannot POST JSON. Must construct `FormData` with `file` field (Blob/File) and string fields for `mode`, `output_format`, `preset`, `params` (JSON-encoded string).
|
||||
- **`params` field is a JSON string** — nested parameter overrides are sent as a serialized JSON string in the form data, not as separate form fields. The engine's `resolve_params()` merges these over preset defaults.
|
||||
- **SVG preview is a raw string** — the engine returns SVG as a string in `response.output`. Rendering it requires either `dangerouslySetInnerHTML` or creating an `<img src="data:image/svg+xml,...">` tag. The former preserves interactivity/scalability.
|
||||
- **No `python` on system PATH** — engine must run via `.venv/bin/uvicorn` or Docker. Dev workflow needs both processes (engine + vite) running simultaneously.
|
||||
- **Engine has no CORS** — `engine/main.py` needs `CORSMiddleware` from `fastapi.middleware.cors` added before any browser-based testing works.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
- **Stale previews from race conditions** — If the user changes params rapidly, multiple `/engine/trace` requests fire. The response from an older request may arrive after a newer one, showing stale output. Fix: use `AbortController` — abort previous request before sending a new one, and/or track a request generation counter and discard responses from older generations.
|
||||
- **SVG viewBox scaling** — Engine-generated SVGs have their own `width`, `height`, and `viewBox`. The preview panel must scale the SVG to fit its container while preserving aspect ratio. Stripping `width`/`height` attributes and relying on `viewBox` + CSS `width: 100%; height: auto` is the standard approach.
|
||||
- **File reference lost after first trace** — The `File` object from the upload input must be retained in state across re-traces. Each debounced trace call rebuilds `FormData` with the same file. Don't re-read the file from the input element — store it in React state.
|
||||
- **Advanced panel param names must match engine expectations** — The `params` JSON keys must exactly match what `resolve_params()` looks for (e.g., `turdsize`, `alphamax`, `epsilon`, `filter_speckle`). Typos silently ignored.
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Existing Solution | Why Use It |
|
||||
|---------|------------------|------------|
|
||||
| Project scaffold | `npm create vite@latest -- --template react-ts` | Standard React + TS + Vite in one command |
|
||||
| Debounce | `setTimeout`/`clearTimeout` pattern or a tiny `useDebouncedCallback` hook | Simple enough to hand-roll as a 10-line hook; no need for lodash |
|
||||
| Request cancellation | `AbortController` (native browser API) | Built-in, no library needed |
|
||||
| SVG rendering | `dangerouslySetInnerHTML` on a container div | The SVG is trusted (comes from our engine), so XSS concern is minimal |
|
||||
|
||||
## Open Risks
|
||||
|
||||
- **Engine latency on large images** — If tracing takes >2s for a large image, the live preview UX degrades. May need a loading spinner overlay on the preview during re-trace. Not a blocker but affects polish.
|
||||
- **SVG simplification mode routing** — When user uploads an SVG instead of a raster, the UI should switch to a reduced control set (only epsilon slider, no preprocessing/vectorization). This is a separate code path that needs clean conditional rendering.
|
||||
|
||||
## Skills Discovered
|
||||
|
||||
| Technology | Skill | Status |
|
||||
|------------|-------|--------|
|
||||
| React | `react-best-practices` | installed (available in skill list) |
|
||||
| Vite | `antfu/skills@vite` (11.5K installs) | available — not installed |
|
||||
|
||||
## Sources
|
||||
|
||||
- Engine API contract: `engine/api/routes.py` (read directly)
|
||||
- Preset structure: `engine/presets/*.json` (read directly)
|
||||
- Project brief: `GSD-INITIATE.md` View 1 specification (read directly)
|
||||
49
.gsd/milestones/M002/slices/S01/tasks/T01-PLAN.md
Normal file
49
.gsd/milestones/M002/slices/S01/tasks/T01-PLAN.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
estimated_steps: 17
|
||||
estimated_files: 8
|
||||
skills_used: []
|
||||
---
|
||||
|
||||
# T01: Add engine CORS, scaffold Vite+React+TS app, build typed API client
|
||||
|
||||
This task unblocks all frontend work by: (1) adding CORSMiddleware to engine/main.py so browser requests succeed, (2) scaffolding the app/ directory as a Vite + React + TypeScript project with dev proxy to the engine, and (3) building a typed API client with TypeScript interfaces matching the engine's request/response shapes.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add `CORSMiddleware` to `engine/main.py` — import from `fastapi.middleware.cors`, allow all origins for dev (restrict in production later). Place it before the router include.
|
||||
2. Scaffold `app/` using `npm create vite@latest app -- --template react-ts`. Run `cd app && npm install`.
|
||||
3. Configure `app/vite.config.ts` with a dev proxy: requests to `/engine/*` forward to `http://localhost:8000`.
|
||||
4. Create `app/src/types/engine.ts` with TypeScript interfaces:
|
||||
- `PresetConfig` — matches the JSON structure in `engine/presets/*.json` (name, description, preprocessing, vectorization, postprocessing sections)
|
||||
- `TraceResponse` — `{ output: string, format: string, metadata: TraceMetadata }`
|
||||
- `TraceMetadata` — `{ format: string, path_count: number, node_count_total: number, open_paths: number, island_count: number, warnings: string[], processing_ms: number }`
|
||||
- `PresetsResponse` — `{ presets: Record<string, PresetConfig> }`
|
||||
5. Create `app/src/api/engine.ts` with typed functions:
|
||||
- `getPresets(): Promise<PresetsResponse>` — calls `GET /engine/presets`
|
||||
- `traceImage(file: File, preset: string, params: Record<string, unknown>, signal?: AbortSignal): Promise<TraceResponse>` — builds `FormData` with file, mode, output_format='svg', preset, and params (JSON.stringify), calls `POST /engine/trace`
|
||||
- `simplifyVector(file: File, epsilon: number, signal?: AbortSignal): Promise<TraceResponse>` — calls `POST /engine/simplify`
|
||||
6. Remove Vite boilerplate: delete `app/src/App.css`, the counter component content from `App.tsx`, and the Vite/React logos.
|
||||
7. Add Vitest as dev dependency: `cd app && npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom`. Configure in `vite.config.ts` with `test: { environment: 'jsdom', globals: true }`.
|
||||
8. Write `app/src/api/__tests__/engine.test.ts` — unit tests for the API client using `vi.fn()` to mock fetch. Test that `traceImage()` builds correct FormData, `getPresets()` calls the right URL, and AbortSignal is passed through.
|
||||
|
||||
## Inputs
|
||||
|
||||
- `engine/main.py`
|
||||
- `engine/api/routes.py`
|
||||
- `engine/presets/sign.json`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- `engine/main.py`
|
||||
- `app/package.json`
|
||||
- `app/vite.config.ts`
|
||||
- `app/tsconfig.json`
|
||||
- `app/src/types/engine.ts`
|
||||
- `app/src/api/engine.ts`
|
||||
- `app/src/api/__tests__/engine.test.ts`
|
||||
- `app/src/App.tsx`
|
||||
- `app/src/main.tsx`
|
||||
|
||||
## Verification
|
||||
|
||||
cd app && npx vitest run --reporter=verbose 2>&1 | tail -20
|
||||
91
.gsd/milestones/M002/slices/S01/tasks/T01-SUMMARY.md
Normal file
91
.gsd/milestones/M002/slices/S01/tasks/T01-SUMMARY.md
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
---
|
||||
id: T01
|
||||
parent: S01
|
||||
milestone: M002
|
||||
provides: []
|
||||
requires: []
|
||||
affects: []
|
||||
key_files: ["engine/main.py", "app/vite.config.ts", "app/src/types/engine.ts", "app/src/api/engine.ts", "app/src/api/__tests__/engine.test.ts", "app/src/App.tsx", "app/src/test-setup.ts", "app/tsconfig.app.json"]
|
||||
key_decisions: ["Allow all CORS origins for dev with comment to restrict in production", "API client functions throw on non-ok responses with status + detail text", "Use vitest globals + jsdom environment for test ergonomics"]
|
||||
patterns_established: []
|
||||
drill_down_paths: []
|
||||
observability_surfaces: []
|
||||
duration: ""
|
||||
verification_result: "Ran `cd app && npx vitest run --reporter=verbose` — all 9 tests pass (3 per API function: getPresets, traceImage, simplifyVector). Ran `cd app && npx tsc --noEmit` — zero TypeScript errors."
|
||||
completed_at: 2026-03-26T05:05:22.615Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T01: Added CORSMiddleware to engine, scaffolded Vite+React+TS app with dev proxy, and built typed API client with 9 passing tests
|
||||
|
||||
> Added CORSMiddleware to engine, scaffolded Vite+React+TS app with dev proxy, and built typed API client with 9 passing tests
|
||||
|
||||
## What Happened
|
||||
---
|
||||
id: T01
|
||||
parent: S01
|
||||
milestone: M002
|
||||
key_files:
|
||||
- engine/main.py
|
||||
- app/vite.config.ts
|
||||
- app/src/types/engine.ts
|
||||
- app/src/api/engine.ts
|
||||
- app/src/api/__tests__/engine.test.ts
|
||||
- app/src/App.tsx
|
||||
- app/src/test-setup.ts
|
||||
- app/tsconfig.app.json
|
||||
key_decisions:
|
||||
- Allow all CORS origins for dev with comment to restrict in production
|
||||
- API client functions throw on non-ok responses with status + detail text
|
||||
- Use vitest globals + jsdom environment for test ergonomics
|
||||
duration: ""
|
||||
verification_result: passed
|
||||
completed_at: 2026-03-26T05:05:22.627Z
|
||||
blocker_discovered: false
|
||||
---
|
||||
|
||||
# T01: Added CORSMiddleware to engine, scaffolded Vite+React+TS app with dev proxy, and built typed API client with 9 passing tests
|
||||
|
||||
**Added CORSMiddleware to engine, scaffolded Vite+React+TS app with dev proxy, and built typed API client with 9 passing tests**
|
||||
|
||||
## What Happened
|
||||
|
||||
Added CORSMiddleware to engine/main.py allowing all origins for dev. Scaffolded app/ as a Vite+React+TS project with dev proxy forwarding /engine to localhost:8000. Created TypeScript interfaces matching all engine preset JSON structures and API response shapes. Built typed API client with getPresets(), traceImage(), and simplifyVector() functions that support AbortSignal cancellation and throw descriptive errors on failure. Wrote 9 unit tests covering URL/method correctness, FormData construction, AbortSignal passthrough, and error handling. All tests pass, zero TypeScript errors.
|
||||
|
||||
## Verification
|
||||
|
||||
Ran `cd app && npx vitest run --reporter=verbose` — all 9 tests pass (3 per API function: getPresets, traceImage, simplifyVector). Ran `cd app && npx tsc --noEmit` — zero TypeScript errors.
|
||||
|
||||
## Verification Evidence
|
||||
|
||||
| # | Command | Exit Code | Verdict | Duration |
|
||||
|---|---------|-----------|---------|----------|
|
||||
| 1 | `cd app && npx vitest run --reporter=verbose` | 0 | ✅ pass | 895ms |
|
||||
| 2 | `cd app && npx tsc --noEmit` | 0 | ✅ pass | 2000ms |
|
||||
|
||||
|
||||
## Deviations
|
||||
|
||||
Added @types/node as dev dependency (not in plan) for Node.js type resolution. Added vitest/globals to tsconfig.app.json types array for global test functions. Added test-setup.ts for jest-dom matchers.
|
||||
|
||||
## Known Issues
|
||||
|
||||
None.
|
||||
|
||||
## Files Created/Modified
|
||||
|
||||
- `engine/main.py`
|
||||
- `app/vite.config.ts`
|
||||
- `app/src/types/engine.ts`
|
||||
- `app/src/api/engine.ts`
|
||||
- `app/src/api/__tests__/engine.test.ts`
|
||||
- `app/src/App.tsx`
|
||||
- `app/src/test-setup.ts`
|
||||
- `app/tsconfig.app.json`
|
||||
|
||||
|
||||
## Deviations
|
||||
Added @types/node as dev dependency (not in plan) for Node.js type resolution. Added vitest/globals to tsconfig.app.json types array for global test functions. Added test-setup.ts for jest-dom matchers.
|
||||
|
||||
## Known Issues
|
||||
None.
|
||||
49
.gsd/milestones/M002/slices/S01/tasks/T02-PLAN.md
Normal file
49
.gsd/milestones/M002/slices/S01/tasks/T02-PLAN.md
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
---
|
||||
estimated_steps: 19
|
||||
estimated_files: 6
|
||||
skills_used: []
|
||||
---
|
||||
|
||||
# T02: Build app shell with view routing, file upload zone, and preset selector
|
||||
|
||||
Build the first user-facing components: the app shell with view-state routing (Import → Canvas → Export), a drag-and-drop file upload zone, and a preset selector that fetches presets from the engine and populates parameter defaults.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create `app/src/views/ImportConvert.tsx` — the main View 1 container. Layout: left panel (upload + controls) and right panel (preview area, placeholder for now). Use CSS modules or a single `app/src/views/ImportConvert.module.css`.
|
||||
2. Update `app/src/App.tsx` with view-state routing using React useState:
|
||||
- `type ViewState = 'import' | 'canvas' | 'export'`
|
||||
- Render `ImportConvert` when state is 'import', placeholder `<div>View 2: Design Canvas</div>` for 'canvas', placeholder for 'export'
|
||||
- Pass a callback `onUseThis(svgOutput: string, metadata: TraceMetadata)` that transitions to 'canvas'
|
||||
3. Create `app/src/components/FileUpload.tsx`:
|
||||
- Drag-and-drop zone with `onDragOver`/`onDrop` handlers + hidden file `<input>` fallback
|
||||
- Accept: `.png,.jpg,.jpeg,.bmp,.tiff,.webp,.svg` via accept attribute
|
||||
- On file selection: store `File` in parent state, show filename + file size, render thumbnail via `URL.createObjectURL()`
|
||||
- Detect SVG uploads (check file type or extension) and surface this to parent for mode switching
|
||||
4. Create `app/src/components/PresetSelector.tsx`:
|
||||
- On mount: call `getPresets()` from `app/src/api/engine.ts`
|
||||
- Render preset cards — each shows name and description from the preset config
|
||||
- Clicking a card selects it (visual highlight) and calls `onPresetSelect(presetName, presetConfig)` callback
|
||||
- Default selection: 'sign' preset
|
||||
5. Create `app/src/App.css` (or global styles) with minimal layout: flexbox two-column layout for View 1, card styles for presets, drop zone styling with dashed border + hover state.
|
||||
6. Wire components together in `ImportConvert.tsx`: FileUpload at top, PresetSelector below it, both in the left panel. Store selected file and preset in local state.
|
||||
|
||||
## Inputs
|
||||
|
||||
- `app/src/App.tsx`
|
||||
- `app/src/main.tsx`
|
||||
- `app/src/types/engine.ts`
|
||||
- `app/src/api/engine.ts`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- `app/src/views/ImportConvert.tsx`
|
||||
- `app/src/views/ImportConvert.module.css`
|
||||
- `app/src/components/FileUpload.tsx`
|
||||
- `app/src/components/PresetSelector.tsx`
|
||||
- `app/src/App.tsx`
|
||||
- `app/src/App.css`
|
||||
|
||||
## Verification
|
||||
|
||||
cd app && npx vitest run --reporter=verbose 2>&1 | tail -20 && npx tsc --noEmit 2>&1 | tail -10
|
||||
65
.gsd/milestones/M002/slices/S01/tasks/T03-PLAN.md
Normal file
65
.gsd/milestones/M002/slices/S01/tasks/T03-PLAN.md
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
---
|
||||
estimated_steps: 36
|
||||
estimated_files: 5
|
||||
skills_used: []
|
||||
---
|
||||
|
||||
# T03: Implement parameter sliders and debounced live SVG preview with AbortController
|
||||
|
||||
Build the core interaction loop: parameter sliders derived from preset defaults, debounced re-trace on any change, AbortController-based request cancellation, and inline SVG rendering. This is the highest-risk piece of the slice.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create `app/src/hooks/useDebouncedTrace.ts` — a custom hook that encapsulates the debounce + abort logic:
|
||||
- Accepts: `file: File | null`, `preset: string`, `params: Record<string, unknown>`, `debounceMs: number` (default 300)
|
||||
- Maintains an `AbortController` ref. On each trigger: clear previous timeout, abort previous request, set new timeout
|
||||
- After debounce: call `traceImage(file, preset, params, signal)` from the API client
|
||||
- Returns: `{ svgOutput: string | null, metadata: TraceMetadata | null, isLoading: boolean, error: string | null }`
|
||||
- On unmount: abort any in-flight request and clear timeout
|
||||
- For SVG file uploads: call `simplifyVector()` instead of `traceImage()`
|
||||
2. Create `app/src/components/ParameterSliders.tsx`:
|
||||
- Accepts current preset config and an `onChange(params)` callback
|
||||
- Renders labeled range inputs for key parameters:
|
||||
- **Detail Level** (epsilon): range 0.5–10, step 0.5, from postprocessing.epsilon
|
||||
- **Noise Filter** (filter_speckle/turdsize): range 0–50, step 1, from vectorization params
|
||||
- **Smooth Curves** (alphamax): range 0–1.334, step 0.1, from vectorization.potrace.alphamax
|
||||
- **Corner Threshold** (corner_threshold): range 0–180, step 1, from vectorization.vtracer.corner_threshold (only shown for vtracer mode)
|
||||
- Show/hide sliders based on vectorization mode (potrace vs vtracer) from preset
|
||||
- Each slider change calls `onChange` with the full current params object
|
||||
- Show current numeric value next to each slider
|
||||
3. Create `app/src/components/SvgPreview.tsx`:
|
||||
- Accepts `svgOutput: string | null`, `isLoading: boolean`, `error: string | null`
|
||||
- When loading: show a spinner/overlay
|
||||
- When error: show error message with retry suggestion
|
||||
- When SVG available: render via `dangerouslySetInnerHTML` inside a container div
|
||||
- Strip `width` and `height` attributes from the SVG string (keep viewBox) for responsive scaling
|
||||
- Apply CSS: `width: 100%; height: auto; max-height: 70vh; object-fit: contain` on the container
|
||||
4. Wire everything in `ImportConvert.tsx`:
|
||||
- Track `currentParams` state, initialized from selected preset defaults
|
||||
- When preset changes: reset params to new preset defaults, trigger re-trace
|
||||
- When slider changes: update params, trigger re-trace via the debounced hook
|
||||
- When file changes: trigger immediate trace (no debounce for initial upload)
|
||||
- Pass svgOutput/isLoading/error to SvgPreview
|
||||
5. Write `app/src/hooks/__tests__/useDebouncedTrace.test.ts` — test the hook with mocked fetch:
|
||||
- Verify debounce behavior (multiple rapid calls → only last fires)
|
||||
- Verify abort: mock AbortController, confirm signal is passed and previous request aborted
|
||||
- Verify SVG mode detection routes to simplifyVector
|
||||
|
||||
## Inputs
|
||||
|
||||
- `app/src/api/engine.ts`
|
||||
- `app/src/types/engine.ts`
|
||||
- `app/src/views/ImportConvert.tsx`
|
||||
- `app/src/components/PresetSelector.tsx`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- `app/src/hooks/useDebouncedTrace.ts`
|
||||
- `app/src/components/ParameterSliders.tsx`
|
||||
- `app/src/components/SvgPreview.tsx`
|
||||
- `app/src/views/ImportConvert.tsx`
|
||||
- `app/src/hooks/__tests__/useDebouncedTrace.test.ts`
|
||||
|
||||
## Verification
|
||||
|
||||
cd app && npx vitest run --reporter=verbose 2>&1 | tail -20 && npx tsc --noEmit 2>&1 | tail -10
|
||||
51
.gsd/milestones/M002/slices/S01/tasks/T04-PLAN.md
Normal file
51
.gsd/milestones/M002/slices/S01/tasks/T04-PLAN.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
estimated_steps: 22
|
||||
estimated_files: 4
|
||||
skills_used: []
|
||||
---
|
||||
|
||||
# T04: Add output info bar, Use This flow, error states, and verify full integration
|
||||
|
||||
Complete the slice by adding the output info bar with color-coded stats, the 'Use This' button that advances to View 2, error/empty states, and a full-stack integration verification.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Create `app/src/components/OutputInfoBar.tsx`:
|
||||
- Accepts `metadata: TraceMetadata | null`
|
||||
- When metadata is null: render nothing or a muted placeholder
|
||||
- Render stats: Path Count, Total Nodes, Open Paths, Processing Time
|
||||
- Color coding: green for normal values, yellow when node_count_total > 5000, red when open_paths > 0
|
||||
- Show warnings from metadata.warnings array if any
|
||||
2. Add 'Use This' button to `ImportConvert.tsx`:
|
||||
- Disabled when no SVG output is available
|
||||
- On click: call `onUseThis(svgOutput, metadata)` prop, which App.tsx uses to transition view state to 'canvas'
|
||||
- Pass the SVG string forward (stored in App.tsx state for S02 to consume)
|
||||
3. Add empty/error states throughout:
|
||||
- No file selected: show upload prompt in preview area
|
||||
- API error: show error message in preview area with status code
|
||||
- Loading: show spinner overlay on preview
|
||||
4. Write `app/src/components/__tests__/OutputInfoBar.test.tsx` — test color coding logic:
|
||||
- Normal metadata → green indicators
|
||||
- High node count → yellow indicator
|
||||
- Open paths > 0 → red indicator
|
||||
- Null metadata → no crash, placeholder shown
|
||||
5. Final verification: ensure `npx tsc --noEmit` passes with zero errors, `npx vitest run` passes all tests, and all components are wired together end-to-end.
|
||||
|
||||
## Inputs
|
||||
|
||||
- `app/src/views/ImportConvert.tsx`
|
||||
- `app/src/App.tsx`
|
||||
- `app/src/types/engine.ts`
|
||||
- `app/src/hooks/useDebouncedTrace.ts`
|
||||
- `app/src/components/SvgPreview.tsx`
|
||||
|
||||
## Expected Output
|
||||
|
||||
- `app/src/components/OutputInfoBar.tsx`
|
||||
- `app/src/components/__tests__/OutputInfoBar.test.tsx`
|
||||
- `app/src/views/ImportConvert.tsx`
|
||||
- `app/src/App.tsx`
|
||||
|
||||
## Verification
|
||||
|
||||
cd app && npx vitest run --reporter=verbose 2>&1 | tail -20 && npx tsc --noEmit 2>&1 | tail -10
|
||||
1282
.gsd/reports/M001-2026-03-26T04-57-16.html
Normal file
1282
.gsd/reports/M001-2026-03-26T04-57-16.html
Normal file
File diff suppressed because it is too large
Load diff
209
.gsd/reports/index.html
Normal file
209
.gsd/reports/index.html
Normal file
|
|
@ -0,0 +1,209 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>GSD Reports — kerf-engine</title>
|
||||
<style>
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
:root{
|
||||
--bg-0:#0f1115;--bg-1:#16181d;--bg-2:#1e2028;--bg-3:#272a33;
|
||||
--border-1:#2b2e38;--border-2:#3b3f4c;
|
||||
--text-0:#ededef;--text-1:#a1a1aa;--text-2:#71717a;
|
||||
--accent:#5e6ad2;--accent-subtle:rgba(94,106,210,.12);
|
||||
--font:'Inter',-apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
|
||||
--mono:'JetBrains Mono','Fira Code',ui-monospace,monospace;
|
||||
}
|
||||
html{font-size:13px}
|
||||
body{background:var(--bg-0);color:var(--text-0);font-family:var(--font);line-height:1.6;-webkit-font-smoothing:antialiased}
|
||||
a{color:var(--accent);text-decoration:none}
|
||||
a:hover{text-decoration:underline}
|
||||
h2{font-size:14px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text-1);margin-bottom:16px;padding-bottom:8px;border-bottom:1px solid var(--border-1)}
|
||||
h3{font-size:13px;font-weight:600;color:var(--text-1);margin:16px 0 8px}
|
||||
code{font-family:var(--mono);font-size:12px;background:var(--bg-3);padding:1px 5px;border-radius:3px}
|
||||
.empty{color:var(--text-2);font-size:13px;padding:8px 0}
|
||||
.count{font-size:11px;font-weight:500;color:var(--text-2);background:var(--bg-3);border-radius:3px;padding:1px 6px}
|
||||
|
||||
/* Header */
|
||||
header{background:var(--bg-1);border-bottom:1px solid var(--border-1);padding:12px 32px;position:sticky;top:0;z-index:100}
|
||||
.hdr-inner{display:flex;align-items:center;gap:16px;max-width:1280px;margin:0 auto}
|
||||
.branding{display:flex;align-items:baseline;gap:6px;flex-shrink:0}
|
||||
.logo{font-size:18px;font-weight:800;letter-spacing:-.5px;color:var(--text-0)}
|
||||
.ver{font-size:10px;color:var(--text-2);font-family:var(--mono)}
|
||||
.hdr-meta{flex:1;min-width:0}
|
||||
.hdr-meta h1{font-size:15px;font-weight:600}
|
||||
.hdr-subtitle{color:var(--text-2);font-weight:400;font-size:13px;margin-left:4px}
|
||||
.hdr-path{font-size:11px;color:var(--text-2);font-family:var(--mono);display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.hdr-right{text-align:right;flex-shrink:0}
|
||||
.gen-lbl{font-size:10px;color:var(--text-2);text-transform:uppercase;letter-spacing:.5px;display:block}
|
||||
.gen{font-size:11px;color:var(--text-1)}
|
||||
|
||||
/* Layout */
|
||||
.layout{display:grid;grid-template-columns:200px 1fr;gap:0;max-width:1280px;margin:0 auto;min-height:calc(100vh - 120px)}
|
||||
|
||||
/* Sidebar */
|
||||
.sidebar{background:var(--bg-1);border-right:1px solid var(--border-1);padding:20px 14px;position:sticky;top:52px;height:calc(100vh - 52px);overflow-y:auto}
|
||||
.sidebar-title{font-size:10px;font-weight:600;color:var(--text-2);text-transform:uppercase;letter-spacing:.5px;margin-bottom:12px}
|
||||
.toc-group{margin-bottom:14px}
|
||||
.toc-group-label{font-size:11px;font-weight:600;color:var(--text-1);margin-bottom:3px;font-family:var(--mono)}
|
||||
.toc-group ul{list-style:none;display:flex;flex-direction:column;gap:1px}
|
||||
.toc-group li{display:flex;align-items:center;gap:6px}
|
||||
.toc-group a{font-size:11px;color:var(--text-2);padding:2px 4px;border-radius:3px;flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.toc-group a:hover{background:var(--bg-2);color:var(--text-0);text-decoration:none}
|
||||
.toc-kind{font-size:9px;color:var(--text-2);font-family:var(--mono);flex-shrink:0}
|
||||
|
||||
/* Main */
|
||||
main{padding:28px;display:flex;flex-direction:column;gap:40px}
|
||||
|
||||
/* Overview */
|
||||
.idx-summary{display:flex;flex-wrap:wrap;gap:1px;background:var(--border-1);border:1px solid var(--border-1);border-radius:4px;overflow:hidden;margin-bottom:16px}
|
||||
.idx-stat{background:var(--bg-1);padding:10px 16px;display:flex;flex-direction:column;gap:2px;min-width:100px;flex:1}
|
||||
.idx-val{font-size:18px;font-weight:600;color:var(--text-0);font-variant-numeric:tabular-nums}
|
||||
.idx-lbl{font-size:10px;color:var(--text-2);text-transform:uppercase;letter-spacing:.4px}
|
||||
.idx-progress{display:flex;align-items:center;gap:10px;margin-top:10px}
|
||||
.idx-bar-track{flex:1;height:4px;background:var(--bg-3);border-radius:2px;overflow:hidden}
|
||||
.idx-bar-fill{height:100%;background:var(--accent);border-radius:2px}
|
||||
.idx-pct{font-size:12px;font-weight:600;color:var(--text-1);min-width:40px;text-align:right}
|
||||
|
||||
/* Sparkline */
|
||||
.sparkline-wrap{margin-top:20px}
|
||||
.sparkline{position:relative}
|
||||
.spark-svg{display:block;background:var(--bg-1);border:1px solid var(--border-1);border-radius:4px;overflow:visible;max-width:100%}
|
||||
.spark-line{stroke:var(--accent);stroke-width:1.5;fill:none}
|
||||
.spark-dot{fill:var(--accent);stroke:var(--bg-1);stroke-width:2;cursor:pointer}
|
||||
.spark-dot:hover{r:4;fill:var(--text-0)}
|
||||
.spark-lbl{font-size:10px;fill:var(--text-2);font-family:var(--mono)}
|
||||
.spark-axis{display:flex;position:relative;height:18px;margin-top:2px}
|
||||
.spark-tick{position:absolute;transform:translateX(-50%);font-size:9px;color:var(--text-2);font-family:var(--mono);white-space:nowrap}
|
||||
|
||||
/* Report cards */
|
||||
.cards-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:10px}
|
||||
.report-card{
|
||||
display:flex;flex-direction:column;gap:6px;
|
||||
background:var(--bg-1);border:1px solid var(--border-1);border-radius:4px;
|
||||
padding:14px;text-decoration:none;color:var(--text-0);
|
||||
transition:border-color .12s;
|
||||
}
|
||||
.report-card:hover{border-color:var(--accent);text-decoration:none}
|
||||
.card-latest{border-color:var(--accent)}
|
||||
.card-top{display:flex;align-items:center;gap:8px}
|
||||
.card-label{flex:1;font-weight:500;font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.card-kind{font-size:10px;color:var(--text-2);font-family:var(--mono);flex-shrink:0}
|
||||
.card-date{font-size:11px;color:var(--text-2)}
|
||||
.card-progress{display:flex;align-items:center;gap:6px}
|
||||
.card-bar-track{flex:1;height:3px;background:var(--bg-3);border-radius:2px;overflow:hidden}
|
||||
.card-bar-fill{height:100%;background:var(--accent);border-radius:2px}
|
||||
.card-pct{font-size:11px;color:var(--text-2);min-width:30px;text-align:right}
|
||||
.card-stats{display:flex;gap:8px;flex-wrap:wrap}
|
||||
.card-stats span{font-size:11px;color:var(--text-2);font-variant-numeric:tabular-nums}
|
||||
.card-delta{display:flex;gap:4px;flex-wrap:wrap}
|
||||
.card-delta span{font-size:10px;color:var(--text-1);font-family:var(--mono)}
|
||||
.card-latest-badge{display:none}
|
||||
|
||||
/* Footer */
|
||||
footer{border-top:1px solid var(--border-1);padding:16px 32px}
|
||||
.ftr-inner{display:flex;align-items:center;gap:6px;justify-content:center;font-size:11px;color:var(--text-2)}
|
||||
.ftr-sep{color:var(--border-2)}
|
||||
|
||||
@media(max-width:768px){
|
||||
.layout{grid-template-columns:1fr}
|
||||
.sidebar{position:static;height:auto;border-right:none;border-bottom:1px solid var(--border-1)}
|
||||
}
|
||||
@media print{
|
||||
.sidebar{display:none}
|
||||
header{position:static}
|
||||
body{background:#fff;color:#1a1a1a}
|
||||
:root{--bg-0:#fff;--bg-1:#fafafa;--bg-2:#f5f5f5;--bg-3:#ebebeb;--border-1:#e5e5e5;--border-2:#d4d4d4;--text-0:#1a1a1a;--text-1:#525252;--text-2:#a3a3a3;--accent:#4f46e5}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<div class="hdr-inner">
|
||||
<div class="branding">
|
||||
<span class="logo">GSD</span>
|
||||
<span class="ver">v2.49.0</span>
|
||||
</div>
|
||||
<div class="hdr-meta">
|
||||
<h1>kerf-engine <span class="hdr-subtitle">Reports</span></h1>
|
||||
<span class="hdr-path">/home/aux/development/xpltdco/kerf-engine</span>
|
||||
</div>
|
||||
<div class="hdr-right">
|
||||
<span class="gen-lbl">Updated</span>
|
||||
<span class="gen">Mar 26, 2026, 04:57 AM</span>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="layout">
|
||||
<!-- Sidebar TOC -->
|
||||
<aside class="sidebar">
|
||||
<div class="sidebar-title">Reports</div>
|
||||
|
||||
<div class="toc-group">
|
||||
<div class="toc-group-label">M001</div>
|
||||
<ul><li><a href="M001-2026-03-26T04-57-16.html">Mar 26, 2026, 04:57 AM</a> <span class="toc-kind toc-milestone">milestone</span></li></ul>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<!-- Main content -->
|
||||
<main>
|
||||
<section class="idx-overview">
|
||||
<h2>Project Overview</h2>
|
||||
|
||||
<div class="idx-summary">
|
||||
<div class="idx-stat"><span class="idx-val">$15.14</span><span class="idx-lbl">Total Cost</span></div>
|
||||
<div class="idx-stat"><span class="idx-val">17.34M</span><span class="idx-lbl">Total Tokens</span></div>
|
||||
<div class="idx-stat"><span class="idx-val">51m 18s</span><span class="idx-lbl">Duration</span></div>
|
||||
<div class="idx-stat"><span class="idx-val">3/9</span><span class="idx-lbl">Slices</span></div>
|
||||
<div class="idx-stat"><span class="idx-val">1/3</span><span class="idx-lbl">Milestones</span></div>
|
||||
<div class="idx-stat"><span class="idx-val">1</span><span class="idx-lbl">Reports</span></div>
|
||||
</div>
|
||||
<div class="idx-progress">
|
||||
<div class="idx-bar-track"><div class="idx-bar-fill" style="width:33%"></div></div>
|
||||
<span class="idx-pct">33% complete</span>
|
||||
</div>
|
||||
|
||||
</section>
|
||||
|
||||
<section class="idx-cards">
|
||||
<h2>Progression <span class="sec-count">1</span></h2>
|
||||
<div class="cards-grid">
|
||||
<a class="report-card card-latest" href="M001-2026-03-26T04-57-16.html">
|
||||
<div class="card-top">
|
||||
<span class="card-label">M001: Kerf Engine — Raster-to-Vector Pipeline & API</span>
|
||||
<span class="card-kind card-kind-milestone">milestone</span>
|
||||
</div>
|
||||
<div class="card-date">Mar 26, 2026, 04:57 AM</div>
|
||||
<div class="card-progress">
|
||||
<div class="card-bar-track">
|
||||
<div class="card-bar-fill" style="width:33%"></div>
|
||||
</div>
|
||||
<span class="card-pct">33%</span>
|
||||
</div>
|
||||
<div class="card-stats">
|
||||
<span>$15.14</span>
|
||||
<span>17.34M</span>
|
||||
<span>51m 18s</span>
|
||||
<span>3/9 slices</span>
|
||||
</div>
|
||||
|
||||
<div class="card-latest-badge">Latest</div>
|
||||
</a></div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
<div class="ftr-inner">
|
||||
<span class="ftr-brand">GSD v2.49.0</span>
|
||||
<span class="ftr-sep">—</span>
|
||||
<span>kerf-engine</span>
|
||||
<span class="ftr-sep">—</span>
|
||||
<span>/home/aux/development/xpltdco/kerf-engine</span>
|
||||
<span class="ftr-sep">—</span>
|
||||
<span>Updated Mar 26, 2026, 04:57 AM</span>
|
||||
</div>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
24
.gsd/reports/reports.json
Normal file
24
.gsd/reports/reports.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"version": 1,
|
||||
"projectName": "kerf-engine",
|
||||
"projectPath": "/home/aux/development/xpltdco/kerf-engine",
|
||||
"gsdVersion": "2.49.0",
|
||||
"entries": [
|
||||
{
|
||||
"filename": "M001-2026-03-26T04-57-16.html",
|
||||
"generatedAt": "2026-03-26T04:57:16.166Z",
|
||||
"milestoneId": "M001",
|
||||
"milestoneTitle": "Kerf Engine — Raster-to-Vector Pipeline & API",
|
||||
"label": "M001: Kerf Engine — Raster-to-Vector Pipeline & API",
|
||||
"kind": "milestone",
|
||||
"totalCost": 15.13526925,
|
||||
"totalTokens": 17340040,
|
||||
"totalDuration": 3078596,
|
||||
"doneSlices": 3,
|
||||
"totalSlices": 9,
|
||||
"doneMilestones": 1,
|
||||
"totalMilestones": 3,
|
||||
"phase": "planning"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"version": 1,
|
||||
"exported_at": "2026-03-26T04:56:43.003Z",
|
||||
"exported_at": "2026-03-26T05:05:22.657Z",
|
||||
"milestones": [
|
||||
{
|
||||
"id": "M001",
|
||||
|
|
@ -280,11 +280,11 @@
|
|||
"completed_at": null,
|
||||
"full_summary_md": "",
|
||||
"full_uat_md": "",
|
||||
"goal": "Build View 1 (Import & Convert) with Engine API integration, preset selector, tuning sliders with live preview, and output info bar",
|
||||
"success_criteria": "- File upload accepts all supported formats\n- Preset cards populate sliders with defaults\n- Sliders update preview within 300ms debounce\n- Output info bar shows path count, node count, warnings\n- 'Use This' carries vector data to View 2\n- 'Re-import' resets the flow",
|
||||
"proof_level": "integration — React frontend calling live Engine API",
|
||||
"integration_closure": "View 1 complete; vector data carried to canvas on 'Use This' action",
|
||||
"observability_impact": "Preview update latency visible to user; info bar shows live stats",
|
||||
"goal": "Build the Import & Convert view (View 1) as a React SPA that integrates with the Engine API — users upload a raster image, select a vectorization preset, tune parameters via sliders, see a live SVG preview updated via debounced re-trace, and click \"Use This\" to advance to the Design Canvas.",
|
||||
"success_criteria": "- Vite + React + TypeScript app scaffolded in `app/` with dev proxy to engine API\n- Engine CORS middleware added so browser requests succeed\n- Typed API client calls `GET /engine/presets` and `POST /engine/trace` successfully\n- File upload accepts PNG/JPG/BMP/TIFF/WebP (raster) and SVG (simplification mode)\n- Preset selector fetches and displays all 5 presets, populates slider defaults on selection\n- Parameter sliders trigger debounced (300ms) re-trace with AbortController cancellation\n- SVG preview renders engine output inline, scaled to fit container with preserved aspect ratio\n- Output info bar shows path count, node count, open paths with color coding\n- \"Use This\" button transitions to placeholder View 2\n- `cd app && npx vitest run` passes integration tests for API client and component rendering",
|
||||
"proof_level": "Integration — real Engine API calls from browser, debounced preview loop verified at component + manual level",
|
||||
"integration_closure": "- Upstream surfaces consumed: `engine/api/routes.py` (GET /engine/presets, POST /engine/trace, POST /engine/simplify), `engine/presets/*.json` (preset structure)\n- New wiring: `app/vite.config.ts` dev proxy forwards `/engine/*` to `localhost:8000`; `engine/main.py` gains CORSMiddleware for production\n- What remains: View 2 (Design Canvas) and View 3 (Export) are placeholder divs — not usable until S02/S03",
|
||||
"observability_impact": "- Runtime signals: console.error on API failures, loading state in preview panel\n- Inspection surfaces: Browser DevTools Network tab shows /engine/trace calls with timing; preview panel shows \"Loading...\" or error messages\n- Failure visibility: API errors render in-place with status code and message; AbortError (cancelled requests) are silently swallowed\n- Redaction constraints: none — no secrets or PII in this flow",
|
||||
"sequence": 0,
|
||||
"replan_triggered_at": null
|
||||
},
|
||||
|
|
@ -811,6 +811,202 @@
|
|||
"observability_impact": "",
|
||||
"full_plan_md": "",
|
||||
"sequence": 0
|
||||
},
|
||||
{
|
||||
"milestone_id": "M002",
|
||||
"slice_id": "S01",
|
||||
"id": "T01",
|
||||
"title": "Added CORSMiddleware to engine, scaffolded Vite+React+TS app with dev proxy, and built typed API client with 9 passing tests",
|
||||
"status": "complete",
|
||||
"one_liner": "Added CORSMiddleware to engine, scaffolded Vite+React+TS app with dev proxy, and built typed API client with 9 passing tests",
|
||||
"narrative": "Added CORSMiddleware to engine/main.py allowing all origins for dev. Scaffolded app/ as a Vite+React+TS project with dev proxy forwarding /engine to localhost:8000. Created TypeScript interfaces matching all engine preset JSON structures and API response shapes. Built typed API client with getPresets(), traceImage(), and simplifyVector() functions that support AbortSignal cancellation and throw descriptive errors on failure. Wrote 9 unit tests covering URL/method correctness, FormData construction, AbortSignal passthrough, and error handling. All tests pass, zero TypeScript errors.",
|
||||
"verification_result": "Ran `cd app && npx vitest run --reporter=verbose` — all 9 tests pass (3 per API function: getPresets, traceImage, simplifyVector). Ran `cd app && npx tsc --noEmit` — zero TypeScript errors.",
|
||||
"duration": "",
|
||||
"completed_at": "2026-03-26T05:05:22.615Z",
|
||||
"blocker_discovered": false,
|
||||
"deviations": "Added @types/node as dev dependency (not in plan) for Node.js type resolution. Added vitest/globals to tsconfig.app.json types array for global test functions. Added test-setup.ts for jest-dom matchers.",
|
||||
"known_issues": "None.",
|
||||
"key_files": [
|
||||
"engine/main.py",
|
||||
"app/vite.config.ts",
|
||||
"app/src/types/engine.ts",
|
||||
"app/src/api/engine.ts",
|
||||
"app/src/api/__tests__/engine.test.ts",
|
||||
"app/src/App.tsx",
|
||||
"app/src/test-setup.ts",
|
||||
"app/tsconfig.app.json"
|
||||
],
|
||||
"key_decisions": [
|
||||
"Allow all CORS origins for dev with comment to restrict in production",
|
||||
"API client functions throw on non-ok responses with status + detail text",
|
||||
"Use vitest globals + jsdom environment for test ergonomics"
|
||||
],
|
||||
"full_summary_md": "---\nid: T01\nparent: S01\nmilestone: M002\nkey_files:\n - engine/main.py\n - app/vite.config.ts\n - app/src/types/engine.ts\n - app/src/api/engine.ts\n - app/src/api/__tests__/engine.test.ts\n - app/src/App.tsx\n - app/src/test-setup.ts\n - app/tsconfig.app.json\nkey_decisions:\n - Allow all CORS origins for dev with comment to restrict in production\n - API client functions throw on non-ok responses with status + detail text\n - Use vitest globals + jsdom environment for test ergonomics\nduration: \"\"\nverification_result: passed\ncompleted_at: 2026-03-26T05:05:22.627Z\nblocker_discovered: false\n---\n\n# T01: Added CORSMiddleware to engine, scaffolded Vite+React+TS app with dev proxy, and built typed API client with 9 passing tests\n\n**Added CORSMiddleware to engine, scaffolded Vite+React+TS app with dev proxy, and built typed API client with 9 passing tests**\n\n## What Happened\n\nAdded CORSMiddleware to engine/main.py allowing all origins for dev. Scaffolded app/ as a Vite+React+TS project with dev proxy forwarding /engine to localhost:8000. Created TypeScript interfaces matching all engine preset JSON structures and API response shapes. Built typed API client with getPresets(), traceImage(), and simplifyVector() functions that support AbortSignal cancellation and throw descriptive errors on failure. Wrote 9 unit tests covering URL/method correctness, FormData construction, AbortSignal passthrough, and error handling. All tests pass, zero TypeScript errors.\n\n## Verification\n\nRan `cd app && npx vitest run --reporter=verbose` — all 9 tests pass (3 per API function: getPresets, traceImage, simplifyVector). Ran `cd app && npx tsc --noEmit` — zero TypeScript errors.\n\n## Verification Evidence\n\n| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n| 1 | `cd app && npx vitest run --reporter=verbose` | 0 | ✅ pass | 895ms |\n| 2 | `cd app && npx tsc --noEmit` | 0 | ✅ pass | 2000ms |\n\n\n## Deviations\n\nAdded @types/node as dev dependency (not in plan) for Node.js type resolution. Added vitest/globals to tsconfig.app.json types array for global test functions. Added test-setup.ts for jest-dom matchers.\n\n## Known Issues\n\nNone.\n\n## Files Created/Modified\n\n- `engine/main.py`\n- `app/vite.config.ts`\n- `app/src/types/engine.ts`\n- `app/src/api/engine.ts`\n- `app/src/api/__tests__/engine.test.ts`\n- `app/src/App.tsx`\n- `app/src/test-setup.ts`\n- `app/tsconfig.app.json`\n",
|
||||
"description": "This task unblocks all frontend work by: (1) adding CORSMiddleware to engine/main.py so browser requests succeed, (2) scaffolding the app/ directory as a Vite + React + TypeScript project with dev proxy to the engine, and (3) building a typed API client with TypeScript interfaces matching the engine's request/response shapes.\n\n## Steps\n\n1. Add `CORSMiddleware` to `engine/main.py` — import from `fastapi.middleware.cors`, allow all origins for dev (restrict in production later). Place it before the router include.\n2. Scaffold `app/` using `npm create vite@latest app -- --template react-ts`. Run `cd app && npm install`.\n3. Configure `app/vite.config.ts` with a dev proxy: requests to `/engine/*` forward to `http://localhost:8000`.\n4. Create `app/src/types/engine.ts` with TypeScript interfaces:\n - `PresetConfig` — matches the JSON structure in `engine/presets/*.json` (name, description, preprocessing, vectorization, postprocessing sections)\n - `TraceResponse` — `{ output: string, format: string, metadata: TraceMetadata }`\n - `TraceMetadata` — `{ format: string, path_count: number, node_count_total: number, open_paths: number, island_count: number, warnings: string[], processing_ms: number }`\n - `PresetsResponse` — `{ presets: Record<string, PresetConfig> }`\n5. Create `app/src/api/engine.ts` with typed functions:\n - `getPresets(): Promise<PresetsResponse>` — calls `GET /engine/presets`\n - `traceImage(file: File, preset: string, params: Record<string, unknown>, signal?: AbortSignal): Promise<TraceResponse>` — builds `FormData` with file, mode, output_format='svg', preset, and params (JSON.stringify), calls `POST /engine/trace`\n - `simplifyVector(file: File, epsilon: number, signal?: AbortSignal): Promise<TraceResponse>` — calls `POST /engine/simplify`\n6. Remove Vite boilerplate: delete `app/src/App.css`, the counter component content from `App.tsx`, and the Vite/React logos.\n7. Add Vitest as dev dependency: `cd app && npm install -D vitest @testing-library/react @testing-library/jest-dom jsdom`. Configure in `vite.config.ts` with `test: { environment: 'jsdom', globals: true }`.\n8. Write `app/src/api/__tests__/engine.test.ts` — unit tests for the API client using `vi.fn()` to mock fetch. Test that `traceImage()` builds correct FormData, `getPresets()` calls the right URL, and AbortSignal is passed through.",
|
||||
"estimate": "45m",
|
||||
"files": [
|
||||
"engine/main.py",
|
||||
"app/package.json",
|
||||
"app/vite.config.ts",
|
||||
"app/tsconfig.json",
|
||||
"app/src/types/engine.ts",
|
||||
"app/src/api/engine.ts",
|
||||
"app/src/api/__tests__/engine.test.ts",
|
||||
"app/src/App.tsx"
|
||||
],
|
||||
"verify": "cd app && npx vitest run --reporter=verbose 2>&1 | tail -20",
|
||||
"inputs": [
|
||||
"engine/main.py",
|
||||
"engine/api/routes.py",
|
||||
"engine/presets/sign.json"
|
||||
],
|
||||
"expected_output": [
|
||||
"engine/main.py",
|
||||
"app/package.json",
|
||||
"app/vite.config.ts",
|
||||
"app/tsconfig.json",
|
||||
"app/src/types/engine.ts",
|
||||
"app/src/api/engine.ts",
|
||||
"app/src/api/__tests__/engine.test.ts",
|
||||
"app/src/App.tsx",
|
||||
"app/src/main.tsx"
|
||||
],
|
||||
"observability_impact": "",
|
||||
"full_plan_md": "",
|
||||
"sequence": 0
|
||||
},
|
||||
{
|
||||
"milestone_id": "M002",
|
||||
"slice_id": "S01",
|
||||
"id": "T02",
|
||||
"title": "Build app shell with view routing, file upload zone, and preset selector",
|
||||
"status": "pending",
|
||||
"one_liner": "",
|
||||
"narrative": "",
|
||||
"verification_result": "",
|
||||
"duration": "",
|
||||
"completed_at": null,
|
||||
"blocker_discovered": false,
|
||||
"deviations": "",
|
||||
"known_issues": "",
|
||||
"key_files": [],
|
||||
"key_decisions": [],
|
||||
"full_summary_md": "",
|
||||
"description": "Build the first user-facing components: the app shell with view-state routing (Import → Canvas → Export), a drag-and-drop file upload zone, and a preset selector that fetches presets from the engine and populates parameter defaults.\n\n## Steps\n\n1. Create `app/src/views/ImportConvert.tsx` — the main View 1 container. Layout: left panel (upload + controls) and right panel (preview area, placeholder for now). Use CSS modules or a single `app/src/views/ImportConvert.module.css`.\n2. Update `app/src/App.tsx` with view-state routing using React useState:\n - `type ViewState = 'import' | 'canvas' | 'export'`\n - Render `ImportConvert` when state is 'import', placeholder `<div>View 2: Design Canvas</div>` for 'canvas', placeholder for 'export'\n - Pass a callback `onUseThis(svgOutput: string, metadata: TraceMetadata)` that transitions to 'canvas'\n3. Create `app/src/components/FileUpload.tsx`:\n - Drag-and-drop zone with `onDragOver`/`onDrop` handlers + hidden file `<input>` fallback\n - Accept: `.png,.jpg,.jpeg,.bmp,.tiff,.webp,.svg` via accept attribute\n - On file selection: store `File` in parent state, show filename + file size, render thumbnail via `URL.createObjectURL()`\n - Detect SVG uploads (check file type or extension) and surface this to parent for mode switching\n4. Create `app/src/components/PresetSelector.tsx`:\n - On mount: call `getPresets()` from `app/src/api/engine.ts`\n - Render preset cards — each shows name and description from the preset config\n - Clicking a card selects it (visual highlight) and calls `onPresetSelect(presetName, presetConfig)` callback\n - Default selection: 'sign' preset\n5. Create `app/src/App.css` (or global styles) with minimal layout: flexbox two-column layout for View 1, card styles for presets, drop zone styling with dashed border + hover state.\n6. Wire components together in `ImportConvert.tsx`: FileUpload at top, PresetSelector below it, both in the left panel. Store selected file and preset in local state.",
|
||||
"estimate": "1h",
|
||||
"files": [
|
||||
"app/src/App.tsx",
|
||||
"app/src/App.css",
|
||||
"app/src/views/ImportConvert.tsx",
|
||||
"app/src/views/ImportConvert.module.css",
|
||||
"app/src/components/FileUpload.tsx",
|
||||
"app/src/components/PresetSelector.tsx"
|
||||
],
|
||||
"verify": "cd app && npx vitest run --reporter=verbose 2>&1 | tail -20 && npx tsc --noEmit 2>&1 | tail -10",
|
||||
"inputs": [
|
||||
"app/src/App.tsx",
|
||||
"app/src/main.tsx",
|
||||
"app/src/types/engine.ts",
|
||||
"app/src/api/engine.ts"
|
||||
],
|
||||
"expected_output": [
|
||||
"app/src/views/ImportConvert.tsx",
|
||||
"app/src/views/ImportConvert.module.css",
|
||||
"app/src/components/FileUpload.tsx",
|
||||
"app/src/components/PresetSelector.tsx",
|
||||
"app/src/App.tsx",
|
||||
"app/src/App.css"
|
||||
],
|
||||
"observability_impact": "",
|
||||
"full_plan_md": "",
|
||||
"sequence": 0
|
||||
},
|
||||
{
|
||||
"milestone_id": "M002",
|
||||
"slice_id": "S01",
|
||||
"id": "T03",
|
||||
"title": "Implement parameter sliders and debounced live SVG preview with AbortController",
|
||||
"status": "pending",
|
||||
"one_liner": "",
|
||||
"narrative": "",
|
||||
"verification_result": "",
|
||||
"duration": "",
|
||||
"completed_at": null,
|
||||
"blocker_discovered": false,
|
||||
"deviations": "",
|
||||
"known_issues": "",
|
||||
"key_files": [],
|
||||
"key_decisions": [],
|
||||
"full_summary_md": "",
|
||||
"description": "Build the core interaction loop: parameter sliders derived from preset defaults, debounced re-trace on any change, AbortController-based request cancellation, and inline SVG rendering. This is the highest-risk piece of the slice.\n\n## Steps\n\n1. Create `app/src/hooks/useDebouncedTrace.ts` — a custom hook that encapsulates the debounce + abort logic:\n - Accepts: `file: File | null`, `preset: string`, `params: Record<string, unknown>`, `debounceMs: number` (default 300)\n - Maintains an `AbortController` ref. On each trigger: clear previous timeout, abort previous request, set new timeout\n - After debounce: call `traceImage(file, preset, params, signal)` from the API client\n - Returns: `{ svgOutput: string | null, metadata: TraceMetadata | null, isLoading: boolean, error: string | null }`\n - On unmount: abort any in-flight request and clear timeout\n - For SVG file uploads: call `simplifyVector()` instead of `traceImage()`\n2. Create `app/src/components/ParameterSliders.tsx`:\n - Accepts current preset config and an `onChange(params)` callback\n - Renders labeled range inputs for key parameters:\n - **Detail Level** (epsilon): range 0.5–10, step 0.5, from postprocessing.epsilon\n - **Noise Filter** (filter_speckle/turdsize): range 0–50, step 1, from vectorization params\n - **Smooth Curves** (alphamax): range 0–1.334, step 0.1, from vectorization.potrace.alphamax\n - **Corner Threshold** (corner_threshold): range 0–180, step 1, from vectorization.vtracer.corner_threshold (only shown for vtracer mode)\n - Show/hide sliders based on vectorization mode (potrace vs vtracer) from preset\n - Each slider change calls `onChange` with the full current params object\n - Show current numeric value next to each slider\n3. Create `app/src/components/SvgPreview.tsx`:\n - Accepts `svgOutput: string | null`, `isLoading: boolean`, `error: string | null`\n - When loading: show a spinner/overlay\n - When error: show error message with retry suggestion\n - When SVG available: render via `dangerouslySetInnerHTML` inside a container div\n - Strip `width` and `height` attributes from the SVG string (keep viewBox) for responsive scaling\n - Apply CSS: `width: 100%; height: auto; max-height: 70vh; object-fit: contain` on the container\n4. Wire everything in `ImportConvert.tsx`:\n - Track `currentParams` state, initialized from selected preset defaults\n - When preset changes: reset params to new preset defaults, trigger re-trace\n - When slider changes: update params, trigger re-trace via the debounced hook\n - When file changes: trigger immediate trace (no debounce for initial upload)\n - Pass svgOutput/isLoading/error to SvgPreview\n5. Write `app/src/hooks/__tests__/useDebouncedTrace.test.ts` — test the hook with mocked fetch:\n - Verify debounce behavior (multiple rapid calls → only last fires)\n - Verify abort: mock AbortController, confirm signal is passed and previous request aborted\n - Verify SVG mode detection routes to simplifyVector",
|
||||
"estimate": "1h30m",
|
||||
"files": [
|
||||
"app/src/hooks/useDebouncedTrace.ts",
|
||||
"app/src/components/ParameterSliders.tsx",
|
||||
"app/src/components/SvgPreview.tsx",
|
||||
"app/src/views/ImportConvert.tsx",
|
||||
"app/src/hooks/__tests__/useDebouncedTrace.test.ts"
|
||||
],
|
||||
"verify": "cd app && npx vitest run --reporter=verbose 2>&1 | tail -20 && npx tsc --noEmit 2>&1 | tail -10",
|
||||
"inputs": [
|
||||
"app/src/api/engine.ts",
|
||||
"app/src/types/engine.ts",
|
||||
"app/src/views/ImportConvert.tsx",
|
||||
"app/src/components/PresetSelector.tsx"
|
||||
],
|
||||
"expected_output": [
|
||||
"app/src/hooks/useDebouncedTrace.ts",
|
||||
"app/src/components/ParameterSliders.tsx",
|
||||
"app/src/components/SvgPreview.tsx",
|
||||
"app/src/views/ImportConvert.tsx",
|
||||
"app/src/hooks/__tests__/useDebouncedTrace.test.ts"
|
||||
],
|
||||
"observability_impact": "",
|
||||
"full_plan_md": "",
|
||||
"sequence": 0
|
||||
},
|
||||
{
|
||||
"milestone_id": "M002",
|
||||
"slice_id": "S01",
|
||||
"id": "T04",
|
||||
"title": "Add output info bar, Use This flow, error states, and verify full integration",
|
||||
"status": "pending",
|
||||
"one_liner": "",
|
||||
"narrative": "",
|
||||
"verification_result": "",
|
||||
"duration": "",
|
||||
"completed_at": null,
|
||||
"blocker_discovered": false,
|
||||
"deviations": "",
|
||||
"known_issues": "",
|
||||
"key_files": [],
|
||||
"key_decisions": [],
|
||||
"full_summary_md": "",
|
||||
"description": "Complete the slice by adding the output info bar with color-coded stats, the 'Use This' button that advances to View 2, error/empty states, and a full-stack integration verification.\n\n## Steps\n\n1. Create `app/src/components/OutputInfoBar.tsx`:\n - Accepts `metadata: TraceMetadata | null`\n - When metadata is null: render nothing or a muted placeholder\n - Render stats: Path Count, Total Nodes, Open Paths, Processing Time\n - Color coding: green for normal values, yellow when node_count_total > 5000, red when open_paths > 0\n - Show warnings from metadata.warnings array if any\n2. Add 'Use This' button to `ImportConvert.tsx`:\n - Disabled when no SVG output is available\n - On click: call `onUseThis(svgOutput, metadata)` prop, which App.tsx uses to transition view state to 'canvas'\n - Pass the SVG string forward (stored in App.tsx state for S02 to consume)\n3. Add empty/error states throughout:\n - No file selected: show upload prompt in preview area\n - API error: show error message in preview area with status code\n - Loading: show spinner overlay on preview\n4. Write `app/src/components/__tests__/OutputInfoBar.test.tsx` — test color coding logic:\n - Normal metadata → green indicators\n - High node count → yellow indicator\n - Open paths > 0 → red indicator\n - Null metadata → no crash, placeholder shown\n5. Final verification: ensure `npx tsc --noEmit` passes with zero errors, `npx vitest run` passes all tests, and all components are wired together end-to-end.",
|
||||
"estimate": "45m",
|
||||
"files": [
|
||||
"app/src/components/OutputInfoBar.tsx",
|
||||
"app/src/components/__tests__/OutputInfoBar.test.tsx",
|
||||
"app/src/views/ImportConvert.tsx",
|
||||
"app/src/App.tsx"
|
||||
],
|
||||
"verify": "cd app && npx vitest run --reporter=verbose 2>&1 | tail -20 && npx tsc --noEmit 2>&1 | tail -10",
|
||||
"inputs": [
|
||||
"app/src/views/ImportConvert.tsx",
|
||||
"app/src/App.tsx",
|
||||
"app/src/types/engine.ts",
|
||||
"app/src/hooks/useDebouncedTrace.ts",
|
||||
"app/src/components/SvgPreview.tsx"
|
||||
],
|
||||
"expected_output": [
|
||||
"app/src/components/OutputInfoBar.tsx",
|
||||
"app/src/components/__tests__/OutputInfoBar.test.tsx",
|
||||
"app/src/views/ImportConvert.tsx",
|
||||
"app/src/App.tsx"
|
||||
],
|
||||
"observability_impact": "",
|
||||
"full_plan_md": "",
|
||||
"sequence": 0
|
||||
}
|
||||
],
|
||||
"decisions": [
|
||||
|
|
@ -861,6 +1057,18 @@
|
|||
"revisable": "Yes",
|
||||
"made_by": "agent",
|
||||
"superseded_by": null
|
||||
},
|
||||
{
|
||||
"seq": 13,
|
||||
"id": "D005",
|
||||
"when_context": "",
|
||||
"scope": "architecture",
|
||||
"decision": "Frontend technology stack for Kerf App",
|
||||
"choice": "Vite + React + TypeScript with plain CSS modules, Vitest for testing, raw fetch + FormData for engine API calls, no UI framework for V1",
|
||||
"rationale": "Minimal dependency surface for a tool that needs fast iteration. The engine API uses multipart/form-data which works naturally with native fetch + FormData. CSS modules avoid global style conflicts without adding a framework. Vitest integrates natively with Vite's transform pipeline. No UI library needed — the app has a focused interface with ~8 components.",
|
||||
"revisable": "Yes",
|
||||
"made_by": "agent",
|
||||
"superseded_by": null
|
||||
}
|
||||
],
|
||||
"verification_evidence": [
|
||||
|
|
@ -1017,6 +1225,28 @@
|
|||
"verdict": "✅ pass",
|
||||
"duration_ms": 940,
|
||||
"created_at": "2026-03-26T04:49:33.510Z"
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"task_id": "T01",
|
||||
"slice_id": "S01",
|
||||
"milestone_id": "M002",
|
||||
"command": "cd app && npx vitest run --reporter=verbose",
|
||||
"exit_code": 0,
|
||||
"verdict": "✅ pass",
|
||||
"duration_ms": 895,
|
||||
"created_at": "2026-03-26T05:05:22.615Z"
|
||||
},
|
||||
{
|
||||
"id": 16,
|
||||
"task_id": "T01",
|
||||
"slice_id": "S01",
|
||||
"milestone_id": "M002",
|
||||
"command": "cd app && npx tsc --noEmit",
|
||||
"exit_code": 0,
|
||||
"verdict": "✅ pass",
|
||||
"duration_ms": 2000,
|
||||
"created_at": "2026-03-26T05:05:22.615Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
24
app/.gitignore
vendored
Normal file
24
app/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
73
app/README.md
Normal file
73
app/README.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
23
app/eslint.config.js
Normal file
23
app/eslint.config.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
13
app/index.html
Normal file
13
app/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>app</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
4104
app/package-lock.json
generated
Normal file
4104
app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
34
app/package.json
Normal file
34
app/package.json
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{
|
||||
"name": "app",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@testing-library/jest-dom": "^6.9.1",
|
||||
"@testing-library/react": "^16.3.2",
|
||||
"@types/node": "^24.12.0",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"jsdom": "^29.0.1",
|
||||
"typescript": "~5.9.3",
|
||||
"typescript-eslint": "^8.57.0",
|
||||
"vite": "^8.0.1",
|
||||
"vitest": "^4.1.1"
|
||||
}
|
||||
}
|
||||
1
app/public/favicon.svg
Normal file
1
app/public/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
24
app/public/icons.svg
Normal file
24
app/public/icons.svg
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
10
app/src/App.tsx
Normal file
10
app/src/App.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
function App() {
|
||||
return (
|
||||
<div id="app">
|
||||
<h1>Kerf Engine</h1>
|
||||
<p>Import & Convert view coming in T02.</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
149
app/src/api/__tests__/engine.test.ts
Normal file
149
app/src/api/__tests__/engine.test.ts
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { getPresets, traceImage, simplifyVector } from '../engine';
|
||||
|
||||
// ---------- helpers ----------
|
||||
|
||||
function mockFetchOk(body: unknown) {
|
||||
return vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: () => Promise.resolve(body),
|
||||
text: () => Promise.resolve(JSON.stringify(body)),
|
||||
});
|
||||
}
|
||||
|
||||
function mockFetchFail(status: number, statusText: string) {
|
||||
return vi.fn().mockResolvedValue({
|
||||
ok: false,
|
||||
status,
|
||||
statusText,
|
||||
text: () => Promise.resolve(statusText),
|
||||
});
|
||||
}
|
||||
|
||||
// ---------- setup ----------
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
|
||||
beforeEach(() => {
|
||||
// reset to a no-op fetch so tests that forget to mock still fail clearly
|
||||
globalThis.fetch = vi.fn().mockRejectedValue(new Error('fetch not mocked'));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
globalThis.fetch = originalFetch;
|
||||
});
|
||||
|
||||
// ---------- getPresets ----------
|
||||
|
||||
describe('getPresets', () => {
|
||||
it('calls GET /engine/presets and returns parsed JSON', async () => {
|
||||
const payload = { presets: { sign: { name: 'sign' } } };
|
||||
globalThis.fetch = mockFetchOk(payload);
|
||||
|
||||
const result = await getPresets();
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalledOnce();
|
||||
const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
expect(url).toBe('/engine/presets');
|
||||
expect(opts?.signal).toBeUndefined();
|
||||
expect(result).toEqual(payload);
|
||||
});
|
||||
|
||||
it('passes AbortSignal when provided', async () => {
|
||||
globalThis.fetch = mockFetchOk({ presets: {} });
|
||||
const controller = new AbortController();
|
||||
|
||||
await getPresets(controller.signal);
|
||||
|
||||
const [, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
expect(opts.signal).toBe(controller.signal);
|
||||
});
|
||||
|
||||
it('throws on non-ok response', async () => {
|
||||
globalThis.fetch = mockFetchFail(500, 'Internal Server Error');
|
||||
|
||||
await expect(getPresets()).rejects.toThrow(/failed.*500/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------- traceImage ----------
|
||||
|
||||
describe('traceImage', () => {
|
||||
it('sends FormData with file, preset, output_format, and JSON params', async () => {
|
||||
globalThis.fetch = mockFetchOk({ output: '<svg/>', format: 'svg', metadata: {} });
|
||||
|
||||
const file = new File(['pixels'], 'test.png', { type: 'image/png' });
|
||||
const params = { epsilon: 2.5, turdsize: 10 };
|
||||
|
||||
await traceImage(file, 'sign', params);
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalledOnce();
|
||||
const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
expect(url).toBe('/engine/trace');
|
||||
expect(opts.method).toBe('POST');
|
||||
|
||||
const body: FormData = opts.body;
|
||||
expect(body).toBeInstanceOf(FormData);
|
||||
expect(body.get('file')).toBeInstanceOf(File);
|
||||
expect(body.get('preset')).toBe('sign');
|
||||
expect(body.get('output_format')).toBe('svg');
|
||||
expect(body.get('params')).toBe(JSON.stringify(params));
|
||||
});
|
||||
|
||||
it('passes AbortSignal when provided', async () => {
|
||||
globalThis.fetch = mockFetchOk({ output: '', format: 'svg', metadata: {} });
|
||||
const controller = new AbortController();
|
||||
|
||||
const file = new File(['px'], 'img.png', { type: 'image/png' });
|
||||
await traceImage(file, 'sign', {}, controller.signal);
|
||||
|
||||
const [, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
expect(opts.signal).toBe(controller.signal);
|
||||
});
|
||||
|
||||
it('throws with detail on non-ok response', async () => {
|
||||
globalThis.fetch = mockFetchFail(422, 'Unprocessable Entity');
|
||||
|
||||
const file = new File(['px'], 'bad.png', { type: 'image/png' });
|
||||
await expect(traceImage(file, 'sign', {})).rejects.toThrow(/failed.*422/i);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------- simplifyVector ----------
|
||||
|
||||
describe('simplifyVector', () => {
|
||||
it('sends FormData with file, epsilon, and output_format', async () => {
|
||||
globalThis.fetch = mockFetchOk({ output: '<svg/>', format: 'svg', metadata: {} });
|
||||
|
||||
const file = new File(['<svg></svg>'], 'input.svg', { type: 'image/svg+xml' });
|
||||
await simplifyVector(file, 3.0);
|
||||
|
||||
const [url, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
expect(url).toBe('/engine/simplify');
|
||||
expect(opts.method).toBe('POST');
|
||||
|
||||
const body: FormData = opts.body;
|
||||
expect(body.get('file')).toBeInstanceOf(File);
|
||||
expect(body.get('epsilon')).toBe('3');
|
||||
expect(body.get('output_format')).toBe('svg');
|
||||
});
|
||||
|
||||
it('passes AbortSignal when provided', async () => {
|
||||
globalThis.fetch = mockFetchOk({ output: '', format: 'svg', metadata: {} });
|
||||
const controller = new AbortController();
|
||||
|
||||
const file = new File(['<svg></svg>'], 'input.svg', { type: 'image/svg+xml' });
|
||||
await simplifyVector(file, 1.0, controller.signal);
|
||||
|
||||
const [, opts] = (globalThis.fetch as ReturnType<typeof vi.fn>).mock.calls[0];
|
||||
expect(opts.signal).toBe(controller.signal);
|
||||
});
|
||||
|
||||
it('throws on non-ok response', async () => {
|
||||
globalThis.fetch = mockFetchFail(400, 'Bad Request');
|
||||
|
||||
const file = new File(['<svg></svg>'], 'input.svg', { type: 'image/svg+xml' });
|
||||
await expect(simplifyVector(file, 1.0)).rejects.toThrow(/failed.*400/i);
|
||||
});
|
||||
});
|
||||
69
app/src/api/engine.ts
Normal file
69
app/src/api/engine.ts
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
/** Typed API client for the Kerf Engine. */
|
||||
|
||||
import type { PresetsResponse, TraceResponse } from '../types/engine';
|
||||
|
||||
const BASE = '/engine';
|
||||
|
||||
/**
|
||||
* Fetch all available presets from the engine.
|
||||
*/
|
||||
export async function getPresets(signal?: AbortSignal): Promise<PresetsResponse> {
|
||||
const res = await fetch(`${BASE}/presets`, { signal });
|
||||
if (!res.ok) {
|
||||
throw new Error(`GET /engine/presets failed: ${res.status} ${res.statusText}`);
|
||||
}
|
||||
return res.json() as Promise<PresetsResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trace a raster image through the vectorization pipeline.
|
||||
* Builds FormData with the file, mode, preset, and JSON-encoded params.
|
||||
*/
|
||||
export async function traceImage(
|
||||
file: File,
|
||||
preset: string,
|
||||
params: Record<string, unknown>,
|
||||
signal?: AbortSignal,
|
||||
): Promise<TraceResponse> {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
form.append('output_format', 'svg');
|
||||
form.append('preset', preset);
|
||||
form.append('params', JSON.stringify(params));
|
||||
|
||||
const res = await fetch(`${BASE}/trace`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
signal,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const detail = await res.text().catch(() => res.statusText);
|
||||
throw new Error(`POST /engine/trace failed: ${res.status} — ${detail}`);
|
||||
}
|
||||
return res.json() as Promise<TraceResponse>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Simplify an existing SVG using RDP path simplification.
|
||||
*/
|
||||
export async function simplifyVector(
|
||||
file: File,
|
||||
epsilon: number,
|
||||
signal?: AbortSignal,
|
||||
): Promise<TraceResponse> {
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
form.append('epsilon', String(epsilon));
|
||||
form.append('output_format', 'svg');
|
||||
|
||||
const res = await fetch(`${BASE}/simplify`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
signal,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const detail = await res.text().catch(() => res.statusText);
|
||||
throw new Error(`POST /engine/simplify failed: ${res.status} — ${detail}`);
|
||||
}
|
||||
return res.json() as Promise<TraceResponse>;
|
||||
}
|
||||
BIN
app/src/assets/hero.png
Normal file
BIN
app/src/assets/hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 44 KiB |
1
app/src/assets/react.svg
Normal file
1
app/src/assets/react.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4 KiB |
1
app/src/assets/vite.svg
Normal file
1
app/src/assets/vite.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
111
app/src/index.css
Normal file
111
app/src/index.css
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
:root {
|
||||
--text: #6b6375;
|
||||
--text-h: #08060d;
|
||||
--bg: #fff;
|
||||
--border: #e5e4e7;
|
||||
--code-bg: #f4f3ec;
|
||||
--accent: #aa3bff;
|
||||
--accent-bg: rgba(170, 59, 255, 0.1);
|
||||
--accent-border: rgba(170, 59, 255, 0.5);
|
||||
--social-bg: rgba(244, 243, 236, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.1) 0 10px 15px -3px, rgba(0, 0, 0, 0.05) 0 4px 6px -2px;
|
||||
|
||||
--sans: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--heading: system-ui, 'Segoe UI', Roboto, sans-serif;
|
||||
--mono: ui-monospace, Consolas, monospace;
|
||||
|
||||
font: 18px/145% var(--sans);
|
||||
letter-spacing: 0.18px;
|
||||
color-scheme: light dark;
|
||||
color: var(--text);
|
||||
background: var(--bg);
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--text: #9ca3af;
|
||||
--text-h: #f3f4f6;
|
||||
--bg: #16171d;
|
||||
--border: #2e303a;
|
||||
--code-bg: #1f2028;
|
||||
--accent: #c084fc;
|
||||
--accent-bg: rgba(192, 132, 252, 0.15);
|
||||
--accent-border: rgba(192, 132, 252, 0.5);
|
||||
--social-bg: rgba(47, 48, 58, 0.5);
|
||||
--shadow:
|
||||
rgba(0, 0, 0, 0.4) 0 10px 15px -3px, rgba(0, 0, 0, 0.25) 0 4px 6px -2px;
|
||||
}
|
||||
|
||||
#social .button-icon {
|
||||
filter: invert(1) brightness(2);
|
||||
}
|
||||
}
|
||||
|
||||
#root {
|
||||
width: 1126px;
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
text-align: center;
|
||||
border-inline: 1px solid var(--border);
|
||||
min-height: 100svh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
font-family: var(--heading);
|
||||
font-weight: 500;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 56px;
|
||||
letter-spacing: -1.68px;
|
||||
margin: 32px 0;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 36px;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
h2 {
|
||||
font-size: 24px;
|
||||
line-height: 118%;
|
||||
letter-spacing: -0.24px;
|
||||
margin: 0 0 8px;
|
||||
@media (max-width: 1024px) {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
code,
|
||||
.counter {
|
||||
font-family: var(--mono);
|
||||
display: inline-flex;
|
||||
border-radius: 4px;
|
||||
color: var(--text-h);
|
||||
}
|
||||
|
||||
code {
|
||||
font-size: 15px;
|
||||
line-height: 135%;
|
||||
padding: 4px 8px;
|
||||
background: var(--code-bg);
|
||||
}
|
||||
10
app/src/main.tsx
Normal file
10
app/src/main.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
1
app/src/test-setup.ts
Normal file
1
app/src/test-setup.ts
Normal file
|
|
@ -0,0 +1 @@
|
|||
import '@testing-library/jest-dom/vitest';
|
||||
79
app/src/types/engine.ts
Normal file
79
app/src/types/engine.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
/** TypeScript interfaces matching the Kerf Engine API shapes. */
|
||||
|
||||
// -- Preset configuration (mirrors engine/presets/*.json) --
|
||||
|
||||
export interface PreprocessingConfig {
|
||||
denoise_d?: number;
|
||||
denoise_sigma_color?: number;
|
||||
denoise_sigma_space?: number;
|
||||
clahe_clip_limit?: number;
|
||||
clahe_tile_grid_size?: [number, number];
|
||||
threshold_manual?: number | null;
|
||||
edge_detect?: boolean;
|
||||
morph_kernel_size?: number;
|
||||
morph_dilate_iterations?: number;
|
||||
morph_erode_iterations?: number;
|
||||
}
|
||||
|
||||
export interface PotraceConfig {
|
||||
turdsize?: number;
|
||||
alphamax?: number;
|
||||
opticurve?: boolean;
|
||||
opttolerance?: number;
|
||||
}
|
||||
|
||||
export interface VtracerConfig {
|
||||
colormode?: string;
|
||||
hierarchical?: string;
|
||||
filter_speckle?: number;
|
||||
corner_threshold?: number;
|
||||
length_threshold?: number;
|
||||
splice_threshold?: number;
|
||||
mode?: string;
|
||||
color_precision?: number;
|
||||
layer_difference?: number;
|
||||
path_precision?: number;
|
||||
max_iterations?: number;
|
||||
}
|
||||
|
||||
export interface VectorizationConfig {
|
||||
mode: 'potrace' | 'vtracer';
|
||||
potrace?: PotraceConfig;
|
||||
vtracer?: VtracerConfig;
|
||||
}
|
||||
|
||||
export interface PostprocessingConfig {
|
||||
epsilon?: number;
|
||||
close_tolerance?: number;
|
||||
auto_close?: boolean;
|
||||
}
|
||||
|
||||
export interface PresetConfig {
|
||||
name: string;
|
||||
description: string;
|
||||
preprocessing: PreprocessingConfig;
|
||||
vectorization: VectorizationConfig;
|
||||
postprocessing: PostprocessingConfig;
|
||||
}
|
||||
|
||||
// -- API responses --
|
||||
|
||||
export interface TraceMetadata {
|
||||
format: string;
|
||||
path_count: number;
|
||||
node_count_total: number;
|
||||
open_paths: number;
|
||||
island_count: number;
|
||||
warnings: string[];
|
||||
processing_ms: number;
|
||||
}
|
||||
|
||||
export interface TraceResponse {
|
||||
output: string;
|
||||
format: string;
|
||||
metadata: TraceMetadata;
|
||||
}
|
||||
|
||||
export interface PresetsResponse {
|
||||
presets: Record<string, PresetConfig>;
|
||||
}
|
||||
28
app/tsconfig.app.json
Normal file
28
app/tsconfig.app.json
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"types": ["vite/client", "vitest/globals"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
app/tsconfig.json
Normal file
7
app/tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
26
app/tsconfig.node.json
Normal file
26
app/tsconfig.node.json
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "ES2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "ESNext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
21
app/vite.config.ts
Normal file
21
app/vite.config.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/// <reference types="vitest" />
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/engine': {
|
||||
target: 'http://localhost:8000',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
globals: true,
|
||||
setupFiles: ['./src/test-setup.ts'],
|
||||
},
|
||||
})
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
"""Kerf Engine — raster-to-vector conversion API."""
|
||||
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from api.routes import router
|
||||
|
||||
|
|
@ -10,6 +11,15 @@ app = FastAPI(
|
|||
version="0.1.0",
|
||||
)
|
||||
|
||||
# CORS — allow all origins for dev; restrict in production via env var
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=["*"],
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
app.include_router(router)
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue