nigig-org/REVIEWS/PLAN_PATH_1TO1_PORT.md
andodeki ac8f8aa002
Some checks failed
p2p-intel / engine (push) Waiting to run
p2p-intel / notifications (push) Waiting to run
p2p-intel / coverage (push) Waiting to run
p2p-intel / makepad-app (push) Waiting to run
p2p-intel / exchange-tab (push) Waiting to run
Payment domain, storage, platform and UI / isolated-payment-tests (push) Waiting to run
Payment domain, storage, platform and UI / payment-ui-tests (push) Waiting to run
repo hygiene / hygiene (push) Has been cancelled
PDF engine / engine (push) Has been cancelled
PDF engine / makepad-integration (push) Has been cancelled
PDF engine / fuzz (push) Has been cancelled
nigig-build (CAD) / supply-chain (push) Has been cancelled
nigig-build (CAD) / cad-module (push) Has been cancelled
nigig-build (CAD) / full-crate-check (push) Has been cancelled
nigig-build (CAD) / cad-engine-coverage (push) Has been cancelled
nigig-build (CAD) / doc-workspace-coverage (push) Has been cancelled
nigig-build (CAD) / cad-widget-coverage (push) Has been cancelled
traffic / gates (push) Has been cancelled
traffic / nigig-traffic (push) Has been cancelled
traffic / supply-chain (push) Has been cancelled
doc-engine / engine (push) Has been cancelled
doc-engine / coverage (push) Has been cancelled
doc-engine / consumer (push) Has been cancelled
email / gates (push) Has been cancelled
email / email-domain (push) Has been cancelled
email / nigig-email (push) Has been cancelled
email / supply-chain (push) Has been cancelled
nigig-map / test (push) Has been cancelled
sms / gates (push) Has been cancelled
sms / robius-sms (push) Has been cancelled
sms / android (push) Has been cancelled
sms / nigig-sms (push) Has been cancelled
sms / supply-chain (push) Has been cancelled
spreadsheet / engine-coverage (push) Has been cancelled
spreadsheet / ui-controller-coverage (push) Has been cancelled
chore: sync full working tree to gitdab
Whole-tree sync: cad-core/cad-ui split sources, nigig-build
construction_frame migration, pdf port progress, mpesa/pay/uikit/doc
updates, workspace members/profiles/lock, CI workflows and reviews.
See individual file history for details.
2026-09-12 07:15:24 +03:00

106 lines
8 KiB
Markdown

