diff --git a/crates/op-pen-loader/src/effects.rs b/crates/op-pen-loader/src/effects.rs index 871858f81..3db58a546 100644 --- a/crates/op-pen-loader/src/effects.rs +++ b/crates/op-pen-loader/src/effects.rs @@ -34,22 +34,29 @@ pub fn effects_to_payload(effects: &[Effect]) -> Vec { /// Rebuild a node's `effects` from payload form. pub fn effects_from_payload(payload: Vec) -> Vec { - payload - .into_iter() - .map(|s| { - Effect::DropShadow(DropShadow { - offset_x: s.offset_x, - offset_y: s.offset_y, - blur: s.blur, - color: openpencil_shell_core::Color { - r: s.color[0], - g: s.color[1], - b: s.color[2], - a: s.color[3], - }, - }) - }) - .collect() + payload.iter().map(shadow_payload_to_effect).collect() +} + +/// Borrowing variant of [`effects_from_payload`] — rebuilds `effects` +/// from a payload slice without consuming it. Used by the +/// `LayoutScene` builder, which reads `NodePayload.effects` by +/// reference. +pub fn effects_from_payload_ref(payload: &[ShadowPayload]) -> Vec { + payload.iter().map(shadow_payload_to_effect).collect() +} + +fn shadow_payload_to_effect(s: &ShadowPayload) -> Effect { + Effect::DropShadow(DropShadow { + offset_x: s.offset_x, + offset_y: s.offset_y, + blur: s.blur, + color: openpencil_shell_core::Color { + r: s.color[0], + g: s.color[1], + b: s.color[2], + a: s.color[3], + }, + }) } /// Per-variant accessor for a canonical node's `effects` list. diff --git a/crates/op-pen-loader/src/layout_scene.rs b/crates/op-pen-loader/src/layout_scene.rs index 3fbea2552..047229456 100644 --- a/crates/op-pen-loader/src/layout_scene.rs +++ b/crates/op-pen-loader/src/layout_scene.rs @@ -1,122 +1,176 @@ //! `EditorState` → [`LayoutScene`] builder. //! -//! Produces the paint-only, layout-resolved render scene that a -//! future `CanvasViewport` painter walks instead of the editor's -//! `Document`. +//! Produces the paint-only, layout-resolved render scene that the +//! `CanvasViewport` painter walks. //! //! The flex layout pass is NOT re-implemented here. `EditorState.doc` -//! is a `PenDocument`; [`pen_document_to_document`] already runs each -//! page-root through jian-core's taffy `LayoutEngine` + -//! `jian_skia::SkiaMeasure` (see `adapter.rs`) and bakes the resolved -//! absolute AABBs into a shell-core `Document`. This builder reuses -//! that exact resolved `Document` and re-shapes its `Node` tree into -//! [`SceneNode`]s, dropping all editor state (selection / chat / -//! history / ui) and resolving variable `$ref` fills against the -//! editor's variables + active theme. +//! is a `PenDocument`; [`pen_document_to_payload`] runs each page-root +//! through jian-core's taffy `LayoutEngine` + `jian_skia::SkiaMeasure` +//! (see `adapter.rs`) and bakes the resolved absolute AABBs — plus +//! every paint field — into a layout-resolved [`DocPayload`]. This +//! builder reuses that resolved payload and re-shapes its `NodePayload` +//! tree into [`SceneNode`]s, dropping all editor state (selection / +//! chat / history / ui) and resolving variable `$ref` fills against +//! the editor's variables + active theme. //! //! So the resolved geometry a `LayoutScene` carries is bit-identical -//! to what `pen_document_to_document` bakes today — there is one -//! layout pass and one set of resolved rects. +//! to what `pen_document_to_payload` bakes — there is one layout pass +//! and one set of resolved rects. The builder no longer routes through +//! the shell-core `Document` model: `DocPayload` already carries the +//! resolved geometry + paint fields, and `apply_payload`'s only +//! transforms on them are lossless format conversions (colour array → +//! struct, kind / fill-type string → enum). -use openpencil_shell_core::document::{Document, Node, VariableTable}; +use openpencil_shell_core::document::{NodeKind, VariableTable}; use openpencil_shell_core::layout_scene::{ LayoutScene, SceneFillType, SceneNode, SceneStroke, ScenePage, }; +use openpencil_shell_core::Color; use crate::editor_state_var_table; -use crate::payload::pen_document_to_document; +use crate::payload::{DocPayload, NodePayload, StrokePayload}; /// Build a paint-only [`LayoutScene`] from an editor state. /// /// Runs the same jian `LayoutEngine` + `SkiaMeasure` flex pass that -/// [`pen_document_to_document`] uses (by delegating to it), resolves -/// variable `$ref` fills / strokes against the editor's variables + -/// active theme (via [`editor_state_var_table`]), and re-shapes the -/// resolved node tree into a render scene that carries NO editor -/// state. +/// the canonical `.op` loader uses (via [`pen_document_to_payload`]), +/// resolves variable `$ref` fills / strokes against the editor's +/// variables + active theme (via [`editor_state_var_table`]), and +/// re-shapes the resolved node tree into a render scene that carries +/// NO editor state. pub fn editor_state_to_layout_scene(state: &op_editor_core::EditorState) -> LayoutScene { - // The resolved `Document` — flex layout already baked into AABBs. - let doc: Document = pen_document_to_document(&state.doc); + // The layout-resolved payload — flex layout already baked into + // every `NodePayload`'s AABB by jian-core's `LayoutEngine`. This + // is the reusable layout-resolution core; it never touches the + // shell-core `Document` model. + let payload: DocPayload = crate::adapter::pen_document_to_payload(&state.doc).payload; // Variables + active theme + the `fill_refs` / `stroke_refs` - // caches the editor holds. `pen_document_to_document` returns a - // `Document` whose `var_table` only carries the persisted - // definitions; the transient `fill_refs` / `stroke_refs` live on - // `EditorState.ui` — so resolve `$ref`s against the editor-state - // table, which folds both halves together. + // caches the editor holds. `editor_state_var_table` folds the + // persisted definitions and the transient `EditorState.ui` + // selection / caches together. let var_table: VariableTable = editor_state_var_table(state); LayoutScene { - pages: doc + pages: payload .pages .iter() .map(|page| ScenePage { - id: page.id.as_str().to_string(), + id: page.id.clone(), name: page.name.clone(), children: page .children .iter() - .map(|n| node_to_scene(n, &var_table)) + .map(|n| node_payload_to_scene(n, &var_table)) .collect(), }) .collect(), - active_page_index: doc.active_page_index, + // The loader always opens on page 0 (`pen_document_to_payload` + // hardcodes `active_page_index: 0`, then `apply_payload` clamps + // it into range — same result for a fresh load). + active_page_index: payload + .active_page_index + .min(payload.pages.len().saturating_sub(1)), } } -/// Convert one resolved shell-core [`Node`] into a [`SceneNode`]. +/// Convert one resolved [`NodePayload`] into a [`SceneNode`]. /// -/// Geometry is copied straight through — `pen_document_to_document` +/// Geometry is copied straight through — `pen_document_to_payload` /// already resolved it. Variable `$ref` fills / strokes are resolved /// here so the scene carries only concrete colours; a registered ref /// wins over the node's authored colour, mirroring the canvas /// painter's `var_table.fill_for(id).or(node.fill)`. -fn node_to_scene(node: &Node, var_table: &VariableTable) -> SceneNode { +fn node_payload_to_scene(node: &NodePayload, var_table: &VariableTable) -> SceneNode { + use openpencil_shell_core::{Point2D, Rect}; + let node_id = openpencil_shell_core::document::NodeId::new(node.id.clone()); SceneNode { - id: node.id.as_str().to_string(), - kind: node.kind.clone(), - bounds: node.bounds, + id: node.id.clone(), + kind: str_to_kind(&node.kind), + bounds: Rect { + origin: Point2D::new(node.x, node.y), + size: Point2D::new(node.w, node.h), + }, rotation: node.rotation, corner_radius: node.corner_radius, // Paint-time `$ref` resolution: a registered fill ref wins, // else the node's own fill. Same precedence as the canvas // painter's `node_fill` helper. - fill: var_table.fill_for(&node.id).or(node.fill), - fill_type: fill_type_to_scene(node.fill_type), - stroke: node.stroke.map(|s| SceneStroke { - // Stroke `$ref` resolution parallels the fill path. - color: var_table - .stroke_color_for(&node.id) - .unwrap_or(s.color), - width: s.width, - }), + fill: var_table + .fill_for(&node_id) + .or_else(|| node.fill.map(array_to_color)), + fill_type: str_to_scene_fill_type(&node.fill_type), + stroke: node.stroke.as_ref().map(|s| scene_stroke(s, &node_id, var_table)), text: node.text.clone(), font_size: node.font_size, font_weight: node.font_weight, text_wrap: node.text_wrap, - points: node.points.clone(), - effects: node.effects.clone(), + points: node + .points + .iter() + .map(|p| Point2D::new(p[0], p[1])) + .collect(), + effects: crate::effects::effects_from_payload_ref(&node.effects), hidden: node.hidden, locked: node.locked, children: node .children .iter() - .map(|c| node_to_scene(c, var_table)) + .map(|c| node_payload_to_scene(c, var_table)) .collect(), } } -/// Map shell-core's editor-model `FillType` onto the scene's own -/// `SceneFillType`. A dedicated enum keeps `LayoutScene` from -/// re-exporting an editor-model type that may diverge. -fn fill_type_to_scene( - ft: openpencil_shell_core::document::FillType, -) -> SceneFillType { - use openpencil_shell_core::document::FillType; - match ft { - FillType::Solid => SceneFillType::Solid, - FillType::LinearGradient => SceneFillType::LinearGradient, - FillType::RadialGradient => SceneFillType::RadialGradient, - FillType::Image => SceneFillType::Image, +/// Resolve a payload stroke into a scene stroke. The `$ref` stroke +/// resolution parallels the fill path. +fn scene_stroke( + s: &StrokePayload, + node_id: &openpencil_shell_core::document::NodeId, + var_table: &VariableTable, +) -> SceneStroke { + SceneStroke { + color: var_table + .stroke_color_for(node_id) + .unwrap_or_else(|| array_to_color(s.color)), + width: s.width, + } +} + +/// `[r, g, b, a]` payload colour → shell-core `Color`. Lossless; +/// the same conversion `apply_payload` runs on the `Document` path. +fn array_to_color(a: [f32; 4]) -> Color { + Color { + r: a[0], + g: a[1], + b: a[2], + a: a[3], + } +} + +/// `NodePayload.kind` string → shell-core `NodeKind`. Mirrors +/// `payload::str_to_kind` so the scene's per-kind paint dispatch +/// matches the `Document` path exactly. +fn str_to_kind(s: &str) -> NodeKind { + match s { + "frame" => NodeKind::Frame, + "group" => NodeKind::Group, + "rect" => NodeKind::Rect, + "ellipse" => NodeKind::Ellipse, + "polygon" => NodeKind::Polygon, + "line" => NodeKind::Line, + "text" => NodeKind::Text, + "path" => NodeKind::Path, + other => NodeKind::Other(other.to_string()), + } +} + +/// `NodePayload.fill_type` string → scene `SceneFillType`. Mirrors +/// `payload::str_to_fill_type` followed by `fill_type_to_scene`. +fn str_to_scene_fill_type(s: &str) -> SceneFillType { + match s { + "linear" => SceneFillType::LinearGradient, + "radial" => SceneFillType::RadialGradient, + "image" => SceneFillType::Image, + _ => SceneFillType::Solid, } } diff --git a/crates/op-pen-loader/src/lib.rs b/crates/op-pen-loader/src/lib.rs index c2e197612..913087e89 100644 --- a/crates/op-pen-loader/src/lib.rs +++ b/crates/op-pen-loader/src/lib.rs @@ -33,11 +33,13 @@ pub mod variables; /// into a shell-core `Document`. pub use payload::pen_document_to_document; -/// Rust-reorg step 1: build a paint-only, layout-resolved -/// `LayoutScene` from an `EditorState`. Reuses the same jian -/// `LayoutEngine` + `SkiaMeasure` flex pass as `pen_document_to_document` -/// and resolves variable `$ref` fills against the editor's -/// variables + active theme. +/// Build a paint-only, layout-resolved `LayoutScene` from an +/// `EditorState`. Reuses the same jian `LayoutEngine` + `SkiaMeasure` +/// flex pass as the canonical `.op` loader (via +/// [`pen_document_to_payload`]) and resolves variable `$ref` fills +/// against the editor's variables + active theme. Builds the scene +/// directly from the layout-resolved `DocPayload` — no intermediate +/// shell-core `Document`. pub use layout_scene::editor_state_to_layout_scene; // The `EditorState` → paint-`Document` type bridge. `op-editor-core`