From ba6aae07dc4dc3c3fbea6a4ae702a6400e488612 Mon Sep 17 00:00:00 2001 From: Kayshen-X Date: Sun, 21 Jun 2026 10:01:37 +0800 Subject: [PATCH] perf(canvas): Arc-share image src/url to make scene-cache O(1) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bump vendor/jian to the ImageSrc(Arc) change: ImageNode.src and ImageFillBody.url are now reference-counted, so the per-frame SceneBuildCache document clone + content-compare settle in O(1) for an unchanged multi-MB image source instead of walking the whole base64 payload. Completes the deeper half of the image-drag perf fix (the insert-time down-scale landed in a084628f). Consumer sites updated for the newtype: writes/constructs use .into(); sites assigning into a String field use .to_string(); reads are unchanged via Deref. Also fixes a stale gl-host test (remote_icon_insert_bakes_svg_d_as_path_node) that still assumed icon insert appends to the end — it now locates the baked path by icon_id and asserts it sits above the selection (the insert-above behavior). --- crates/op-editor-core/src/fills.rs | 4 +-- crates/op-editor-core/src/host_support.rs | 6 ++-- crates/op-editor-core/src/image_node_props.rs | 2 +- .../widgets/property_panel_image_assets.rs | 2 +- crates/op-figma/src/clipboard/tests.rs | 2 +- crates/op-figma/src/image_resolver.rs | 2 +- crates/op-figma/src/mappers.rs | 2 +- .../op-host-desktop/src/image_panel_host.rs | 2 +- .../src/image_search_session.rs | 6 ++-- .../src/image_search_session_tests.rs | 2 +- .../src/widget_host/image_panel_dispatch.rs | 2 +- .../src/widget_host/instance_panel_tests.rs | 34 +++++++++++++------ crates/op-host-web/src/dom_io.rs | 2 +- .../src/widget_host/image_panel_dispatch.rs | 2 +- crates/op-pen-loader/src/adapter.rs | 2 +- crates/op-pen-loader/src/style_payload.rs | 2 +- vendor/jian | 2 +- 17 files changed, 44 insertions(+), 32 deletions(-) diff --git a/crates/op-editor-core/src/fills.rs b/crates/op-editor-core/src/fills.rs index 81aa38db0..3d2677213 100644 --- a/crates/op-editor-core/src/fills.rs +++ b/crates/op-editor-core/src/fills.rs @@ -360,7 +360,7 @@ pub fn first_image_fill_summary(node: &PenNode) -> Option { Some(ImageFillSummary { mode: ImageFillMode::from_schema(body.mode.as_ref()), has_image: !trimmed_url.is_empty(), - image_url: (!trimmed_url.is_empty()).then(|| body.url.clone()), + image_url: (!trimmed_url.is_empty()).then(|| body.url.to_string()), exposure: body.exposure.unwrap_or(0.0), contrast: body.contrast.unwrap_or(0.0), saturation: body.saturation.unwrap_or(0.0), @@ -630,7 +630,7 @@ fn default_fill_of_type(kind: FillType, hex: &str) -> PenFill { blend_mode: None, }), FillType::Image => PenFill::Image(ImageFillBody { - url: String::new(), + url: "".into(), mode: None, original_size: None, transform: None, diff --git a/crates/op-editor-core/src/host_support.rs b/crates/op-editor-core/src/host_support.rs index d56b0eff8..1a4e3ea55 100644 --- a/crates/op-editor-core/src/host_support.rs +++ b/crates/op-editor-core/src/host_support.rs @@ -240,7 +240,7 @@ impl EditorState { y: Some(centre_y - H / 2.0), ..Default::default() }, - src: src.to_string(), + src: src.into(), object_fit: None, width: Some(SizingBehavior::Number(W)), height: Some(SizingBehavior::Number(H)), @@ -448,14 +448,14 @@ impl EditorState { return false; }; if let PenNode::Image(image) = node { - image.src = src.to_string(); + image.src = src.into(); return true; } let Some(fills) = crate::fills::node_fills_mut(node) else { return false; }; let body = PenFill::Image(ImageFillBody { - url: src.to_string(), + url: src.into(), mode: None, original_size: None, transform: None, diff --git a/crates/op-editor-core/src/image_node_props.rs b/crates/op-editor-core/src/image_node_props.rs index 7a73f348d..13f147f83 100644 --- a/crates/op-editor-core/src/image_node_props.rs +++ b/crates/op-editor-core/src/image_node_props.rs @@ -14,7 +14,7 @@ pub fn image_node_summary(node: &PenNode) -> Option { Some(ImageFillSummary { mode: ImageFillMode::from_image_node_schema(image.object_fit.as_ref()), has_image: !trimmed_url.is_empty(), - image_url: (!trimmed_url.is_empty()).then(|| image.src.clone()), + image_url: (!trimmed_url.is_empty()).then(|| image.src.to_string()), exposure: image.exposure.unwrap_or(0.0) as f32, contrast: image.contrast.unwrap_or(0.0) as f32, saturation: image.saturation.unwrap_or(0.0) as f32, diff --git a/crates/op-editor-ui/src/widgets/property_panel_image_assets.rs b/crates/op-editor-ui/src/widgets/property_panel_image_assets.rs index dbc19e445..8bafad798 100644 --- a/crates/op-editor-ui/src/widgets/property_panel_image_assets.rs +++ b/crates/op-editor-ui/src/widgets/property_panel_image_assets.rs @@ -81,7 +81,7 @@ pub fn image_panel_view(state: &EditorState, node: &PenNode) -> Option Option { }) .unwrap_or_default(); Some(PenFill::Image(ImageFillBody { - url, + url: url.into(), mode: Some(map_scale_mode(paint.get_str("imageScaleMode"))), original_size: normalize_original_size( paint.get_f64("originalImageWidth"), diff --git a/crates/op-host-desktop/src/image_panel_host.rs b/crates/op-host-desktop/src/image_panel_host.rs index ee55b1c1e..88d74278f 100644 --- a/crates/op-host-desktop/src/image_panel_host.rs +++ b/crates/op-host-desktop/src/image_panel_host.rs @@ -203,7 +203,7 @@ impl ImagePanelJobs { fn selected_image_src(host: &WidgetHostNative) -> Option<(String, String)> { match host.editor_state().selected_node() { - Some(PenNode::Image(image)) => Some((image.base.id.clone(), image.src.clone())), + Some(PenNode::Image(image)) => Some((image.base.id.clone(), image.src.to_string())), _ => None, } } diff --git a/crates/op-host-desktop/src/image_search_session.rs b/crates/op-host-desktop/src/image_search_session.rs index 2e2220530..4d65442af 100644 --- a/crates/op-host-desktop/src/image_search_session.rs +++ b/crates/op-host-desktop/src/image_search_session.rs @@ -598,12 +598,12 @@ pub(crate) fn apply_result(state: &mut EditorState, node_id: &NodeId, url: &str) if image.src == url { return false; } - image.src = url.to_string(); + image.src = url.into(); true } PenNode::Frame(frame) if is_unfilled_placeholder_frame => { frame.container.fill = Some(vec![PenFill::Image(ImageFillBody { - url: url.to_string(), + url: url.into(), mode: Some(ImageFillMode::Crop), original_size: None, transform: None, @@ -622,7 +622,7 @@ pub(crate) fn apply_result(state: &mut EditorState, node_id: &NodeId, url: &str) } PenNode::Rectangle(rect) if is_unfilled_placeholder_rectangle => { rect.container.fill = Some(vec![PenFill::Image(ImageFillBody { - url: url.to_string(), + url: url.into(), mode: Some(ImageFillMode::Crop), original_size: None, transform: None, diff --git a/crates/op-host-desktop/src/image_search_session_tests.rs b/crates/op-host-desktop/src/image_search_session_tests.rs index dc7d41d5c..f36f502a9 100644 --- a/crates/op-host-desktop/src/image_search_session_tests.rs +++ b/crates/op-host-desktop/src/image_search_session_tests.rs @@ -13,7 +13,7 @@ fn image_node(id: &str, src: &str, query: Option<&str>) -> PenNode { name: Some("Menu photo".into()), ..Default::default() }, - src: src.to_string(), + src: src.into(), object_fit: None, width: Some(SizingBehavior::Number(240.0)), height: Some(SizingBehavior::Number(160.0)), diff --git a/crates/op-host-native/src/widget_host/image_panel_dispatch.rs b/crates/op-host-native/src/widget_host/image_panel_dispatch.rs index acd9c9506..fb5bf4128 100644 --- a/crates/op-host-native/src/widget_host/image_panel_dispatch.rs +++ b/crates/op-host-native/src/widget_host/image_panel_dispatch.rs @@ -172,7 +172,7 @@ impl WidgetHostNative { if let Some(PenNode::Image(image)) = op_editor_core::walkers::find_node_mut(self.editor_state.active_children_mut(), &id) { - image.src = src.to_string(); + image.src = src.into(); } self.mark_editor_state_dirty(); } diff --git a/crates/op-host-native/src/widget_host/instance_panel_tests.rs b/crates/op-host-native/src/widget_host/instance_panel_tests.rs index 5a6b90da0..21d1a8bd7 100644 --- a/crates/op-host-native/src/widget_host/instance_panel_tests.rs +++ b/crates/op-host-native/src/widget_host/instance_panel_tests.rs @@ -228,15 +228,27 @@ fn remote_icon_insert_bakes_svg_d_as_path_node() { let children = host.editor_state().active_children(); assert_eq!(children.len(), before + 1, "insert landed"); - match children.last().expect("inserted node") { - PenNode::Path(p) => { - assert_eq!( - p.d.as_deref(), - Some("M3 9l9-7 9 7v11h-6v-7H9v7H3z"), - "the remote icon's d is baked (no fallback dot)" - ); - assert_eq!(p.icon_id.as_deref(), Some("mdi:zwxq-home")); - } - other => panic!("expected a Path node with baked d, got {}", other.id_str()), - } + // The icon inserts ABOVE the selected node (`inst1`), so it is no + // longer the last child — locate the baked path by its icon id. + let path_idx = children + .iter() + .position(|n| matches!(n, PenNode::Path(p) if p.icon_id.as_deref() == Some("mdi:zwxq-home"))) + .expect("baked remote icon path was inserted"); + let inst1_idx = children + .iter() + .position(|n| n.id_str() == "inst1") + .expect("selection still present"); + assert!( + path_idx < inst1_idx, + "the inserted icon sits above the selected node" + ); + let PenNode::Path(p) = &children[path_idx] else { + unreachable!("path_idx points at the matched Path"); + }; + assert_eq!( + p.d.as_deref(), + Some("M3 9l9-7 9 7v11h-6v-7H9v7H3z"), + "the remote icon's d is baked (no fallback dot)" + ); + assert_eq!(p.icon_id.as_deref(), Some("mdi:zwxq-home")); } diff --git a/crates/op-host-web/src/dom_io.rs b/crates/op-host-web/src/dom_io.rs index 4fab7e7c2..a5b76403f 100644 --- a/crates/op-host-web/src/dom_io.rs +++ b/crates/op-host-web/src/dom_io.rs @@ -658,7 +658,7 @@ fn relink_image(inner: &InnerRc) { if let Some(jian_ops_schema::node::PenNode::Image(image)) = op_editor_core::walkers::find_node_mut(state.active_children_mut(), &id) { - image.src = url; + image.src = url.into(); } state.editor_ui.image_panel.asset_check = None; b.host_mut().mark_editor_state_dirty(); diff --git a/crates/op-host-web/src/widget_host/image_panel_dispatch.rs b/crates/op-host-web/src/widget_host/image_panel_dispatch.rs index 6dafdd13c..3db09859c 100644 --- a/crates/op-host-web/src/widget_host/image_panel_dispatch.rs +++ b/crates/op-host-web/src/widget_host/image_panel_dispatch.rs @@ -156,7 +156,7 @@ impl WidgetHost { if let Some(PenNode::Image(image)) = op_editor_core::walkers::find_node_mut(self.editor_state.active_children_mut(), &id) { - image.src = src.to_string(); + image.src = src.into(); } self.mark_dirty(); } diff --git a/crates/op-pen-loader/src/adapter.rs b/crates/op-pen-loader/src/adapter.rs index a1328f2af..68023a5a9 100644 --- a/crates/op-pen-loader/src/adapter.rs +++ b/crates/op-pen-loader/src/adapter.rs @@ -789,7 +789,7 @@ fn image_to_payload(n: &ImageNode) -> NodePayload { // draw the bitmap. `fill` stays at a neutral grey so the // placeholder reads correctly when the bytes fail to decode // (corrupt url / unsupported codec). - p.image_src = Some(n.src.clone()); + p.image_src = Some(n.src.to_string()); p.image_fit = n.object_fit.as_ref().map(image_node_fit_to_payload); p.image_adjustments = image_node_adjustments(n); p.fill = Some([0.85, 0.86, 0.88, 1.0]); diff --git a/crates/op-pen-loader/src/style_payload.rs b/crates/op-pen-loader/src/style_payload.rs index 94319aea7..a4047376c 100644 --- a/crates/op-pen-loader/src/style_payload.rs +++ b/crates/op-pen-loader/src/style_payload.rs @@ -106,7 +106,7 @@ fn first_image_fill( None } else { Some(( - body.url.clone(), + body.url.to_string(), image_fill_mode_to_payload(body.mode.as_ref()), image_fill_adjustments(body), )) diff --git a/vendor/jian b/vendor/jian index d50a7157e..2623df522 160000 --- a/vendor/jian +++ b/vendor/jian @@ -1 +1 @@ -Subproject commit d50a7157ece8e1f880f2b64dd396540de7eec1f3 +Subproject commit 2623df52265bf124f08319691c57134d8a340a69