perf(editor): patch the scene incrementally on web node drag
A web node drag called mark_dirty() per move, forcing a full serde doc->scene reconversion + taffy solve on every pixel. Mirror the native host: translate just the affected absolute scene nodes in place (editable + non-flex-flow, matching translate_selected) and defer the full rebuild to release. Adds a regression test that a locked node in the selection does not drift in the scene.
This commit is contained in:
parent
7b80dc51e7
commit
10daa64ffa
|
|
@ -73,7 +73,36 @@ impl WidgetHost {
|
|||
drag.last_screen_y = y;
|
||||
}
|
||||
if self.editor_state.translate_selected(dx as f64, dy as f64) {
|
||||
self.mark_dirty();
|
||||
// Incremental scene patch instead of a full serde reconversion per
|
||||
// moved pixel (mirrors the native host). A plain node drag only
|
||||
// moves absolute-positioned nodes, so translate just those scene
|
||||
// nodes in place and defer the full rebuild to release. Flex-flow
|
||||
// children are positioned by their parent, not absolute coords, so
|
||||
// they still need the full pass. Skip the fast path when a
|
||||
// reconversion is already pending (`editor_state_dirty`).
|
||||
if !self.editor_state_dirty {
|
||||
let children = self.editor_state.active_children();
|
||||
let ids: Vec<String> = self
|
||||
.editor_state
|
||||
.selection
|
||||
.set
|
||||
.iter()
|
||||
.filter(|id| {
|
||||
// Move exactly what `translate_selected` moved in the
|
||||
// document: editable nodes only (locked / hidden are
|
||||
// skipped there) and not flex-flow children (positioned
|
||||
// by their parent). Otherwise the scene would drift nodes
|
||||
// the doc never moved, then snap back on the release-time
|
||||
// reconversion.
|
||||
self.editor_state.is_editable(id)
|
||||
&& !op_editor_core::walkers::is_flow_child_of_flex(children, id)
|
||||
})
|
||||
.map(|id| id.as_str().to_string())
|
||||
.collect();
|
||||
let _ = self.layout_scene.translate_nodes(&ids, dx, dy);
|
||||
} else {
|
||||
self.mark_dirty();
|
||||
}
|
||||
} else {
|
||||
self.editor_state.editor_ui.active_guides.clear();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,15 @@ fn box_bounds(host: &WidgetHost) -> op_editor_core::DocRect {
|
|||
}
|
||||
}
|
||||
|
||||
fn scene_origin(host: &WidgetHost, id: &str) -> Point2D {
|
||||
host.layout_scene
|
||||
.active_page()
|
||||
.and_then(|page| page.find(id))
|
||||
.expect("node present in scene")
|
||||
.bounds
|
||||
.origin
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_tool_dragging_selected_node_moves_it_without_resizing() {
|
||||
let mut host = WidgetHost::new();
|
||||
|
|
@ -99,3 +108,51 @@ fn select_tool_dragging_flex_child_reorders_on_release_like_native() {
|
|||
|
||||
assert_eq!(child_order(&host, "stack"), vec!["b", "a", "c"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dragging_a_selection_with_a_locked_node_does_not_drift_it_in_the_scene() {
|
||||
let mut host = WidgetHost::new();
|
||||
let doc = jian_ops_schema::load_str(
|
||||
r##"{"version":"0.8.0","children":[
|
||||
{"type":"rectangle","id":"free","name":"Free","x":100,"y":100,"width":80,"height":60},
|
||||
{"type":"rectangle","id":"locked","name":"Locked","x":300,"y":100,"width":80,"height":60,"locked":true}
|
||||
]}"##,
|
||||
)
|
||||
.expect("fixture JSON parses")
|
||||
.value;
|
||||
host.editor_state = op_editor_core::EditorState::from_document(doc);
|
||||
host.editor_state.tool = Tool::Select;
|
||||
|
||||
// Press the free node to start a drag, then widen the selection to include
|
||||
// the locked node — the incremental fast path reads the live selection at
|
||||
// move time.
|
||||
let press = screen(&host, 140.0, 130.0);
|
||||
assert!(host.apply_press(press.x, press.y, VW, VH));
|
||||
host.editor_state.selection.set = vec![NodeId::new("free"), NodeId::new("locked")];
|
||||
|
||||
// Force-resolve the scene, then clear dirty so the move takes the
|
||||
// incremental path (a real frame would have painted + cleared the flag).
|
||||
host.editor_state_dirty = true;
|
||||
host.refresh_layout_scene();
|
||||
host.editor_state_dirty = false;
|
||||
|
||||
let free_before = scene_origin(&host, "free");
|
||||
let locked_before = scene_origin(&host, "locked");
|
||||
|
||||
let move_to = screen(&host, 200.0, 130.0); // +60 doc px in x
|
||||
assert!(host.apply_cursor_move(move_to.x, move_to.y));
|
||||
|
||||
let free_after = scene_origin(&host, "free");
|
||||
let locked_after = scene_origin(&host, "locked");
|
||||
|
||||
// The editable node tracks the drag in the scene...
|
||||
assert!(
|
||||
(free_after.x - free_before.x).abs() > 1.0,
|
||||
"editable node should move in the scene during the drag"
|
||||
);
|
||||
// ...the locked node, which `translate_selected` leaves untouched, must not
|
||||
// drift — otherwise it would jump and then snap back on the release-time
|
||||
// reconversion.
|
||||
assert_eq!(locked_after.x, locked_before.x, "locked node must not drift in x");
|
||||
assert_eq!(locked_after.y, locked_before.y, "locked node must not drift in y");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue