From 2bcc12454229baa23c8a62ad91f4b7c3f3bb061e Mon Sep 17 00:00:00 2001 From: jlightner Date: Thu, 26 Mar 2026 06:19:30 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Lifted=20useCanvasState()=20from=20Desi?= =?UTF-8?q?gnCanvas=20to=20App.tsx,=20threaded=20al=E2=80=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - "app/src/App.tsx" - "app/src/views/DesignCanvas.tsx" GSD-Task: S01/T02 --- .gsd/event-log.jsonl | 1 + .gsd/milestones/M003/slices/S01/S01-PLAN.md | 2 +- .../M003/slices/S01/tasks/T01-VERIFY.json | 24 ++++++ .../M003/slices/S01/tasks/T02-SUMMARY.md | 80 +++++++++++++++++++ .gsd/state-manifest.json | 64 ++++++++++++--- app/src/App.tsx | 42 +++++++++- app/src/views/DesignCanvas.tsx | 53 +++++++----- 7 files changed, 228 insertions(+), 38 deletions(-) create mode 100644 .gsd/milestones/M003/slices/S01/tasks/T01-VERIFY.json create mode 100644 .gsd/milestones/M003/slices/S01/tasks/T02-SUMMARY.md diff --git a/.gsd/event-log.jsonl b/.gsd/event-log.jsonl index b6f46fc..882be30 100644 --- a/.gsd/event-log.jsonl +++ b/.gsd/event-log.jsonl @@ -32,3 +32,4 @@ {"cmd":"complete-milestone","params":{"milestoneId":"M002"},"ts":"2026-03-26T06:06:46.769Z","actor":"agent","hash":"56704af548d63e18","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} {"cmd":"plan-slice","params":{"milestoneId":"M003","sliceId":"S01"},"ts":"2026-03-26T06:13:19.433Z","actor":"agent","hash":"0c1c14d2a8ee7643","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} {"cmd":"complete-task","params":{"milestoneId":"M003","sliceId":"S01","taskId":"T01"},"ts":"2026-03-26T06:16:59.236Z","actor":"agent","hash":"f6bd52e1fbbe7e7f","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} +{"cmd":"complete-task","params":{"milestoneId":"M003","sliceId":"S01","taskId":"T02"},"ts":"2026-03-26T06:19:28.695Z","actor":"agent","hash":"20e62f4b5af835c3","session_id":"49f8e0fe-34a0-4608-b519-eca93850ed7c"} diff --git a/.gsd/milestones/M003/slices/S01/S01-PLAN.md b/.gsd/milestones/M003/slices/S01/S01-PLAN.md index cdeb1e9..ab60728 100644 --- a/.gsd/milestones/M003/slices/S01/S01-PLAN.md +++ b/.gsd/milestones/M003/slices/S01/S01-PLAN.md @@ -8,7 +8,7 @@ - Estimate: 1h - Files: engine/output/dxf.py, engine/api/routes.py, engine/tests/test_output.py - Verify: cd engine && .venv/bin/python -m pytest tests/test_output.py -v -W ignore::DeprecationWarning -- [ ] **T02: Lift canvas state to App.tsx and add Export navigation** — Currently `useCanvasState()` is instantiated inside `DesignCanvas.tsx`, making canvas state inaccessible to View 3. This task lifts the hook instantiation to `App.tsx`, passes state + actions as props to `DesignCanvas`, and adds an `onExport` callback prop that triggers navigation to the 'export' view. The Export view placeholder in App.tsx is updated to receive canvas state props. DesignCanvas gets an 'Export' button in its toolbar area. All existing DesignCanvas functionality must remain unchanged — this is a mechanical prop-threading refactor. +- [x] **T02: Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition** — Currently `useCanvasState()` is instantiated inside `DesignCanvas.tsx`, making canvas state inaccessible to View 3. This task lifts the hook instantiation to `App.tsx`, passes state + actions as props to `DesignCanvas`, and adds an `onExport` callback prop that triggers navigation to the 'export' view. The Export view placeholder in App.tsx is updated to receive canvas state props. DesignCanvas gets an 'Export' button in its toolbar area. All existing DesignCanvas functionality must remain unchanged — this is a mechanical prop-threading refactor. Key constraints: - `useCanvasState` hook itself is NOT modified — only where it's called changes diff --git a/.gsd/milestones/M003/slices/S01/tasks/T01-VERIFY.json b/.gsd/milestones/M003/slices/S01/tasks/T01-VERIFY.json new file mode 100644 index 0000000..4f8973a --- /dev/null +++ b/.gsd/milestones/M003/slices/S01/tasks/T01-VERIFY.json @@ -0,0 +1,24 @@ +{ + "schemaVersion": 1, + "taskId": "T01", + "unitId": "M003/S01/T01", + "timestamp": 1774505826214, + "passed": false, + "discoverySource": "task-plan", + "checks": [ + { + "command": "cd engine", + "exitCode": 0, + "durationMs": 11, + "verdict": "pass" + }, + { + "command": ".venv/bin/python -m pytest tests/test_output.py -v -W ignore::DeprecationWarning", + "exitCode": 127, + "durationMs": 5, + "verdict": "fail" + } + ], + "retryAttempt": 1, + "maxRetries": 2 +} diff --git a/.gsd/milestones/M003/slices/S01/tasks/T02-SUMMARY.md b/.gsd/milestones/M003/slices/S01/tasks/T02-SUMMARY.md new file mode 100644 index 0000000..c552e33 --- /dev/null +++ b/.gsd/milestones/M003/slices/S01/tasks/T02-SUMMARY.md @@ -0,0 +1,80 @@ +--- +id: T02 +parent: S01 +milestone: M003 +provides: [] +requires: [] +affects: [] +key_files: ["app/src/App.tsx", "app/src/views/DesignCanvas.tsx"] +key_decisions: ["UseCanvasStateReturn interface used via extends for DesignCanvasProps — avoids duplicating 14 prop types", "PNG capture happens in handleExport before view transition so Konva stage is still mounted", "pngDataUrl state added to App.tsx — will be passed to ExportView in T04"] +patterns_established: [] +drill_down_paths: [] +observability_surfaces: [] +duration: "" +verification_result: "Ran `cd app && npx tsc -b --noEmit` — 0 new type errors in App.tsx or DesignCanvas.tsx. Ran `cd app && npx vitest run` — 95/95 tests pass across 7 test files. Ran engine tests: 36/36 pass." +completed_at: 2026-03-26T06:19:28.652Z +blocker_discovered: false +--- + +# T02: Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition + +> Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition + +## What Happened +--- +id: T02 +parent: S01 +milestone: M003 +key_files: + - app/src/App.tsx + - app/src/views/DesignCanvas.tsx +key_decisions: + - UseCanvasStateReturn interface used via extends for DesignCanvasProps — avoids duplicating 14 prop types + - PNG capture happens in handleExport before view transition so Konva stage is still mounted + - pngDataUrl state added to App.tsx — will be passed to ExportView in T04 +duration: "" +verification_result: passed +completed_at: 2026-03-26T06:19:28.660Z +blocker_discovered: false +--- + +# T02: Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition + +**Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition** + +## What Happened + +Moved `useCanvasState(traceMetadata)` call from DesignCanvas.tsx to App.tsx. DesignCanvas now receives all 14 canvas state/action properties via props (using `extends UseCanvasStateReturn` on its props interface), plus `stageRef` and `onExport`. The local `stageRef` in DesignCanvas was removed; it now uses the one passed from App.tsx. App.tsx creates the ref, instantiates the hook, and spreads all return values into DesignCanvas. An Export button was added to DesignCanvas's toolbar area. The `handleExport` callback in App.tsx captures a 2x PNG data URL from the Konva stage before navigating to the export view, storing it in `pngDataUrl` state for later use by ExportView (T04). A placeholder export view shows object count and PNG capture status, with a Back to Design button. + +## Verification + +Ran `cd app && npx tsc -b --noEmit` — 0 new type errors in App.tsx or DesignCanvas.tsx. Ran `cd app && npx vitest run` — 95/95 tests pass across 7 test files. Ran engine tests: 36/36 pass. + +## Verification Evidence + +| # | Command | Exit Code | Verdict | Duration | +|---|---------|-----------|---------|----------| +| 1 | `cd app && npx tsc -b --noEmit` | 0 | ✅ pass | 5000ms | +| 2 | `cd app && npx vitest run` | 0 | ✅ pass (95/95) | 2260ms | +| 3 | `cd engine && .venv/bin/python -m pytest tests/test_output.py -v -W ignore::DeprecationWarning` | 0 | ✅ pass (36/36) | 390ms | + + +## Deviations + +None. + +## Known Issues + +None. + +## Files Created/Modified + +- `app/src/App.tsx` +- `app/src/views/DesignCanvas.tsx` + + +## Deviations +None. + +## Known Issues +None. diff --git a/.gsd/state-manifest.json b/.gsd/state-manifest.json index 0f61ff8..2c2f120 100644 --- a/.gsd/state-manifest.json +++ b/.gsd/state-manifest.json @@ -1,6 +1,6 @@ { "version": 1, - "exported_at": "2026-03-26T06:16:59.234Z", + "exported_at": "2026-03-26T06:19:28.693Z", "milestones": [ { "id": "M001", @@ -1476,19 +1476,26 @@ "milestone_id": "M003", "slice_id": "S01", "id": "T02", - "title": "Lift canvas state to App.tsx and add Export navigation", - "status": "pending", - "one_liner": "", - "narrative": "", - "verification_result": "", + "title": "Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition", + "status": "complete", + "one_liner": "Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition", + "narrative": "Moved `useCanvasState(traceMetadata)` call from DesignCanvas.tsx to App.tsx. DesignCanvas now receives all 14 canvas state/action properties via props (using `extends UseCanvasStateReturn` on its props interface), plus `stageRef` and `onExport`. The local `stageRef` in DesignCanvas was removed; it now uses the one passed from App.tsx. App.tsx creates the ref, instantiates the hook, and spreads all return values into DesignCanvas. An Export button was added to DesignCanvas's toolbar area. The `handleExport` callback in App.tsx captures a 2x PNG data URL from the Konva stage before navigating to the export view, storing it in `pngDataUrl` state for later use by ExportView (T04). A placeholder export view shows object count and PNG capture status, with a Back to Design button.", + "verification_result": "Ran `cd app && npx tsc -b --noEmit` — 0 new type errors in App.tsx or DesignCanvas.tsx. Ran `cd app && npx vitest run` — 95/95 tests pass across 7 test files. Ran engine tests: 36/36 pass.", "duration": "", - "completed_at": null, + "completed_at": "2026-03-26T06:19:28.652Z", "blocker_discovered": false, - "deviations": "", - "known_issues": "", - "key_files": [], - "key_decisions": [], - "full_summary_md": "", + "deviations": "None.", + "known_issues": "None.", + "key_files": [ + "app/src/App.tsx", + "app/src/views/DesignCanvas.tsx" + ], + "key_decisions": [ + "UseCanvasStateReturn interface used via extends for DesignCanvasProps — avoids duplicating 14 prop types", + "PNG capture happens in handleExport before view transition so Konva stage is still mounted", + "pngDataUrl state added to App.tsx — will be passed to ExportView in T04" + ], + "full_summary_md": "---\nid: T02\nparent: S01\nmilestone: M003\nkey_files:\n - app/src/App.tsx\n - app/src/views/DesignCanvas.tsx\nkey_decisions:\n - UseCanvasStateReturn interface used via extends for DesignCanvasProps — avoids duplicating 14 prop types\n - PNG capture happens in handleExport before view transition so Konva stage is still mounted\n - pngDataUrl state added to App.tsx — will be passed to ExportView in T04\nduration: \"\"\nverification_result: passed\ncompleted_at: 2026-03-26T06:19:28.660Z\nblocker_discovered: false\n---\n\n# T02: Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition\n\n**Lifted useCanvasState() from DesignCanvas to App.tsx, threaded all state/actions as props, added Export button and PNG capture on view transition**\n\n## What Happened\n\nMoved `useCanvasState(traceMetadata)` call from DesignCanvas.tsx to App.tsx. DesignCanvas now receives all 14 canvas state/action properties via props (using `extends UseCanvasStateReturn` on its props interface), plus `stageRef` and `onExport`. The local `stageRef` in DesignCanvas was removed; it now uses the one passed from App.tsx. App.tsx creates the ref, instantiates the hook, and spreads all return values into DesignCanvas. An Export button was added to DesignCanvas's toolbar area. The `handleExport` callback in App.tsx captures a 2x PNG data URL from the Konva stage before navigating to the export view, storing it in `pngDataUrl` state for later use by ExportView (T04). A placeholder export view shows object count and PNG capture status, with a Back to Design button.\n\n## Verification\n\nRan `cd app && npx tsc -b --noEmit` — 0 new type errors in App.tsx or DesignCanvas.tsx. Ran `cd app && npx vitest run` — 95/95 tests pass across 7 test files. Ran engine tests: 36/36 pass.\n\n## Verification Evidence\n\n| # | Command | Exit Code | Verdict | Duration |\n|---|---------|-----------|---------|----------|\n| 1 | `cd app && npx tsc -b --noEmit` | 0 | ✅ pass | 5000ms |\n| 2 | `cd app && npx vitest run` | 0 | ✅ pass (95/95) | 2260ms |\n| 3 | `cd engine && .venv/bin/python -m pytest tests/test_output.py -v -W ignore::DeprecationWarning` | 0 | ✅ pass (36/36) | 390ms |\n\n\n## Deviations\n\nNone.\n\n## Known Issues\n\nNone.\n\n## Files Created/Modified\n\n- `app/src/App.tsx`\n- `app/src/views/DesignCanvas.tsx`\n", "description": "Currently `useCanvasState()` is instantiated inside `DesignCanvas.tsx`, making canvas state inaccessible to View 3. This task lifts the hook instantiation to `App.tsx`, passes state + actions as props to `DesignCanvas`, and adds an `onExport` callback prop that triggers navigation to the 'export' view. The Export view placeholder in App.tsx is updated to receive canvas state props. DesignCanvas gets an 'Export' button in its toolbar area. All existing DesignCanvas functionality must remain unchanged — this is a mechanical prop-threading refactor.\n\nKey constraints:\n- `useCanvasState` hook itself is NOT modified — only where it's called changes\n- DesignCanvas receives `state`, `addObject`, `removeObject`, `updateObject`, `selectObjects`, `deselectAll`, `reorderObject`, `toggleVisibility`, `toggleLock`, `setArtboard`, `undo`, `redo`, `canUndo`, `canRedo` as props instead of calling the hook internally\n- The `traceMetadata` param to `useCanvasState()` comes from App.tsx's existing `traceMetadata` state\n- App.tsx passes a `stageRef` to DesignCanvas and receives it back so PNG export can work later\n- A `Ref` is created in App.tsx and passed to DesignCanvas for stage access from View 3", "estimate": "1.5h", "files": [ @@ -2134,6 +2141,39 @@ "verdict": "✅ pass", "duration_ms": 1060, "created_at": "2026-03-26T06:16:59.178Z" + }, + { + "id": 40, + "task_id": "T02", + "slice_id": "S01", + "milestone_id": "M003", + "command": "cd app && npx tsc -b --noEmit", + "exit_code": 0, + "verdict": "✅ pass", + "duration_ms": 5000, + "created_at": "2026-03-26T06:19:28.653Z" + }, + { + "id": 41, + "task_id": "T02", + "slice_id": "S01", + "milestone_id": "M003", + "command": "cd app && npx vitest run", + "exit_code": 0, + "verdict": "✅ pass (95/95)", + "duration_ms": 2260, + "created_at": "2026-03-26T06:19:28.653Z" + }, + { + "id": 42, + "task_id": "T02", + "slice_id": "S01", + "milestone_id": "M003", + "command": "cd engine && .venv/bin/python -m pytest tests/test_output.py -v -W ignore::DeprecationWarning", + "exit_code": 0, + "verdict": "✅ pass (36/36)", + "duration_ms": 390, + "created_at": "2026-03-26T06:19:28.653Z" } ] } \ No newline at end of file diff --git a/app/src/App.tsx b/app/src/App.tsx index 13ab741..8569544 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -1,5 +1,7 @@ -import { useState } from 'react'; +import { useCallback, useRef, useState } from 'react'; +import type Konva from 'konva'; import type { TraceMetadata } from './types/engine'; +import { useCanvasState } from './hooks/useCanvasState'; import ImportConvert from './views/ImportConvert'; import DesignCanvas from './views/DesignCanvas'; import './App.css'; @@ -10,6 +12,13 @@ function App() { const [view, setView] = useState('import'); const [svgResult, setSvgResult] = useState(null); const [traceMetadata, setTraceMetadata] = useState(null); + const [pngDataUrl, setPngDataUrl] = useState(null); + + // Lifted canvas state — shared between View 2 (DesignCanvas) and View 3 (ExportView) + const canvasState = useCanvasState(traceMetadata); + + // Stage ref created here so View 3 can capture PNG from View 2's Konva stage + const stageRef = useRef(null); const handleUseThis = (svgOutput: string, metadata: TraceMetadata) => { setSvgResult(svgOutput); @@ -17,13 +26,40 @@ function App() { setView('canvas'); }; + const handleExport = useCallback(() => { + // Capture PNG data URL from the Konva stage before navigating away + if (stageRef.current) { + const dataUrl = stageRef.current.toDataURL({ pixelRatio: 2 }); + setPngDataUrl(dataUrl); + } + setView('export'); + }, []); + + const handleBackToCanvas = useCallback(() => { + setView('canvas'); + }, []); + return (
{view === 'import' && } {view === 'canvas' && ( - + + )} + {view === 'export' && ( +
+

View 3: Export (placeholder)

+

PNG preview captured: {pngDataUrl ? 'Yes' : 'No'}

+

Objects: {canvasState.state.objects.length}

+ +
)} - {view === 'export' &&
View 3: Export
}
); } diff --git a/app/src/views/DesignCanvas.tsx b/app/src/views/DesignCanvas.tsx index 3cae09f..64990d6 100644 --- a/app/src/views/DesignCanvas.tsx +++ b/app/src/views/DesignCanvas.tsx @@ -8,9 +8,8 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import type Konva from 'konva'; -import type { TraceMetadata } from '../types/engine'; import type { ArtboardConfig, CanvasObject, ImageObject } from '../types/canvas'; -import { useCanvasState } from '../hooks/useCanvasState'; +import type { UseCanvasStateReturn } from '../hooks/useCanvasState'; import ArtboardSetup from '../components/canvas/ArtboardSetup'; import KonvaStage from '../components/canvas/KonvaStage'; import type { CanvasTool } from '../components/canvas/KonvaStage'; @@ -21,31 +20,33 @@ import ShapeProperties from '../components/canvas/ShapeProperties'; import { toPx } from '../utils/artboardShapes'; import styles from './DesignCanvas.module.css'; -interface DesignCanvasProps { +export interface DesignCanvasProps extends UseCanvasStateReturn { svgData: string | null; - traceMetadata: TraceMetadata | null; + /** Ref to the Konva Stage, created in App.tsx for cross-view access (PNG export). */ + stageRef: React.RefObject; + /** Called when user clicks Export — App.tsx navigates to View 3. */ + onExport: () => void; } export default function DesignCanvas({ svgData, - traceMetadata, + state, + addObject, + removeObject, + updateObject, + selectObjects, + deselectAll, + reorderObject, + toggleVisibility, + toggleLock, + setArtboard, + undo, + redo, + canUndo, + canRedo, + stageRef, + onExport, }: DesignCanvasProps) { - const { - state, - addObject, - removeObject, - updateObject, - selectObjects, - deselectAll, - reorderObject, - toggleVisibility, - toggleLock, - setArtboard, - undo, - redo, - canUndo, - canRedo, - } = useCanvasState(traceMetadata); const [activeTool, setActiveTool] = useState('pointer'); const [showArtboardSetup, setShowArtboardSetup] = useState(true); @@ -53,7 +54,6 @@ export default function DesignCanvas({ const [showGrid, setShowGrid] = useState(false); const [zoomLevel, setZoomLevel] = useState(1); - const stageRef = useRef(null); const canvasContainerRef = useRef(null); const [stageSize, setStageSize] = useState({ width: 800, height: 600 }); @@ -265,6 +265,15 @@ export default function DesignCanvas({ onZoomOut={handleZoomOut} onZoomFit={handleZoomFit} /> + {/* Main area: canvas + right panel */}