# Plan: 1:1 Port of dart-pdf `path.dart` → `nigig-pdf` (Phase 7.5 — Path Object Parity)
**Date:** 2026-09-06
**Reference:** `dart-pdf/packages/pdf_graphics/lib/src/path.dart:1` (343 LOC), `dart-pdf/packages/pdf_graphics/test/path_test.dart:1` (73 LOC)
**Nigig baseline:** `crates/apps/pdf/pdf-graphics/src/recording.rs:11` (`RenderCommand` path ops), `crates/apps/pdf/pdf-graphics/src/lib.rs:1`
**Master plan:** `NIGIG_PDF_FEATURE_PARITY_PLAN.md:746` (`path_test | DONE` — overstated, see §1)
---
## 1. Objective
Bring `nigig-pdf` path object model to 1:1 parity with dart-pdf's `path.dart` without changing interpreter correctness. `RenderCommand` stays as the paint-level wire; `PdfPath` becomes the packed, allocation-conscious path payload that dense CAD streams need and that `PdfPathCursor` traverses without allocating.
Non-goal: path booleans, stroking, simplification (no dart equivalent).
## 2. Gap Audit (why `DONE` is partial)
| Aspect | dart-pdf `path.dart` | nigig `recording.rs` | Gap |
|---|---|---|---|
| **Path type** | `sealed PdfPathSegment` + `PdfPath` dual storage: `List<PdfPathSegment>` *or* packed `Uint8List verbs` + `Float32List coordinates` (wire f32 preserved) + `segmentCount` + `Expando` materialized cache `path.dart:38` | `RenderCommand::MoveTo/LineTo/CurveTo/ClosePath/Rectangle` — path implicit as command sequence | No `PdfPath` struct, no packed wire, no materialized cache |
| **Builder** | `PdfPathBuilder(verbCapacity=16, coordinateCapacity=48)` pools buffers, `_ensure` grows `>>1`, `takePath()` copies exact-size `Uint8List`/`Float64List` and reuses scratch (tens of k one-segment CAD paths) `path.dart:196` | `RecordingDevice::move_to` pushes `RenderCommand` directly `recording.rs:554` | No pooling, no exact-size copy, no reuse |
| **Cursor** | `PdfPathCursor` non-allocating `moveNext()` over packed or object storage `path.dart:106` | `for cmd in &commands` over `Vec<RenderCommand>` | No cursor, allocates per segment if `segments` needed |
| **Fill rule** | `enum PdfFillRule { nonzero, evenOdd }` `path.dart:302` | Two commands `FillWinding`/`FillEvenOdd` `recording.rs:21` — no enum | Missing enum (trivial) |
| **Stroke** | `class PdfStroke { width cap join miterLimit dashArray dashPhase }` page-space `path.dart:305` | Five commands `SetStrokeWidth/Cap/Join/MiterLimit/Dash` `recording.rs:27` | Not bundled |
Correctness is already 1:1 (ops round-trip). Performance + wire exactness is ~60%.
## 3. Scope
### 3.1 New file `crates/apps/pdf/pdf-graphics/src/path.rs` (~350 LOC)
```rust
pub enum PdfPathSegment { MoveTo{x:f64,y:f64}, LineTo{x,y}, CubicTo{x1,y1,x2,y2,x3,y3}, Close }
pub struct PdfPath { // dual storage, like dart
segments: Option<Vec<PdfPathSegment>>,
verbs: Option<Vec<u8>>,
coords: Option<Vec<f32>>, // f32 on wire, f64 in builder — mirrors dart Float32List vs Float64List
len: usize,
materialized: OnceCell<Vec<PdfPathSegment>>,
}
impl PdfPath {
pub fn from_segments(Vec<PdfPathSegment>) -> Self;
pub fn packed_float32(Vec<u8>, Vec<f32>, usize) -> Self; // wire exactness
pub fn segments(&self) -> &[PdfPathSegment]; // lazy materialize + cache
pub fn cursor(&self) -> PdfPathCursor;
pub fn segment_count(&self) -> usize;
pub fn is_empty(&self) -> bool;
}
pub enum PdfPathVerb { MoveTo, LineTo, CubicTo, Close }
pub struct PdfPathCursor { verb: PdfPathVerb, x1,y1,x2,y2,x3,y3, move_next() -> bool }
pub struct PdfPathBuilder { verbs: Vec<u8>, coords: Vec<f64>, verb_count, coord_count }
impl PdfPathBuilder {
pub fn new(verb_capacity: usize, coord_capacity: usize) -> Self; // defaults 16/48
pub fn move_to(&mut self, x:f64, y:f64);
pub fn line_to(&mut self, x:f64, y:f64);
pub fn cubic_to(&mut self, x1:f64,y1:f64,x2:f64,y2:f64,x3:f64,y3:f64);
pub fn close(&mut self);
pub fn add_segment(&mut self, PdfPathSegment);
pub fn take_path(&mut self) -> PdfPath; // exact-size copy + reuse scratch
pub fn clear(&mut self);
}
pub enum PdfFillRule { NonZero, EvenOdd }
pub struct PdfStroke { pub width:f64, pub cap:u32, pub join:u32, pub miter_limit:f64, pub dash_array:Vec<f64>, pub dash_phase:f64 }
```
Constants: `_moveTag=0, _lineTag=1, _cubicTag=2, _closeTag=3` `path.dart:184`, builder defaults `16/48` `path.dart:198`.
### 3.2 Modified files
* `crates/apps/pdf/pdf-graphics/src/recording.rs:11` — add `RenderCommand::Path(Rc<PdfPath>, PdfFillRule, Option<PdfStroke>)` alternative *or* keep `MoveTo` etc. for back-compat and add `PdfPath` as optional payload; update `format_command` + `wire.rs` codec to preserve f32 bit-exact (dart guarantee).
* `crates/apps/pdf/pdf-graphics/src/device.rs:6` `MakepadPdfDevice` — when a path is open, delegate `move_to` etc. to `PdfPathBuilder`, on `fill`/`stroke` consume via `take_path()`.
* `crates/apps/pdf/pdf-graphics/src/lib.rs:1``pub mod path; pub use path::{PdfPath, PdfPathBuilder, PdfPathCursor, PdfFillRule, PdfStroke, PdfPathVerb};`
* `NIGIG_PDF_FEATURE_PARITY_PLAN.md:746` — flip `path_test | DONE``path_test | Phase 7.5 | packed + cursor + builder pool + stroke/fill_rule, wire f32 exact` until landed, then back to `DONE`.
## 4. Test Port (dart → Rust)
| # | dart `pdf_graphics/test/path_test.dart` | Rust `crates/apps/pdf/pdf-graphics/src/path.rs` or `tests/path.rs` | Notes |
|---|---|---|---|
| T1 | `packed path cursor and compatibility segments agree``PdfPathBuilder(1,2) moveTo→lineTo→cubicTo→close`, `cursor.moveNext()` verbs/coords, `segments` lazy + `identical` cache `path_test.dart:5` | `packed_path_cursor_and_compatibility_segments_agree` | Verbatim port, checks `segment_count==4`, cursor `(verb,x1,y1…)`, `segments[0].is::<PdfMoveTo>()`, `ptr_eq(segments, segments)` via `Rc::ptr_eq` |
| T2 | `takePath copies immutable storage and reuses the builder``takePath` empties builder, second `takePath` independent `path_test.dart:47` | `take_path_copies_immutable_storage_and_reuses_builder` | Verbatim, checks `(segment_count, x1, y1)` for both paths |
| T3 | `ordinary const paths retain their supplied segment list``const PdfPath(segments)` returns `identical` list `path_test.dart:64` | `ordinary_const_paths_retain_supplied_segment_list` | `PdfPath::from_segments(Rc::new(vec![]))``ptr_eq` |
| T4 | *(new, perf)* dense CAD does not reallocate per path | `dense_cad_does_not_reallocate_per_path` | 10k `moveTo→lineTo→close` via one builder, assert `verbs.capacity()` amortized, `take_path` exact-size (`verbs.len()==verb_count`) |
| T5 | *(new, wire)* wire round-trip preserves f32 bit-exact | `wire_round_trip_preserves_float32_bit_exact` | `PdfPath` via `wire::encode`/`decode``coords` bitwise equal (dart wire guarantee) |
| Tool | `pdf_graphics/tool/dump_paths.dart`, `dump_bar_paths.dart` | `cargo run --bin dump_paths` (optional) | Perf dump for dense files, not required for green |
All 3 dart tests must pass as Rust equivalents before `746` flips to `DONE`.
## 5. Exit Criteria
* All 5 tests green: `cargo test -p nigig-pdf-graphics --lib path` + `cargo test -p nigig-pdf-graphics --test path` (if separate) + golden.
* `cargo test -p nigig-pdf-graphics` full suite still green (no interpreter regression).
* `NIGIG_PDF_FEATURE_PARITY_PLAN.md:746` updated to `DONE` with note `— packed + cursor + builder pool + stroke/fill_rule, wire f32 exact`.
* ADR `REVIEWS/adr/00xx-pdf-path-object-parity.md` records why `RenderCommand::Rectangle` stays as sugar (PDF `re` is rect) and why builder coords are `f64` but wire is `f32` (mirrors dart `Float64List` vs `Float32List`).
* No change to `pdf-makepad` rendering path except via `PdfPath` payload; existing goldens unchanged (wire is additive).
## 6. Risks & Mitigations
* **Wire breaking change** — mitigation: keep `MoveTo/LineTo` variants, add `Path` as new variant; old goldens unchanged; deprecate after one release.
* **Allocation win not measurable** — mitigation: T4 asserts amortized growth; bench with `cargo bench -p nigig-pdf-graphics path_dense`.
* **Const `identical` semantics** — Rust `const` not same as Dart `const`; use `Rc` + `ptr_eq` to emulate.
## 7. Estimate
* ~1 session, no fork needed, pure `pdf-graphics` crate.
* LOC: ~350 new + ~50 modified.
* Dependencies: none new.