fix(loader): .op render-parity — fixed-height clip, status-bar box, path geometry
A clipped frame with an explicit numeric height now honours that height and clips overflow (layout_repair no longer lets the root grow to content), matching Pencil's fixed-height artboards. Status-bar shell strokes are suppressed in the scene path (width-independent) so they don't paint a stray black frame, and legacy .op path nodes remap geometry→d so authored paths aren't dropped.
This commit is contained in:
parent
0730e1ccce
commit
fb2ebb62ac
|
|
@ -569,6 +569,18 @@ fn height_can_expand_to_content(props: &ContainerProps) -> bool {
|
|||
}
|
||||
|
||||
fn height_can_expand_to_content_or_root(props: &ContainerProps, is_root: bool) -> bool {
|
||||
// A clipped, explicitly-sized frame must honour its declared height even at
|
||||
// the root: Pencil clips a fixed-height screen whose content overflows
|
||||
// rather than growing the frame to fit it. Without this guard the
|
||||
// `|| is_root` override grew a `height: 900, clip: true` screen to its
|
||||
// ~950px content height (off-by-50 vs Pencil's clipped baseline). The
|
||||
// non-root path already refuses via `height_can_expand_to_content`; mirror
|
||||
// it for the root instead of blanket-allowing expansion.
|
||||
if matches!(props.height.as_ref(), Some(SizingBehavior::Number(_)))
|
||||
&& props.clip_content == Some(true)
|
||||
{
|
||||
return height_can_expand_to_content(props);
|
||||
}
|
||||
height_can_expand_to_content(props) || is_root
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -188,11 +188,15 @@ fn non_clipped_fixed_height_layout_containers_expand_to_overflowing_children() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn clipped_vertical_root_expands_when_repaired_content_overflows() {
|
||||
// Top-level artboards still clip at paint time, but generated legacy roots
|
||||
// can receive taller repaired children after exact text/layout measurement.
|
||||
// The root bounds must grow with that repaired stack or the bottom nav is
|
||||
// clipped out of the exported artboard.
|
||||
fn clipped_vertical_root_with_fixed_height_clips_overflow_like_pencil() {
|
||||
// A root that declares an explicit (Number) height AND `clip:true` must
|
||||
// honour that height and clip the overflow — matching Pencil, which renders
|
||||
// a fixed-height artboard at exactly its declared size and clips content
|
||||
// that spills past it (verified against a Pencil `export_nodes` baseline:
|
||||
// a 900-tall clipped screen exported at 900, not its ~950px content). This
|
||||
// also makes the root CONSISTENT with the nested case below, where a
|
||||
// clipped fixed-height container already refuses to grow. `fit_content`
|
||||
// roots (no Number height) still expand via `height_can_follow_content`.
|
||||
let src = r##"{
|
||||
"version":"1.0.0","pages":[{"id":"p","name":"P","children":[
|
||||
{"type":"frame","id":"root","width":200,"height":50,"clip":true,
|
||||
|
|
@ -213,11 +217,17 @@ fn clipped_vertical_root_expands_when_repaired_content_overflows() {
|
|||
}"##;
|
||||
let scene = editor_state_to_layout_scene(&state_from(src));
|
||||
let root = &scene.pages[0].children[0];
|
||||
let after = root.find("after").expect("following child");
|
||||
|
||||
// Children still lay out at their repaired positions (overflowing past 50)…
|
||||
let after = root.find("after").expect("following child");
|
||||
assert_eq!(after.bounds.origin.y, 51.0);
|
||||
assert_eq!(root.bounds.size.y, 61.0);
|
||||
assert!(root.clip_content, "root must still clip after expanding");
|
||||
// …but the root keeps its declared 50px height and clips the overflow,
|
||||
// rather than growing to the ~61px content height.
|
||||
assert_eq!(root.bounds.size.y, 50.0);
|
||||
assert!(
|
||||
root.clip_content,
|
||||
"fixed-height clipped root must still clip"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -221,11 +221,20 @@ fn node_payload_to_scene(
|
|||
.shader
|
||||
.as_ref()
|
||||
.map(|s| payload_shader_to_scene(s, cum_opacity)),
|
||||
stroke: node.stroke.as_ref().map(|s| {
|
||||
let mut st = scene_stroke(s, &node_id, var_table);
|
||||
st.color = mul_alpha(st.color, cum_opacity);
|
||||
st
|
||||
}),
|
||||
stroke: if is_status_bar_shell_stroke(node) {
|
||||
// The scene path (editor canvas + render-shots) bypasses the
|
||||
// adapter's `legacy_payload_repair`, so an iPhone status-bar
|
||||
// shell ("Time"/"Levels") authored with a no-fill stroke would
|
||||
// paint a phantom black box around the clock / signal cluster —
|
||||
// invisible in Pencil. Drop the stroke the same way here.
|
||||
None
|
||||
} else {
|
||||
node.stroke.as_ref().map(|s| {
|
||||
let mut st = scene_stroke(s, &node_id, var_table);
|
||||
st.color = mul_alpha(st.color, cum_opacity);
|
||||
st
|
||||
})
|
||||
},
|
||||
text: node.text.clone(),
|
||||
text_runs: text_runs_to_scene(&node.text_runs, cum_opacity),
|
||||
font_family: node.font_family.clone(),
|
||||
|
|
@ -472,6 +481,25 @@ fn scene_stroke(
|
|||
}
|
||||
}
|
||||
|
||||
/// An iPhone status-bar layout shell ("Time" / "Levels") authored with a
|
||||
/// stroke but no fill — Pencil paints nothing, so the no-fill stroke (which
|
||||
/// resolves to opaque black) must not draw a phantom box. Mirrors
|
||||
/// `legacy_payload_repair::is_legacy_status_bar_shell` on the resolved
|
||||
/// `NodePayload` the scene path carries (no canonical `PenNode` here). Width
|
||||
/// is intentionally unconstrained — the shells are `fill_container`, so a
|
||||
/// wider status bar computes a larger width than a fixed-width sample.
|
||||
fn is_status_bar_shell_stroke(node: &NodePayload) -> bool {
|
||||
// A thin status-bar row: the authored 22 px computes to ~22-23 here
|
||||
// (stroke / rounding), so match a small range rather than an exact 22.
|
||||
// The no-fill + stroke + non-empty-children gates already exclude the
|
||||
// inner "Time" text glyph node (which is filled, strokeless, leaf).
|
||||
node.stroke.is_some()
|
||||
&& node.fill.is_none()
|
||||
&& matches!(node.name.as_str(), "Time" | "Levels")
|
||||
&& !node.children.is_empty()
|
||||
&& (18.0..=28.0).contains(&node.h)
|
||||
}
|
||||
|
||||
/// `[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 {
|
||||
|
|
|
|||
|
|
@ -45,7 +45,13 @@ fn is_legacy_status_bar_shell(frame: &FrameNode, payload: &NodePayload) -> bool
|
|||
return false;
|
||||
}
|
||||
|
||||
nearly(payload.w, 100.0) && nearly(payload.h, 22.0)
|
||||
// Height (~22) + the `Time`/`Levels` name + a no-fill stroke over
|
||||
// non-empty children pins this to an iPhone status-bar shell. The
|
||||
// width is NOT constrained: the shells are authored `fill_container`,
|
||||
// so a wider status bar (pencil-demo's 402-px bar) computes a much
|
||||
// larger width than the ~100 an earlier fixed-width sample had, and a
|
||||
// hard width match silently left those shells painting a black frame.
|
||||
nearly(payload.h, 22.0)
|
||||
}
|
||||
|
||||
fn is_legacy_battery_wrapper(frame: &FrameNode, payload: &NodePayload) -> bool {
|
||||
|
|
|
|||
|
|
@ -528,6 +528,7 @@ fn normalize_node_value(
|
|||
let is_image = kind == Some("image");
|
||||
let is_icon = kind == Some("icon");
|
||||
let is_prompt = kind == Some("prompt");
|
||||
let is_path = kind == Some("path");
|
||||
if is_image && !map.contains_key("src") {
|
||||
map.insert("src".to_string(), Value::String(String::new()));
|
||||
*changed = true;
|
||||
|
|
@ -554,6 +555,19 @@ fn normalize_node_value(
|
|||
}
|
||||
*changed = true;
|
||||
}
|
||||
// Pencil stores a `path` node's flattened outline as an SVG
|
||||
// string under `geometry`; jian's `PathNode` reads it from
|
||||
// `d`. Without this remap the geometry is dropped and the
|
||||
// path renders empty — status-bar icons, logos, and any
|
||||
// vector art authored as a path vanish.
|
||||
if is_path {
|
||||
if let Some(geom) = map.get("geometry").cloned() {
|
||||
if geom.is_string() && !map.contains_key("d") {
|
||||
map.insert("d".to_string(), geom);
|
||||
*changed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// A PenNode `fill` written as a bare string / single object /
|
||||
// legacy `type:"color"` array → canonical `Vec<PenFill>`.
|
||||
normalize_fill(map, changed);
|
||||
|
|
|
|||
Loading…
Reference in a new issue