perf(canvas): Arc-share image src/url to make scene-cache O(1)
Bump vendor/jian to the ImageSrc(Arc<str>) 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).
This commit is contained in:
parent
a922bd2d0b
commit
ba6aae07dc
|
|
@ -360,7 +360,7 @@ pub fn first_image_fill_summary(node: &PenNode) -> Option<ImageFillSummary> {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ pub fn image_node_summary(node: &PenNode) -> Option<ImageFillSummary> {
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -81,7 +81,7 @@ pub fn image_panel_view(state: &EditorState, node: &PenNode) -> Option<ImagePane
|
|||
};
|
||||
let node_id = image.base.id.as_str().to_string();
|
||||
let name = image.base.name.clone().unwrap_or_default();
|
||||
let src = (!image.src.trim().is_empty()).then(|| image.src.clone());
|
||||
let src = (!image.src.trim().is_empty()).then(|| image.src.to_string());
|
||||
let warning = src
|
||||
.as_deref()
|
||||
.filter(|s| is_local_asset_path(s))
|
||||
|
|
|
|||
|
|
@ -122,7 +122,7 @@ fn parse_clipboard_html_styles_handles_background_color() {
|
|||
fn fix_unresolved_images_swaps_blob_image_for_placeholder_rect() {
|
||||
let mut nodes = vec![PenNode::Image(ImageNode {
|
||||
base: base(),
|
||||
src: "__blob:5".to_string(),
|
||||
src: "__blob:5".into(),
|
||||
object_fit: None,
|
||||
width: Some(SizingBehavior::Number(100.0)),
|
||||
height: Some(SizingBehavior::Number(80.0)),
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@ fn patch_fills(
|
|||
if let PenFill::Image(img) = fill {
|
||||
if img.url.starts_with("__blob:") || img.url.starts_with("__hash:") {
|
||||
if let Some(url) = resolve_ref(&img.url, data_urls, hash_urls) {
|
||||
img.url = url;
|
||||
img.url = url.into();
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ fn map_single_fill(paint: &FigValue) -> Option<PenFill> {
|
|||
})
|
||||
.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"),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)),
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -658,7 +658,7 @@ fn relink_image<C: RepaintContext + 'static>(inner: &InnerRc<C>) {
|
|||
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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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]);
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
))
|
||||
|
|
|
|||
2
vendor/jian
vendored
2
vendor/jian
vendored
|
|
@ -1 +1 @@
|
|||
Subproject commit d50a7157ece8e1f880f2b64dd396540de7eec1f3
|
||||
Subproject commit 2623df52265bf124f08319691c57134d8a340a69
|
||||
Loading…
Reference in a new issue