From 286bdf6426f955f3d1d1ef68d62f4553c3bb5619 Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Sun, 18 Oct 2026 19:07:10 +0100 Subject: [PATCH] fix(block-editor): manage parameter and basepoint graph items --- src/scene/block_authoring/edit.rs | 175 ++++++++++++++++++++++++++--- src/scene/block_authoring/graph.rs | 2 +- 2 files changed, 163 insertions(+), 14 deletions(-) diff --git a/src/scene/block_authoring/edit.rs b/src/scene/block_authoring/edit.rs index 490640db3..544820755 100644 --- a/src/scene/block_authoring/edit.rs +++ b/src/scene/block_authoring/edit.rs @@ -93,12 +93,31 @@ pub fn items(doc: &CadDocument, definition: Handle) -> Vec { p.parameter.definition_point, )); } - if let Some((handle, p)) = crate::scene::block_basepoint::parameter(doc, definition) { - result.push(single( + let basepoint = crate::scene::block_basepoint::parameter(doc, definition) + .map(|(handle, p)| (handle, p.parameter.definition_point)) + .or_else(|| { + let name = doc + .block_records + .iter() + .find(|record| record.handle == definition)? + .name + .as_str(); + Some(( + definition, + crate::scene::render_graph::block_base_point(doc, name), + )) + }); + if let Some((handle, point)) = basepoint { + let label = point + Vector3::new(0.25, 0.25, 0.0); + let mut graphic = Graphic::default(); + graphic.lines.push([point, label]); + graphic.labels.push((label, "Basepoint".into())); + result.push(Item { handle, - "Basepoint".into(), - p.parameter.definition_point, - )); + name: "Basepoint".into(), + graphic, + grips: vec![(Part::Point, point)], + }); } if let Some(p) = crate::scene::block_visibility::parameter(doc, definition) { result.push(single(p.handle, p.name.clone(), p.def_point)); @@ -178,13 +197,16 @@ pub fn move_part( if (at - old).length() < 1e-10 { return Ok(false); } - let graph = graph(doc, definition)?.ok_or("The authoring graph is unavailable.")?; let mut staged = doc.clone(); - if crate::scene::block_basepoint::parameter(doc, definition).is_some_and(|(h, _)| h == handle) { + if handle == definition + || crate::scene::block_basepoint::parameter(doc, definition) + .is_some_and(|(h, _)| h == handle) + { crate::scene::block_basepoint::create(&mut staged, definition, at)?; *doc = staged; return Ok(true); } + let graph = graph(doc, definition)?.ok_or("The authoring graph is unavailable.")?; let mut moved_grips = vec![]; match staged .objects @@ -287,12 +309,8 @@ pub fn move_part( p.definition_label_point = at } } - DynamicBlockData::MoveAction(p) => { - p.action.display_location = at - } - DynamicBlockData::ArrayAction(p) => { - p.action.display_location = at - } + DynamicBlockData::MoveAction(p) => p.action.display_location = at, + DynamicBlockData::ArrayAction(p) => p.action.display_location = at, DynamicBlockData::StretchAction(p) => p.action.display_location = at, DynamicBlockData::FlipAction(p) => p.action.display_location = at, DynamicBlockData::ScaleAction(p) | DynamicBlockData::RotateAction(p) => { @@ -412,6 +430,137 @@ pub fn move_part( *doc = staged; Ok(true) } + +/// Remove an authoring parameter together with the graph expressions that it +/// owns. Action bindings are reciprocal graph edges, so they are detached from +/// both ends before the parameter nodes are removed. +pub fn remove_parameter( + doc: &mut CadDocument, + definition: Handle, + handle: Handle, +) -> Result<(), &'static str> { + let item = items(doc, definition) + .into_iter() + .find(|item| item.handle == handle) + .ok_or("The selected authoring parameter is no longer available.")?; + if handle == definition && crate::scene::block_basepoint::parameter(doc, definition).is_none() { + return Err("The block basepoint cannot be deleted."); + } + if matches!( + doc.objects.get(&handle), + Some(ObjectType::DynamicBlock(object)) + if matches!(object.data, DynamicBlockData::MoveAction(_) | DynamicBlockData::ArrayAction(_) | DynamicBlockData::StretchAction(_) | DynamicBlockData::FlipAction(_) | DynamicBlockData::ScaleAction(_) | DynamicBlockData::RotateAction(_)) + ) || matches!( + doc.objects.get(&handle), + Some(ObjectType::BlockVisibilityParameter(_)) + ) { + return Err("Delete actions and visibility parameters from their dedicated editor."); + } + let graph_handle = graph(doc, definition)?.ok_or("The authoring graph is unavailable.")?; + let ObjectType::DynamicBlock(graph_object) = doc.objects.get(&graph_handle).unwrap() else { + unreachable!() + }; + let DynamicBlockData::EvaluationGraph(original) = &graph_object.data else { + unreachable!() + }; + let mut graph = original.clone(); + let parameter_nodes = graph + .nodes + .iter() + .filter(|node| node.expression == handle) + .map(|node| node.id) + .collect::>(); + if parameter_nodes.len() != 1 { + return Err("The selected parameter has an invalid graph node."); + } + let parameter_node = parameter_nodes[0]; + let mut owned_nodes = vec![parameter_node]; + // Native authoring groups have one grip edge (grip -> parameter) and up + // to two component edges (parameter -> grip location components). Classify + // endpoint objects as well as edge direction so unrelated graph edges + // cannot cause another parameter to be erased. + for edge in graph.edges.iter().filter(|edge| edge.next_id == 0) { + if edge.destination_node == parameter_node + && graph.nodes.iter().find(|node| node.id == edge.source_node).is_some_and(|node| { + matches!(doc.objects.get(&node.expression), Some(ObjectType::DynamicBlock(object)) if matches!(object.data, DynamicBlockData::LinearGrip(_) | DynamicBlockData::PolarGrip(_) | DynamicBlockData::XYGrip(_) | DynamicBlockData::RotationGrip(_) | DynamicBlockData::AlignmentGrip(_) | DynamicBlockData::FlipGrip(_) | DynamicBlockData::LookupGrip(_) | DynamicBlockData::VisibilityGrip(_))) + }) + { + owned_nodes.push(edge.source_node); + } else if edge.source_node == parameter_node + && graph.nodes.iter().find(|node| node.id == edge.destination_node).is_some_and(|node| { + matches!(doc.objects.get(&node.expression), Some(ObjectType::DynamicBlock(object)) if matches!(object.data, DynamicBlockData::GripLocationComponent(_))) + }) + { + owned_nodes.push(edge.destination_node); + } + } + owned_nodes.sort_unstable(); + owned_nodes.dedup(); + let mut edge_ids = graph + .edges + .iter() + .filter(|edge| { + owned_nodes.contains(&edge.source_node) || owned_nodes.contains(&edge.destination_node) + }) + .map(|edge| edge.id) + .collect::>(); + // Action relationships use reciprocal next_id=4 edges. Remove their + // inverse too, while leaving the action's graph node and object intact. + let selected_edges = edge_ids.iter().copied().collect::>(); + for selected in selected_edges { + let Some(edge) = graph.edges.iter().find(|edge| edge.id == selected) else { + continue; + }; + if edge.next_id == 4 { + let inverse = graph + .edges + .iter() + .find(|candidate| candidate.id == edge.outgoing_edges[4]); + let Some(inverse) = inverse.filter(|inverse| { + inverse.next_id == 4 + && inverse.outgoing_edges[4] == edge.id + && inverse.source_node == edge.destination_node + && inverse.destination_node == edge.source_node + }) else { + return Err("The parameter has an invalid action binding; it was not deleted."); + }; + edge_ids.insert(inverse.id); + } + } + for edge in edge_ids { + crate::scene::block_authoring::graph::remove(&mut graph, edge); + } + let removed_handles = owned_nodes + .iter() + .filter_map(|node_id| { + graph + .nodes + .iter() + .find(|node| node.id == *node_id) + .map(|node| node.expression) + }) + .collect::>(); + graph.nodes.retain(|node| !owned_nodes.contains(&node.id)); + if let Some(max) = graph.nodes.iter().map(|node| node.next_id).max() { + graph.first_node_id = max; + graph.first_node_id_copy = max; + } else { + graph.first_node_id = 0; + graph.first_node_id_copy = 0; + } + if let Some(ObjectType::DynamicBlock(object)) = doc.objects.get_mut(&graph_handle) { + object.data = DynamicBlockData::EvaluationGraph(graph); + } + for owned in removed_handles.into_iter().chain(std::iter::once(handle)) { + if owned != graph_handle { + doc.objects.remove(&owned); + } + } + doc.block_visibility_params.remove(&handle); + let _ = item; + Ok(()) +} + fn label_offset(p: &BlockTwoPointParameter, at: Vector3) -> f64 { let d = p.definition_end_point - p.definition_base_point; let n = Vector3::new(-d.y, d.x, 0.) * (1.0 / d.x.hypot(d.y)); diff --git a/src/scene/block_authoring/graph.rs b/src/scene/block_authoring/graph.rs index 1b8356c56..a6293c9ec 100644 --- a/src/scene/block_authoring/graph.rs +++ b/src/scene/block_authoring/graph.rs @@ -50,7 +50,7 @@ pub(crate) fn valid_lists(g: &BlockEvaluationGraph, node: i32) -> bool { true } -fn remove(g: &mut BlockEvaluationGraph, id: i32) { +pub(crate) fn remove(g: &mut BlockEvaluationGraph, id: i32) { let edge = g.edges.iter().find(|e| e.id == id).unwrap().clone(); let [pi, ni, po, no, _] = edge.outgoing_edges; for (neighbor, field, value) in [(pi, 1, ni), (ni, 0, pi), (po, 3, no), (no, 2, po)] {