-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathprd.json
More file actions
373 lines (373 loc) · 20 KB
/
prd.json
File metadata and controls
373 lines (373 loc) · 20 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
{
"projectName": "ElmishPaint",
"branchName": "main",
"description": "A 1-bit monochrome pixel art editor built with F#, Fable, Elmish, and Feliz. Homage to MacPaint targeting the Macintosh Classic II 512x342 monochrome display.",
"userStories": [
{
"id": "S01-scaffold",
"spec": "Project Setup",
"title": "Project scaffolding and build toolchain",
"priority": 1,
"description": "Initialize the project with F# (.fsproj), package.json, Vite config (with vite-plugin-fable), TailwindCSS config, index.html entry point, and a minimal App.fs that renders 'Hello ElmishPaint' via Feliz. The project must compile via Fable and serve via Vite dev server.",
"acceptanceCriteria": [
"dotnet fable src compiles with zero warnings",
"npx vite build produces a working production bundle",
"npx vite dev serves the app with HMR",
"The browser displays 'Hello ElmishPaint' rendered by a Feliz React component",
"TailwindCSS utility classes are functional in the rendered output",
".fsproj lists all .fs files in correct dependency order"
],
"passes": true
},
{
"id": "S02-types",
"spec": "Spec 1, 2",
"title": "Core type definitions: Bit, Tool, Pattern, Modifiers, Model, Msg",
"priority": 2,
"description": "Define all shared types in Types.fs, Model.fs, and Msg.fs. This includes Bit (White|Black), Tool DU, Pattern record, Modifiers record, MouseState, UIState, HistoryState, Selection, ImportPreview, the top-level Model record, and the complete Msg discriminated union. No implementation — only type definitions.",
"acceptanceCriteria": [
"All types from the PRD are defined and compile",
"Msg DU includes all cases: SelectTool, SelectPattern, CanvasMouseDown/Move/Up, Undo, Redo, ClearSelection, MoveSelection, StampSelection, ImportImage, ImportPreviewReady, ConfirmImport, CancelImport, ExportPNG, SetZoom, ScrollCanvas, KeyDown",
"Model record includes all fields: Canvas, Tool, ToolOptions, Pattern, Mouse, Selection, History, UI, ImportPreview",
".fsproj file order is correct: Types.fs before Model.fs before Msg.fs",
"dotnet fable src compiles with zero warnings"
],
"passes": true
},
{
"id": "S03-bitcanvas",
"spec": "Spec 1",
"title": "BitCanvas data structure and core operations",
"priority": 3,
"description": "Implement BitCanvas.fs: the 512x342 packed bit array backed by Uint8Array. Implement create, getPixel, setPixel, clear, fill, clone. Out-of-bounds getPixel returns White, out-of-bounds setPixel is a no-op.",
"acceptanceCriteria": [
"BitCanvas.create() produces a 512x342 all-white canvas",
"getPixel and setPixel correctly address all 175,104 pixels",
"Out-of-bounds getPixel returns White",
"Out-of-bounds setPixel is a no-op (no crash)",
"clone produces a deep copy — mutations to clone do not affect original",
"Vitest unit tests verify all acceptance criteria",
"dotnet fable src compiles with zero warnings"
],
"passes": true
},
{
"id": "S04-bitcanvas-imagedata",
"spec": "Spec 1",
"title": "BitCanvas toImageData and fromImageData",
"priority": 4,
"description": "Implement toImageData (with integer scale factor) and fromImageData (threshold to 1-bit). toImageData at scale 1 produces 512x342 ImageData. At scale 2, each pixel becomes a 2x2 block. Round-trip fromImageData(toImageData 1 canvas) must be identical.",
"acceptanceCriteria": [
"toImageData at scale 1 produces 512x342 ImageData with correct black/white values",
"toImageData at scale 2 produces 1024x684 ImageData with 2x2 pixel blocks",
"fromImageData correctly thresholds to 1-bit",
"Round-trip: fromImageData(toImageData 1 canvas) produces identical BitCanvas",
"Vitest unit tests verify all acceptance criteria"
],
"passes": true
},
{
"id": "S05-elmish-runtime",
"spec": "Spec 2",
"title": "Elmish runtime wiring with Feliz.UseElmish",
"priority": 5,
"description": "Implement Update.fs with a skeleton update function that exhaustively matches all Msg cases (returning model unchanged + Cmd.none for each). Wire App.fs with Feliz.UseElmish(init, update, [||]). The init function returns a default Model with a blank BitCanvas. Verify dispatching a message triggers a re-render.",
"acceptanceCriteria": [
"update function exhaustively matches all Msg cases (zero compiler warnings)",
"init returns a valid default Model with blank canvas",
"Feliz.UseElmish wiring compiles and renders",
"Dispatching SelectTool changes model.Tool and triggers re-render",
"Cmd.none produces no side effects",
"Vitest tests verify update is pure: same input → same output",
"dotnet fable src compiles with zero warnings"
],
"passes": true
},
{
"id": "S06-canvas-render",
"spec": "Spec 3",
"title": "Canvas rendering at 1x zoom",
"priority": 6,
"description": "Implement CanvasView.fs: a Feliz v3 ReactComponent that renders BitCanvas onto an HTML5 canvas element at 1x zoom (512x342 pixels). Use React.useRef for canvas element reference and React.useEffect for rendering lifecycle. Mouse events on the canvas dispatch CanvasMouseDown/Move/Up with correct pixel coordinates.",
"acceptanceCriteria": [
"Canvas element renders at 512x342 pixels",
"Rendered output matches BitCanvas pixel-for-pixel",
"Mouse events dispatch correct pixel coordinates",
"Setting a pixel via update and re-rendering shows the change",
"dotnet fable src compiles with zero warnings"
],
"passes": true
},
{
"id": "S06B-fix",
"spec": "Quality",
"title": "Fix S06 build errors and harden verification pipeline",
"priority": 6,
"description": "Fix FS0247 namespace collision in CanvasView.fs, fix ImageData constructor TypeError in BitCanvas.toImageData, remove vite-plugin-fable.",
"acceptanceCriteria": [
"dotnet build succeeds with zero errors",
"dotnet fable src -e fs.jsx succeeds with zero warnings",
"pnpm test passes (including toImageData and CanvasView tests)",
"pnpm build succeeds",
"App renders in browser without ImageData TypeError",
"vite-plugin-fable removed from vite.config.js and package.json"
],
"passes": true
},
{
"id": "S07-canvas-zoom",
"spec": "Spec 3",
"title": "Canvas zoom levels and coordinate mapping",
"priority": 7,
"description": "Extend CanvasView to support zoom levels 1x, 2x, 4x, 8x. At zoom N, canvas element is 512*N x 342*N pixels. Mouse coordinates are correctly divided by zoom to map to BitCanvas pixel coordinates. Optional pixel grid overlay at zoom >= 4x.",
"acceptanceCriteria": [
"At 8x zoom, individual pixels are clearly visible as 8x8 blocks",
"Mouse position correctly maps to canvas pixel coordinates at every zoom level",
"Drawing at high zoom produces same result as drawing at 1x on same pixel",
"Pixel grid overlay visible at zoom >= 4x, not visible below",
"SetZoom message changes zoom level and re-renders correctly"
],
"passes": true
},
{
"id": "S08-pencil",
"spec": "Spec 4a",
"title": "Pencil tool with Bresenham line drawing",
"priority": 8,
"description": "Implement Pencil.fs and the Bresenham line algorithm in Algorithms.fs. Pencil toggles pixel polarity on mouse-down (white→draws black for entire stroke, black→draws white). Consecutive mouse-move events use Bresenham interpolation for gap-free lines. Stroke is committed to BitCanvas on mouse-up.",
"acceptanceCriteria": [
"Clicking a white pixel turns it black",
"Clicking a black pixel turns it white",
"Polarity is locked for the entire stroke duration",
"Fast diagonal movement produces a continuous line with no gaps",
"Stroke is committed to BitCanvas on mouse-up",
"Vitest tests verify Bresenham produces correct pixels for known inputs",
"Vitest tests verify pencil polarity locking"
],
"passes": true
},
{
"id": "S09-undo-redo",
"spec": "Spec 5",
"title": "Undo/redo with immutable history stacks",
"priority": 9,
"description": "Implement History module: push, undo, redo, canUndo, canRedo. History stores BitCanvas snapshots with max depth of 50. Every tool stroke pushes previous canvas onto undo stack. Undo pops undo stack, pushes current to redo. New edit after undo clears redo stack.",
"acceptanceCriteria": [
"Drawing a stroke then Undo restores pre-stroke canvas exactly",
"Undo then Redo restores the stroke",
"Drawing after undo clears redo stack",
"50 sequential operations can all be undone",
"Undo past beginning of history is a no-op",
"Redo with empty redo stack is a no-op",
"Undo/redo does not change tool selection, zoom, or non-canvas state",
"Vitest tests verify all acceptance criteria"
],
"passes": true
},
{
"id": "S10-eraser",
"spec": "Spec 4b",
"title": "Eraser tool with configurable brush size",
"priority": 10,
"description": "Implement Eraser.fs. Eraser sets pixels to White along stroke path. Configurable square brush sizes: 1x1, 2x2, 4x4, 8x8. Uses Bresenham interpolation between mouse-move events. Brush size selectable from ToolOptions.",
"acceptanceCriteria": [
"At 1x1, eraser draws white pixels like an inverted pencil",
"At 8x8, a single click clears an 8x8 block",
"Dragging across black region leaves continuous white trail with no gaps",
"Changing brush size takes effect on next stroke, not mid-stroke",
"Eraser strokes are single undo entries",
"Vitest tests verify eraser behavior at each brush size"
],
"passes": true
},
{
"id": "S11-line-tool",
"spec": "Spec 4c",
"title": "Line tool with XOR preview",
"priority": 11,
"description": "Implement Line.fs. Click sets start point, drag shows XOR preview line, release commits using Bresenham. Preview inverts pixels so line is visible on both black and white backgrounds.",
"acceptanceCriteria": [
"Horizontal line from (0,100) to (511,100) produces exactly 512 black pixels",
"45-degree line has no gaps",
"Preview is visible over both black and white regions",
"Releasing mouse commits the line as a single undo entry",
"Vitest tests verify line endpoints and pixel count for known inputs"
],
"passes": true
},
{
"id": "S12-rectangle",
"spec": "Spec 4d",
"title": "Rectangle and Filled Rectangle tools",
"priority": 12,
"description": "Implement Rectangle.fs. Two modes: outline (1px border, hollow) and filled. Click sets one corner, drag to opposite, release commits. Filled rectangle uses the active Pattern. Preview shown during drag.",
"acceptanceCriteria": [
"Outline rectangle from (10,10) to (50,50) is 41x41 pixels, 1px border, hollow",
"Filled rectangle with solid black pattern fills entire region",
"Filled rectangle with dither pattern fills correctly",
"Preview shown during drag",
"Rectangle is single undo entry",
"Vitest tests verify outline and filled rectangle pixel output"
],
"passes": true
},
{
"id": "S13-flood-fill",
"spec": "Spec 4e",
"title": "Flood fill with iterative algorithm",
"priority": 13,
"description": "Implement FloodFill.fs using an iterative (not recursive) flood fill algorithm. Fills contiguous same-color region. Respects active Pattern. Must handle full 512x342 canvas without stack overflow.",
"acceptanceCriteria": [
"Filling white region surrounded by black fills only enclosed area",
"Filling with pattern produces correct tiled pattern in region",
"Filling entire blank canvas completes without crash or hang",
"Filling single isolated pixel works",
"Flood fill is single undo entry",
"Vitest tests verify fill boundaries and pattern application"
],
"passes": true
},
{
"id": "S14-patterns",
"spec": "Spec 6",
"title": "Pattern definitions and sampling",
"priority": 14,
"description": "Implement Patterns.fs with all 12 built-in 8x8 patterns: solid black, solid white, 50% checkerboard, 25% dot, 75% dot, horizontal stripes, vertical stripes, diagonal stripes (left/right), crosshatch, brick, polka dot. Implement the sample function for global-coordinate pattern lookup.",
"acceptanceCriteria": [
"All 12 patterns defined as 8x8 bool arrays",
"Pattern.sample returns correct bit for any global (x,y) coordinate",
"Pattern alignment is global — shifting a shape shifts its pattern content",
"Vitest tests verify each pattern tile and sampling at boundary coordinates"
],
"passes": true
},
{
"id": "S15-pattern-palette",
"spec": "Spec 6, 10",
"title": "Pattern palette UI component",
"priority": 15,
"description": "Implement PatternPalette.fs: a grid of pattern swatches. Each swatch renders the 8x8 tile tiled into a small preview. Clicking a swatch dispatches SelectPattern. Active pattern is visually highlighted.",
"acceptanceCriteria": [
"All 12+ patterns displayed as visually distinct swatches",
"Active pattern has distinct visual highlight",
"Clicking a swatch dispatches SelectPattern and updates model",
"Filled rectangle and flood fill use the selected pattern"
],
"passes": true
},
{
"id": "S16-marquee",
"spec": "Spec 7",
"title": "Marquee selection: select, move, stamp",
"priority": 16,
"description": "Implement Marquee.fs. Click-drag selects rectangular region with marching ants border. Selected region lifts (original area becomes white). Arrow keys move selection by 1px. Clicking outside stamps. Escape returns to original position. Delete clears. Full cycle is single undo entry.",
"acceptanceCriteria": [
"Dragging shows marching ants around selection boundary",
"Selected region lifts — original area becomes white",
"Arrow keys move selection by exactly 1px per press",
"Stamping merges floating pixels with canvas at new position",
"Off-canvas pixels are clipped",
"Escape returns selection to original position and stamps",
"Delete clears selection area",
"Full select-move-stamp is one undo entry",
"Marching ants animate smoothly"
],
"passes": true
},
{
"id": "S17-toolbar",
"spec": "Spec 10",
"title": "Toolbar, zoom controls, and status bar",
"priority": 17,
"description": "Implement Toolbar.fs, StatusBar.fs, and integrate layout in App.fs. Toolbar has all 7 tool buttons with active highlight, eraser brush size selector. Zoom controls (1x/2x/4x/8x). Status bar shows mouse coordinates. Undo/redo buttons enabled/disabled by history state. Import/export action buttons.",
"acceptanceCriteria": [
"All 7 tools selectable with distinct active state",
"Eraser brush size selector appears when eraser is active",
"Zoom controls change canvas display zoom",
"Status bar shows correct pixel coordinates on mouse move",
"Undo/redo buttons enabled/disabled based on history state",
"Import and export actions accessible from UI",
"Layout does not break at 1280x720 and above"
],
"passes": true
},
{
"id": "S18-keyboard",
"spec": "Spec 11",
"title": "Keyboard shortcuts",
"priority": 18,
"description": "Set up keyboard subscription via React.useEffect on document keydown. Dispatch KeyDown messages. Handle all shortcuts: P/E/L/R/F/M for tools, Ctrl+Z/Ctrl+Shift+Z for undo/redo, Ctrl+S/Ctrl+Shift+S for export, Ctrl+I for import, Delete/Backspace for clear selection, Escape for cancel, 1/2/3/4 for zoom, arrows for selection move.",
"acceptanceCriteria": [
"Each shortcut triggers documented action",
"Shortcuts do not fire when text input is focused",
"Ctrl+S prevents browser save dialog",
"Shortcuts discoverable via tooltips on toolbar buttons"
],
"passes": true
},
{
"id": "S19-dithering",
"spec": "Spec 8",
"title": "Atkinson dithering algorithm",
"priority": 19,
"description": "Implement Dithering.fs with the Atkinson dithering algorithm. Input: float array (grayscale), width, height, threshold offset. Output: BitCanvas. Atkinson distributes 6/8 of error (not full Floyd-Steinberg). Implement grayscale conversion and image scaling to fit 512x342.",
"acceptanceCriteria": [
"Atkinson output matches expected algorithm: 6/8 error diffusion pattern",
"Threshold offset parameter visibly changes output",
"Output is a valid BitCanvas",
"Vitest tests verify dithering output for known small input images",
"Vitest tests verify 6/8 diffusion pattern (not 16/16 Floyd-Steinberg)"
],
"passes": true
},
{
"id": "S20-import",
"spec": "Spec 8",
"title": "Image import with dithering preview",
"priority": 20,
"description": "Implement ImportDialog.fs and file import flow. User imports via file picker or drag-and-drop. Image is scaled to fit 512x342, converted to grayscale, dithered via Atkinson. Preview shown with brightness/threshold slider. Confirm replaces canvas (single undo entry). Cancel returns to previous state. Heavy computation must not block UI.",
"acceptanceCriteria": [
"File picker accepts PNG, JPEG, GIF, WebP",
"Drag-and-drop opens import preview",
"Image scaled to fit 512x342 without distortion",
"Brightness/threshold slider produces visible changes in dithered output",
"Confirm replaces canvas and is single undo entry",
"Cancel returns to previous canvas with no changes",
"Import of 4000x3000 image completes within 2 seconds",
"UI remains responsive during dithering"
],
"passes": true
},
{
"id": "S21-export",
"spec": "Spec 9",
"title": "PNG export at multiple scales",
"priority": 21,
"description": "Implement export flow. Export current canvas as PNG at 1x (512x342), 2x (1024x684), 4x (2048x1368). PNG is strictly black/white only. Triggers browser file download. Ctrl+S = 1x export, Ctrl+Shift+S = 2x export.",
"acceptanceCriteria": [
"1x export is exactly 512x342 pixels",
"2x export is exactly 1024x684 pixels with 2x2 blocks",
"Exported PNG contains only #000000 and #FFFFFF pixels",
"Export triggers browser file download",
"Ctrl+S triggers 1x export",
"Export does not mutate canvas or model"
],
"passes": true
},
{
"id": "S22-weekend-wrap",
"spec": "Release",
"title": "Weekend wrap-up: GitHub Pages deployment",
"priority": 22,
"description": "Finalize the weekend demo by configuring GitHub Pages deployment for the repository rastreus/ElmishPaint using GitHub Actions (build with pnpm, upload ./dist, deploy to Pages). Ensure Vite is configured for project pages with base path '/ElmishPaint/'.",
"acceptanceCriteria": [
"Vite config sets base to '/ElmishPaint/' so asset and route URLs work under https://rastreus.github.io/ElmishPaint/",
"A GitHub Pages deployment workflow exists at .github/workflows/deploy.yml that installs with pnpm, runs the build, uploads ./dist as the Pages artifact, and deploys via GitHub Actions",
"package.json has scripts for build and preview (and start/dev as already present) and pnpm build produces the dist/ folder successfully",
"pnpm test passes and pnpm build passes after these changes",
"The story is marked passes=true after verification is complete"
],
"passes": true
}
]
}