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

8 KiB

Plan: 1:1 Port of dart-pdf path.dartnigig-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)

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:1pub mod path; pub use path::{PdfPath, PdfPathBuilder, PdfPathCursor, PdfFillRule, PdfStroke, PdfPathVerb};
  • NIGIG_PDF_FEATURE_PARITY_PLAN.md:746 — flip path_test | DONEpath_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 agreePdfPathBuilder(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 buildertakePath 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 listconst 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/decodecoords 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.