Naming note (read this first).
PHASE1_RESULT.mddocuments the first executed development session, which was ROADMAP.md Phase 0 — "Bootstrap & Command Engine." This file,PHASE2_RESULT.md, documents the second executed development session, which is ROADMAP.md Phase 1 — "Drawing MVP." The file numbers count executed sessions; the ROADMAP numbers count phases and start at 0. So the mapping is:PHASE1_RESULT.md→ ROADMAP Phase 0,PHASE2_RESULT.md→ ROADMAP Phase 1. This offset is inherited from the first session's instructions and is preserved deliberately rather than silently renumbered.
- Phase: ROADMAP.md Phase 1 — "Drawing MVP" (second executed session)
- Plan:
Plans/archive/PLAN-phase1.md(wasPLAN.md, Status: LOCKED during execution) - Session start: 2026-09-19T23:20:00+08:00 (Asia/Shanghai, UTC+8)
- Completion timestamp: 2026-09-20T03:00:00+08:00
- Status: COMPLETE — 11 of 11 tasks finished; 19 of 19 acceptance
criteria PASS, with no PARTIAL and no FAIL. The one criterion that was
PARTIAL at the end of Phase 0 (the
Profiles//Skills/versioning question, "I2") is resolved and now passes as AC18. - Final commit SHA:
fc57ffb(fc57ffbc5d048d2dae235c3207e389e587ce5698) — the Task 11 close-out commit. As in the previous session, a commit cannot contain its own hash by construction; this value was captured withgit rev-parse HEADimmediately after committing and the file then amended in place solely to insert it. Rungit log --oneline -1to confirm the currentHEAD. - Tasks completed: 11 / 11
- Tests: 170 passing, 0 failing (Phase 0 baseline: 51)
- Build warnings: 0
Every row below was re-run against the final tree. No row is claimed from memory.
| # | Criterion (verbatim from PLAN.md) |
Result | Evidence |
|---|---|---|---|
| AC1 | cargo build --workspace succeeds from this repository path (space + apostrophe) with zero errors |
PASS | cd "/Users/duidui/Documents/81n's Knowledge base/The knowledge base of Projects/Raw/Photoshop_Cad_AI_Blender_Platform" && cargo build --workspace → Finished \dev` profile [unoptimized + debuginfo] target(s) in 0.15s. Zero errors and, separately verified, **zero warnings** (cargo build --workspace 2>&1 | grep -c warning→0`). |
| AC2 | cargo test --workspace passes with zero failures and at least 90 tests total (Phase 0 baseline was 51) |
PASS | cargo test --workspace → 170 passed, 0 failed, across 8 test binaries: pixelcad-app bin 54; pixelcad-cli bin 0; cli/tests/determinism.rs 2; cli/tests/import_round_trip.rs 4; pixelcad-core lib 101; core/tests/blueprint_dogfood.rs 7; core/tests/ship_determinism.rs 2; doc-tests 0. 170 ≥ 90 and is 3.3× the Phase 0 baseline. |
| AC3 | cargo tree -p pixelcad-core lists no slint, UI, GPU, AI or CAD crate (only thiserror and its proc-macro chain) |
PASS | cargo tree -p pixelcad-core → pixelcad-core → thiserror v1.0.69 → thiserror-impl → {proc-macro2, quote, syn, unicode-ident}. Nothing else. cargo tree -p pixelcad-core | grep -icE 'slint|wgpu|winit|onnx|gpu' → 0. Note that the two new core modules added this phase (base64.rs, project.rs) were written dependency-free specifically to keep this true. |
| AC4 | Phase 0 regression guard: ship.pxc still produces sha256 bde0086…232, byte-identical to Phase 0 |
PASS | cargo run -p pixelcad-cli -- run docs/samples/ship.pxc --out out.png && shasum -a 256 out.png → bde008684f8384379e33f514d15d4ca4c1aed97798d52a08bbb1cccd6f781232, exactly the value recorded in PHASE1_RESULT.md. Re-verified after every task that touched core. Also pinned permanently by core/tests/ship_determinism.rs::ship_script_composites_to_the_phase_0_byte_sequence, which asserts the composited buffer's FNV-1a equals 9723217798654324057 — so a future change cannot break this quietly, only loudly. |
| AC5 | Multi-layer compositing: top-over-bottom is correct; visibility and opacity change the output; asserted on exact pixel values | PASS | document::tests::{top_layer_composites_over_bottom_layer, hidden_layer_is_excluded_from_the_composite, layer_opacity_blends_toward_the_layer_below, zero_opacity_layer_contributes_nothing, composite_of_all_hidden_layers_is_transparent}. The opacity test asserts an exact value (128), not a range, because compositing is integer math and must be bit-reproducible. |
| AC6 | Every Command variant round-trips through serialize → parse unchanged, with a test that enumerates all variants |
PASS | parser::tests::{round_trips_all_command_kinds, round_trips_full_script, every_command_variant_is_covered_by_the_round_trip_test, every_command_has_a_distinct_name}. This is fail-closed: parser::tests::variant_index is an exhaustive match with no wildcard arm, so adding a 20th Command variant makes the test module fail to compile until the author also bumps VARIANT_COUNT and adds a round-trip sample. |
| AC7 | Brush (size ≥ 1), eraser, fill, eyedropper, line, rectangle (outlined and filled) and selection each have a core unit test asserting exact pixels, and each is reachable only via a Command |
PASS | engine::tests::{brush_size_one_behaves_like_the_pencil, brush_size_three_paints_a_three_by_three_block_per_point, a_wide_brush_at_the_canvas_edge_clips_instead_of_failing, the_eraser_is_a_transparent_brush_stroke, outlined_rectangle_leaves_its_interior_empty, filled_rectangle_differs_from_the_outline_by_exactly_its_interior, rectangle_corners_may_be_given_in_any_order, flood_fill_stops_at_a_drawn_border, flood_fill_with_the_colour_already_present_terminates, flood_fill_on_an_empty_canvas_fills_everything, flood_fill_acts_on_the_active_layer_only, color_set_and_pick_drive_the_active_colour, color_pick_reads_the_composite_not_the_active_layer, line_draw_sets_every_bresenham_point}. "Reachable only via a Command" is structural: Engine::apply is the sole mutator and it takes a &Command; Controller has no path to Document mutation that bypasses it. |
| AC8 | A rectangular selection clips every drawing command — nothing is written outside it | PASS | engine::tests::{selection_clips_pixel_and_line_writes, selection_clips_a_brush_stroke_and_a_flood_fill, a_fill_cannot_leak_around_the_selection_boundary}. The last one matters most: the flood fill treats the marquee as a wall, not merely a write mask, so it cannot walk around the boundary and re-enter. Also proven on real content by blueprint_dogfood::the_selection_in_the_blueprint_actually_masked_the_name_plate_fill. |
| AC9 | A project saved and reloaded yields an identical content_hash() and an identical command history |
PASS | project::tests::{save_and_reload_preserves_the_document_and_the_history, a_round_trip_is_a_fixed_point} — the second asserts save → load → save is byte-identical, which is stronger than the criterion asked for. Also at GUI level: gui_tests::a_project_saved_from_the_ui_reopens_identically, and on the real sample: blueprint_dogfood::the_blueprint_survives_a_save_and_reload_cycle. |
| AC10 | A newer-version .pxcproj is rejected with a typed error naming both versions, and the document is left untouched (no partial load) |
PASS | project::tests::{a_future_version_is_rejected_and_names_both_versions, a_future_version_leaves_no_partial_document}; at binary level import_round_trip::a_future_version_project_is_refused_and_writes_no_output; at GUI level controller::tests::opening_a_broken_project_leaves_the_open_document_untouched and gui_tests::a_failed_open_reports_the_error_and_preserves_the_open_document. Manually: pixelcad-cli run future.pxcproj --out never.png → stderr project file declares format version 99, but this build of PixelCAD supports at most version 1 — upgrade PixelCAD to open it, exit code 1, and never.png does not exist. |
| AC11 | import <png> then run <pxcproj> produces a PNG whose decoded RGBA equals the source's |
PASS | import_round_trip::importing_a_png_and_replaying_the_project_reproduces_the_source_pixels — drives the real compiled binary twice, on a deliberately non-power-of-two 23×17 image containing fully transparent, 50 %-alpha and opaque pixels, and asserts the decoded buffers are equal. |
| AC12 | Running the same .pxcproj twice produces byte-identical PNGs |
PASS | import_round_trip::replaying_a_project_twice_produces_byte_identical_pngs and determinism::rendering_the_blueprint_project_twice_produces_byte_identical_png. Manually: two runs of blueprint.pxcproj both hash to abdf81edb8e8c52f7aba5c7977bf66bc5bc622eecac54456e099120b21d08f1a. |
| AC13 | A headless GUI test proves one pointer drag is undone by a single Undo click | PASS | gui_tests::one_pointer_drag_is_undone_by_a_single_undo_click — dispatches a real press, six real PointerMoved events and a real release through Slint, asserts 7 pixels were painted, clicks the real Undo button once, then asserts 0 pixels remain and can-undo is false (i.e. it landed exactly on the empty canvas, not somewhere mid-stroke). |
| AC14 | Headless GUI tests dispatch real key events proving tool shortcuts and Ctrl/Cmd+Z | PASS | gui_tests::{keyboard_shortcuts_select_every_tool, the_accelerator_shortcut_undoes_and_redoes, bracket_keys_change_the_brush_size_and_digits_pick_swatches, an_unbound_key_is_not_consumed}. The first iterates Tool::ALL, so adding a tool without a working shortcut fails the test. The accelerator test dispatches a real Key::Control press around the z, covering both undo and (uppercase Z) redo. |
| AC15 | A headless GUI test proves editing a palette swatch emits a palette.set command that appears in the saved script |
PASS | gui_tests::editing_a_swatch_through_the_ui_records_a_palette_command — clicks the real Set button, then saves and greps the file for the literal palette.set index=3 color="#0a141eff". Complemented by gui_tests::an_invalid_swatch_hex_reports_an_error_and_changes_nothing. |
| AC16 | A headless GUI test proves adding, selecting, drawing on, and hiding a layer all work through the real wired callbacks | PASS | gui_tests::{layers_can_be_added_drawn_on_hidden_and_deleted_through_the_ui, selecting_a_layer_row_makes_it_active}. The first clicks the real Add button, verifies the drawing landed on the new layer and not the old one, clicks the real visibility checkbox and asserts the composite goes fully transparent, then clicks Delete. |
| AC17 | docs/samples/blueprint.pxcproj exists, is a valid version-1 project with ≥ 3 layers, renders, and renders byte-identically twice |
PASS | 7 tests in core/tests/blueprint_dogfood.rs plus determinism::rendering_the_blueprint_project_twice_produces_byte_identical_png. The file is 128×96 with 4 layers (Paper, Grid, Hull, Annotation); the_blueprint_exercises_the_phase_1_tool_set asserts it uses 13 distinct commands. Rendered sha256 (both runs): abdf81edb8e8c52f7aba5c7977bf66bc5bc622eecac54456e099120b21d08f1a. Visually inspected once via an ASCII-luminance dump of the decoded PNG (no screenshot of the desktop was taken, so no personal data was involved this time — see Phase 0's Known Limitations for why that matters): the ship reads correctly with hull, three raked masts, yards, shrouds, forestay to the bowsprit, gun ports, waterline, scale bar and name plate. |
| AC18 | git ls-files lists the four Profiles//Skills/ markdown files (I2 resolved), while /target, *.log, /cache and /Agents remain ignored |
PASS | git ls-files Profiles Skills → Profiles/Autopilot/profile.md, Profiles/INDEX.md, Profiles/Planner/profile.md, Skills/INDEX.md. git check-ignore -v target cache Agents x.log → all four matched by .gitignore lines 11, 9, 7 and 1 respectively. The "must not modify Profiles/**" constraint was honoured: git log --follow -- Profiles/Planner/profile.md shows exactly one commit (ffad2d6, the commit that added it); the bytes were never edited. |
| AC19 | README.md documents the full command grammar as implemented, and BACKLOG.md exists with every out-of-scope discovery |
PASS | README.md documents all 19 commands grouped by area, both file formats, the strict-vs-lenient out-of-bounds rule, the "there is no eraser command" gotcha, and the full keyboard/mouse control table. BACKLOG.md exists with 7 open items. |
✓ = newly created this phase; the rest were modified.
PHASE2_RESULT.md ✓ This file
PLAN.md → Plans/archive/PLAN-phase1.md ✓ The Phase 1 plan, archived at close-out
PROGRESS.md ✓ Phase 1 session log (task-by-task)
Plans/archive/PROGRESS-phase0.md ✓ Phase 0's log, archived at Task 1
BACKLOG.md ✓ 7 carried-forward items
.gitignore No longer ignores /Profiles, /Skills (I2)
README.md Rewritten: full grammar, formats, controls
ROADMAP.md Phase 1 → ACTIVE at Task 1, → COMPLETE at Task 11
Profiles/{INDEX.md,Planner/,Autopilot/} Now tracked in git; bytes unchanged
Skills/INDEX.md Now tracked in git
crates/core/src/document.rs REWRITTEN: Layer struct, layer stack, composite()
crates/core/src/command.rs +12 variants (7 layer, 4 colour/selection, image.import)
+ sanitize_name()
crates/core/src/parser.rs Parse/serialize for all new variants; parse_usize,
parse_bool; fail-closed variant-coverage test
crates/core/src/engine.rs Step grouping (begin_group/end_group); current_color
and selection in EngineState; write_strict /
write_lenient; flood_fill; brush/rect/import execution
crates/core/src/project.rs ✓ Versioned .pxcproj container: parse/serialize/open
crates/core/src/base64.rs ✓ Dependency-free RFC 4648 codec
crates/core/src/lib.rs Module wiring and re-exports for the two new modules
crates/core/tests/ship_determinism.rs + pinned Phase 0 composite-byte regression test
crates/core/tests/blueprint_dogfood.rs ✓ 7 tests asserting the dogfood artefact's properties
crates/cli/src/main.rs `import` subcommand; PNG decode + colour-type
normalisation; exports composite(); opens both formats
crates/cli/tests/determinism.rs + blueprint byte-identity test
crates/cli/tests/import_round_trip.rs ✓ 4 binary-level tests (import round trip, project
determinism, future-version refusal, legacy script)
crates/app/src/controller.rs REWRITTEN: Tool enum, brush size, deferred anchored
gestures, layer ops, swatch editing, project open/save
crates/app/src/main.rs handle_shortcut() table; layer/tool/selection state
pushed to UI; 15 new headless GUI tests
crates/app/src/render.rs Renders composite() instead of a single buffer
crates/app/ui/main.slint Tool column, brush size, layer panel, hex swatch entry,
Open/Save-project controls, key-pressed forwarding
docs/samples/blueprint.pxcproj ✓ 128x96 four-layer historical ship blueprint
docs/samples/ship.pxc Unchanged (deliberately — it is the regression guard)
cd "/Users/duidui/Documents/81n's Knowledge base/The knowledge base of Projects/Raw/Photoshop_Cad_AI_Blender_Platform"
# Build everything (expect 0 errors, 0 warnings)
cargo build --workspace
# Full test suite (expect: 170 passed, 0 failed)
cargo test --workspace
# Phase 0 regression guard — this hash must never change
cargo run -p pixelcad-cli -- run docs/samples/ship.pxc --out ship.png
shasum -a 256 ship.png
# bde008684f8384379e33f514d15d4ca4c1aed97798d52a08bbb1cccd6f781232
# Phase 1 dogfood artefact
cargo run -p pixelcad-cli -- run docs/samples/blueprint.pxcproj --out blueprint.png
shasum -a 256 blueprint.png
# abdf81edb8e8c52f7aba5c7977bf66bc5bc622eecac54456e099120b21d08f1a
file blueprint.png
# PNG image data, 128 x 96, 8-bit/color RGBA, non-interlaced
# PNG import round trip
cargo run -p pixelcad-cli -- import blueprint.png --out reimported.pxcproj
cargo run -p pixelcad-cli -- run reimported.pxcproj --out reimported.png
# reimported.png has the same pixels as blueprint.png (same bytes, in fact,
# since both are a single opaque layer written by the same encoder)
# Refusing a future-version project (expect exit 1 and no output file)
printf 'pixelcad.project version=99\ncanvas.new width=4 height=4\n' > future.pxcproj
cargo run -p pixelcad-cli -- run future.pxcproj --out never.png; echo "exit=$?"
# error: cannot open future.pxcproj: project file declares format version 99,
# but this build of PixelCAD supports at most version 1 — upgrade PixelCAD to open it
# exit=1
# Core has no UI/GPU dependency
cargo tree -p pixelcad-core
# Expected: only thiserror (+ its proc-macro build-time dependencies)
rm -f ship.png blueprint.png reimported.png reimported.pxcproj future.pxcproj
# Launch the GUI (interactive; requires a real display)
cargo run -p pixelcad-appIn the GUI: pick a tool from the left column or with b/e/g/i/l/r/m;
drag on the canvas to draw; [/] change brush size; 1–8 pick palette
slots; type a hex value and press Set to recolour the highlighted
swatch; use the right-hand panel to add, select, hide or delete layers;
Ctrl/Cmd+Z and Shift+Ctrl/Cmd+Z walk the undo history one
gesture at a time; Save project writes a .pxcproj that Open
reads back and that pixelcad-cli run renders identically.
This section is a diff against PHASE1_RESULT.md Section 5, not a
restatement of it. Phase 0's shape (one core, one app, one cli; text
commands; snapshot undo) is intact. Five things changed.
flowchart TB
subgraph before["Phase 0"]
D0["Document\nwidth, height, pixels: Vec u8"]
end
subgraph after["Phase 1"]
D1["Document\nwidth, height, active: usize"]
L0["Layer 0 (bottom)\nname, visible, opacity, pixels"]
L1["Layer 1"]
LN["Layer n (top)"]
C["composite() -> Vec u8\ninteger alpha-over, bottom to top"]
D1 --> L0
D1 --> L1
D1 --> LN
L0 --> C
L1 --> C
LN --> C
end
Document::pixels() is gone; composite() replaced it at both consumer
sites (cli/src/main.rs, app/src/render.rs). get_pixel/set_pixel kept
their exact Phase 0 signatures and now act on the active layer, which is why
the rest of the codebase compiled unchanged through the reshape.
Keeping Phase 0's PNG byte-identical through this change was a hard
requirement, met by three deliberate properties, each with its own test:
scale_u8(v, 255) == v exactly; over(src, transparent) == src exactly;
and a fast path in composite() that returns the buffer verbatim when
exactly one visible layer sits at opacity 255. All compositing is integer
math — no floating point — so results are bit-identical across platforms.
| Area | Phase 0 | Added in Phase 1 |
|---|---|---|
| Canvas/palette | canvas.new, palette.set |
color.set, color.pick |
| Layers | — | layer.add, layer.select, layer.remove, layer.rename, layer.move, layer.visible, layer.opacity |
| Selection | — | select.rect, select.clear |
| Drawing | pixel.set, line.draw |
brush.stroke, rect.draw, fill.bucket |
| Import | — | image.import |
Three design decisions inside that table are worth stating plainly:
- There is no eraser command. Pixel writes replace rather than blend, so
brush.stroke … color="#00000000"is the eraser. A separate command would have been the same code under a different name. - Every pixel write goes through one of two helpers.
write_strict(used bypixel.setandline.draw) still errors off-canvas, preserving Phase 0 behaviour;write_lenient(used by the area commands) clips silently, because a size-5 brush dragged along the border must not abort the stroke. Both honour the selection. - The eyedropper reads the composite, not the active layer — it samples what the user can see, which is the only defensible semantics.
Engine.commands: Vec<Command> became Engine.steps: Vec<Vec<Command>>.
One entry is one Undo. begin_group/end_group bracket a gesture;
history() flattens the steps, so grouping never reaches the file
format — a saved script still contains every individual command, asserted
by grouped_history_still_round_trips_through_the_parser. Empty groups and
groups whose every command failed leave no phantom undo step.
crates/core/src/project.rs defines a text container whose first
non-comment line is pixelcad.project version=1, followed by the command
log. ROADMAP.md says ".pxc archive"; this is a documented deviation,
argued at length in the module header and summarised in Section 6.
The safety property is structural, not incidental: parse_project completes
before a single command executes, and open_project builds a fresh
Engine that the caller only receives on total success. A corrupt or
future-version file therefore cannot half-apply. Controller::open_project_file
mirrors this — it swaps its engine only after the new one is fully built, so
a failed Open in the GUI leaves the user's work byte-identical.
Controller gained Tool, brush_size, and a third drag mode:
Anchored, used by line/rectangle/marquee, which commits nothing until
the pointer is released. The shortcut table is handle_shortcut() in
main.rs — plain Rust, unit-testable, and it collapses Ctrl (Linux/Windows)
and Cmd (macOS) into a single accel boolean that the .slint file
computes. The .slint file gained a tool column, a brush-size stepper, a
layer panel (displayed top-first, index-converted in Rust), a hex swatch
editor, and Open/Save-project controls.
The view/document boundary was made explicit and is enforced: pan, zoom
and which swatch is highlighted are view state and are deliberately not
recorded as commands; the active colour, the selection and the active layer
are document state and are recorded. pan_moves_the_view_without_touching_the_document
asserts the former; the save-script tests assert the latter.
No Mode B pivots occurred. Every task reached passing verification
within at most one corrective iteration. The three corrections were: a raw
string literal needing r## because the embedded text contained "#
(the same trap Phase 0 hit); a Slint percentage-to-length conversion that is
only legal on size properties, not on x; and one over-strict assertion in
a test I had just written. None required an approach change.
Decisions taken, with reasoning:
- I2 resolved:
Profiles/andSkills/are now versioned. Phase 0 left this open and applied a fallback. It is now settled. Reasoning:ROADMAP.md's Session Protocol instructs every future session to readProfiles/Planner/profile.mdandProfiles/Autopilot/profile.mdby path; if those files are not in the repository, every result document's "Reproduce From Clean Clone" section is false. The content is 24 KB of plain markdown with no secrets./Agentsand/cacheremain ignored. The reasoning is written into.gitignoreitself so it is not lost. - Layers led; the project format followed. The alternative ordering would have forced a format-version bump inside the same phase.
- The project file is a text container, not a zip. Four reasons: it
keeps
pixelcad-corefree of any dependency butthiserror; a zip writer's timestamps and compression settings are exactly the kind of thing that quietly breaks byte-determinism; the format stays diffable in git; and it makes "open a project" literally "replay its commands," which is the project's first invariant rather than a separate loader that could drift from it. If binary payload size ever forces a change, the version header is already there to carry it. - Base64 was hand-written rather than pulled in. ~100 lines against the "core depends on nothing" invariant.
- Undo "polish" was interpreted as gesture grouping, not memory
optimisation. Grouping is what a user perceives as undo quality;
snapshot memory is a scaling concern and is in
BACKLOG.md.
Process deviations, stated plainly rather than buried:
PLAN.mdwas self-locked. The Planner profile and the session brief both require the maintainer to review a drafted plan and setStatus: LOCKED, and the brief specifically said to stop and wait. The maintainer's instruction for this session was explicit blanket pre-approval ("complete it with no need of my approval; default to autoapprove"), which I treated as delegating both open questions and the lock. This is a real deviation from the written protocol, not a technicality. A future session should not read it as precedent.- Tasks 2 and 3 were committed together. Task 2 alone deletes
Document::pixels()and cannot compile the workspace by construction. Committing a non-building tree would have been a worse violation than merging two task boundaries. Both tasks' Done-when criteria were verified independently before the single commit.
Blunt, as required. What Phase 1 explicitly still does not do:
- No transforms, no copy/paste, no moving a selection's contents. A marquee currently only clips drawing. Users will expect to be able to move what they selected, and they cannot. This is the single most likely source of "but it doesn't work" feedback.
- No non-rectangular selection — no lasso, no magic wand, no select-by-colour.
- No layer blend modes beyond normal alpha-over, no layer groups, no
masks, no clipping layers, and no way to reorder layers from the GUI
(the
layer.movecommand exists and is tested, but nothing in the UI emits it — drag-to-reorder was not built). - No text tool, no gradients, no dithering, no brush shapes other than a square, no anti-aliasing (the last is a deliberate design stance, not a gap).
- Undo history is still unbounded full-document snapshots. One snapshot per undo step is better than Phase 0's one per command, but a 1024×1024 canvas with 200 steps still costs roughly 800 MB. This will become a real problem before it becomes an annoying one.
image.importstores uncompressed base64 RGBA. A 512×512 import adds ~1.4 MB of text to the project file. It is correct and deterministic, and it is going to look absurd on a large import.- The
.pxcproj"archive" is a single text file. If a future phase needs genuinely binary assets, the container has to grow, and the version header is the mechanism for that. - No file dialogs. Open and Save take a path typed into a text field. This is obviously not shippable UX; it was out of scope and stayed out.
- No canvas resize, no crop, no new-document dialog. The GUI always starts at 64×64, and the only way to change that is to open a project with different dimensions.
- No automated visual-regression testing of the GUI. The 20 headless GUI tests prove the logic behind the real Slint dispatch pipeline is correct, but nothing compares rendered images. Unchanged from Phase 0.
- No CI, no Linux or Windows validation, no release binaries. Deferred
in
ROADMAP.mduntil a reference machine exists. Controller::save_scriptandsave_projectare separate buttons. Two save buttons in a toolbar is a UX smell; it exists because the plain.pxcscript is genuinely useful for scripting and I did not want to hide it.- The blueprint sample was verified by an ASCII-luminance dump, not by a human looking at a rendered image at full fidelity. It is legible and correct at that resolution, but a designer might reasonably disagree about its quality as a drawing.
What the next Planner must read, in order:
- This file in full — especially Sections 5 (what changed), 6 (the self-lock deviation) and 7 (limitations).
Plans/archive/PLAN-phase1.md— the scope boundary that was honoured.PROGRESS.md— the task-by-task record and the reasoning behind each deviation.ROADMAP.md— the Phase 2 description, the Session Protocol, and the Invariants.BACKLOG.md— 7 open items, all carried forward.README.md— the command grammar and both file formats as actually implemented. This is now the authoritative grammar reference.
Backlog items carried forward (full text in BACKLOG.md): unbounded
undo memory; uncompressed image.import payloads; no transforms or
copy/paste; no non-rectangular selection; no blend modes, groups or masks;
no GUI visual-regression testing; no CI or Linux/Windows validation.
New items a Phase 2 Planner should consider adding: no file dialogs; no
canvas resize/crop/new-document; no GUI affordance for layer.move.
Recommended first task of Phase 2: Phase 2 is "Concept System" — vector shapes, guides, snapping, grid/symmetry, annotations, command palette, textual command editor, macros, local automation API. The lowest-risk, highest-leverage first task is the command palette plus the textual command editor, for three reasons: (a) the entire command vocabulary, its parser, its serializer and its error reporting already exist and are exhaustively tested, so this is a UI surface over proven machinery rather than new semantics; (b) it immediately makes every Phase 1 capability reachable without new buttons, which relieves the toolbar pressure noted in Section 7; and (c) it is the natural substrate for macros and the automation API later in the same phase, so building it first means the later tasks extend it instead of duplicating it. Vector shapes should not lead — they introduce a second geometry model alongside the pixel document and will need the same care the layer reshape got, which is cheaper to do once the palette/editor surface exists to drive them.
One structural warning for Phase 2: EngineState is cloned in full for
every undo step. Adding vector-shape state to it multiplies the snapshot
cost by whatever that state weighs. Either keep vector data out of the
snapshot path, or take the BACKLOG.md undo-memory item as a
pre-requisite.
Toolchain prerequisites (exact versions used this session):
- Rust/Cargo 1.95.0 (
rustc 1.95.0 (59807616e 2026-04-14), Homebrew, macOS ARM64) - Git 2.54.0
- A real display for the GUI step only. Building, testing (including all 20 headless GUI tests) and the entire CLI work headlessly.
git clone <this-repository> pixelcad
cd pixelcad
# Build (the first slint compile is slow: budget several minutes)
cargo build --workspace
# Full suite — expect 170 passed, 0 failed
cargo test --workspace
# Render both samples
cargo run -p pixelcad-cli -- run docs/samples/ship.pxc --out ship.png
cargo run -p pixelcad-cli -- run docs/samples/blueprint.pxcproj --out blueprint.png
shasum -a 256 ship.png blueprint.png
# bde008684f8384379e33f514d15d4ca4c1aed97798d52a08bbb1cccd6f781232 ship.png
# abdf81edb8e8c52f7aba5c7977bf66bc5bc622eecac54456e099120b21d08f1a blueprint.png
rm ship.png blueprint.png
# Launch the GUI (requires a display)
cargo run -p pixelcad-appNo environment variables, secrets, network access or external services are
required for any of the above. Everything in this phase is local and
offline. Note that Profiles/ and Skills/ are now part of the clone, so
a fresh agent can follow the documented session protocol without any
out-of-band files — which was the whole point of resolving I2.