fix(agent): preserve compact avatar media during cleanup
This commit is contained in:
parent
29fc8cae95
commit
9dc2bafe40
|
|
@ -271,6 +271,37 @@ fn cleanup_deletes_only_empty_decorated_small_frame_stubs() {
|
|||
assert!(find_by_name(&root, "Spacer").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cleanup_preserves_image_filled_avatar_leaf_in_header() {
|
||||
let mut sink = insert_root(json!({
|
||||
"type":"frame","id":"root","name":"Root","width":390,"height":160,"layout":"vertical","children":[
|
||||
{"type":"frame","id":"top-bar","name":"Top Bar","width":"fill_container","height":52,
|
||||
"layout":"horizontal","justifyContent":"space_between","children":[
|
||||
{"type":"text","id":"title","name":"Title","content":"Hearth"},
|
||||
{"type":"frame","id":"avatar","name":"User Portrait","width":40,"height":40,
|
||||
"cornerRadius":20,"fill":[{"type":"image","url":"data:image/png;base64,AAAA","mode":"crop"}],"children":[]},
|
||||
{"type":"frame","id":"avatar-fallback","name":"Profile Avatar","role":"avatar","width":36,"height":36,
|
||||
"cornerRadius":18,"fill":[{"type":"solid","color":"#334155"}],"children":[]}
|
||||
]}
|
||||
]
|
||||
}));
|
||||
|
||||
run_cleanup(&mut sink);
|
||||
|
||||
let root = active_root_json(&sink);
|
||||
let top_bar = find_by_name(&root, "Top Bar").expect("top bar survives");
|
||||
assert_eq!(
|
||||
top_bar
|
||||
.get("children")
|
||||
.and_then(Value::as_array)
|
||||
.map(Vec::len),
|
||||
Some(3),
|
||||
"cleanup must not delete compact image-filled or semantic avatar leaves"
|
||||
);
|
||||
assert!(find_by_name(&root, "User Portrait").is_some());
|
||||
assert!(find_by_name(&root, "Profile Avatar").is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn geometry_diagnostics_reports_empty_decorated_frame_stub() {
|
||||
let sink = insert_root(json!({
|
||||
|
|
|
|||
|
|
@ -74,12 +74,53 @@ fn is_empty_decorated_stub(v: &Value, resolved_size: Option<(f64, f64)>) -> bool
|
|||
&& v.get("x").is_none()
|
||||
&& v.get("y").is_none()
|
||||
&& children(v).is_empty()
|
||||
// A frame whose paint is an image is already meaningful content.
|
||||
// Compact avatar/photo frames are commonly authored this way, with
|
||||
// no child image node, so treating them as empty stubs destroys the
|
||||
// asset before the later avatar-repair pass can inspect it.
|
||||
&& !is_meaningful_media_leaf(v)
|
||||
&& has_visible_paint(v)
|
||||
&& (padding_positive(v) || numeric(v, "cornerRadius").is_some_and(|r| r > 0.0))
|
||||
&& width.is_some_and(|w| w > 0.0 && w < 80.0)
|
||||
&& height.is_some_and(|h| h > 0.0 && h < 60.0)
|
||||
}
|
||||
|
||||
fn is_meaningful_media_leaf(v: &Value) -> bool {
|
||||
has_image_fill(v) || has_avatar_semantics(v)
|
||||
}
|
||||
|
||||
fn has_image_fill(v: &Value) -> bool {
|
||||
match v.get("fill") {
|
||||
Some(Value::Array(fills)) => fills.iter().any(is_image_paint),
|
||||
Some(fill) => is_image_paint(fill),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn has_avatar_semantics(v: &Value) -> bool {
|
||||
let role = v.get("role").and_then(Value::as_str).unwrap_or_default();
|
||||
if matches!(
|
||||
role.to_ascii_lowercase().as_str(),
|
||||
"avatar" | "user-avatar" | "profile-avatar" | "profile-photo"
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let name = v
|
||||
.get("name")
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_ascii_lowercase();
|
||||
name.contains("avatar") || name.contains("profile photo") || name.contains("user photo")
|
||||
}
|
||||
|
||||
fn is_image_paint(paint: &Value) -> bool {
|
||||
paint
|
||||
.get("type")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|kind| kind.eq_ignore_ascii_case("image"))
|
||||
}
|
||||
|
||||
fn resolved_sizes(state: &op_editor_core::EditorState) -> HashMap<String, (f64, f64)> {
|
||||
let scene = op_pen_loader::editor_state_to_layout_scene(state);
|
||||
let mut out = HashMap::new();
|
||||
|
|
|
|||
Loading…
Reference in a new issue