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
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.
106 lines
8 KiB
Markdown
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.
|