diff --git a/crates/doc/src/dynblock/grips.rs b/crates/doc/src/dynblock/grips.rs index f592bd66..301d9eca 100644 --- a/crates/doc/src/dynblock/grips.rs +++ b/crates/doc/src/dynblock/grips.rs @@ -10,7 +10,7 @@ use std::collections::{HashMap, HashSet}; use cadcraft_geom::{Vec2, Vec3}; use serde::Serialize; -use super::{ParameterKind, Value, custom_properties, dynamic_block, reference}; +use super::{DynamicBlock, ParameterKind, Value, custom_properties, dynamic_block, reference}; use crate::{Drawing, EntityKind, Handle}; /// Most grips listed for one reference. @@ -79,6 +79,29 @@ pub struct DynGrip { pub choices: Vec, } +/// Which ends of two-point parameter `node` the block's actions follow, as (base, end): a +/// connection to one point's change (BaseXDelta, EndYDelta, UpdatedEnd…) follows that end, one to +/// the value itself (a distance, an angle) follows both. None when no action is attached to the +/// parameter: its grips are all kept, as the block has them. +fn ends_used(m: &DynamicBlock, node: i32) -> Option<(bool, bool)> { + let mut used: Option<(bool, bool)> = None; + for (n, name) in m.actions.iter().flat_map(|a| a.connections.iter()) { + if *n != node { + continue; + } + let c = name.to_ascii_lowercase(); + let (base, end) = used.unwrap_or((false, false)); + used = Some(if c.starts_with("base") || c.starts_with("updatedbase") { + (true, end) + } else if c.starts_with("end") || c.starts_with("updatedend") { + (base, true) + } else { + (true, true) + }); + } + used +} + fn point(p: &Vec2, s: S) -> Result { [p.x, p.y].serialize(s) } @@ -112,8 +135,15 @@ pub fn grips(d: &Drawing, insert: Handle) -> Vec { break; } } + // A grip shows in the current visibility state when the state lists it, or lists its + // parameter (AutoCAD shows a listed parameter's grips though the grips aren't listed). + let param_handle: HashMap = m.parameters.iter().map(|p| (p.node, p.handle)).collect(); let shown = |node: i32, id: i32| { - id != 0 && (Some(node) == visibility_node || shown_in_state.as_ref().is_none_or(|a| objects.get(&id).is_some_and(|h| a.contains(h)))) + id != 0 + && (Some(node) == visibility_node + || shown_in_state + .as_ref() + .is_none_or(|a| objects.get(&id).is_some_and(|h| a.contains(h)) || param_handle.get(&node).is_some_and(|h| a.contains(h)))) }; let choices: HashMap> = custom_properties(d, insert).map(|(_, ps)| ps.into_iter().map(|p| (p.name.to_ascii_uppercase(), p.choices)).collect()).unwrap_or_default(); @@ -121,15 +151,21 @@ pub fn grips(d: &Drawing, insert: Handle) -> Vec { for p in &m.parameters { let grip = |i: usize| p.grips.get(i).copied().unwrap_or(0); match &p.kind { - ParameterKind::Linear { base, end, label, .. } => { + ParameterKind::Linear { base, end, label, from_midpoint, .. } => { let (b, e) = match r.values.get(&p.node) { Some(Value::Points { base, end }) => (*base, *end), _ => (*base, *end), }; let u = e - b; let u = if u.len() > 1e-12 { u * (1.0 / u.len()) } else { Vec3::new(1.0, 0.0, 0.0) }; + // An end no action follows has no grip (dragging it would change nothing); a + // distance from its midpoint moves both ends. + let used = match ends_used(&m, p.node) { + Some((base, end)) if !*from_midpoint => [base, end], + _ => [true, true], + }; for (i, part, at, way) in [(0, GripPart::Base, b, u * -1.0), (1, GripPart::End, e, u)] { - if shown(p.node, grip(i)) { + if used[i] && shown(p.node, grip(i)) { let (at, dir) = (world(at), toward(at, way)); out.push(DynGrip { node: p.node, part, shape: GripShape::Linear, at, dir, property: label.clone(), choices: Vec::new() }); } diff --git a/crates/engine/tests/dynamic_grips.rs b/crates/engine/tests/dynamic_grips.rs index 151ea2de..88977c0c 100644 --- a/crates/engine/tests/dynamic_grips.rs +++ b/crates/engine/tests/dynamic_grips.rs @@ -23,6 +23,11 @@ fn dxf(src: &str) -> String { /// A session with DOOR (Width's end grip 9 shown in the Open state, the visibility grip 10) and /// a reference to it at (100, 0). fn session() -> (Session, Handle) { + session_with(|src| src) +} + +/// As [`session`], with the fixture's text changed by `edit` first. +fn session_with(edit: impl Fn(String) -> String) -> (Session, Handle) { let grip = "0 BLOCKLINEARGRIP\n5 68\n330 61\n100 AcDbEvalExpr\n90 9\n98 33\n99 232\n100 AcDbBlockElement\n300 End Grip\n98 33\n99 232\n1071 0\n"; let src = include_str!("../../io/tests/data/dynblock_door.txt") .replace("\r\n", "\n") @@ -31,7 +36,7 @@ fn session() -> (Session, Handle) { .replace("91 5\n93 32\n95 6\n360 67", "91 5\n93 32\n95 6\n360 67\n92 -1\n92 -1\n92 -1\n92 -1\n91 6\n93 32\n95 9\n360 68") .replace("95 1\n333 63\n", "95 2\n333 63\n333 68\n") .replace("0 BLOCKFLIPGRIP", &format!("{grip}0 BLOCKFLIPGRIP")); - let d = cadcraft_io::read_dxf(dxf(&src).as_bytes()).unwrap(); + let d = cadcraft_io::read_dxf(dxf(&edit(src)).as_bytes()).unwrap(); let mut s = Session::empty(); s.open_drawing(d, "door", None); let r = s.execute("insert", &json!({ "name": "Door", "at": [100, 0] })).unwrap(); @@ -88,3 +93,37 @@ fn a_grip_drag_previews_the_reference_and_takes_a_point_or_a_distance() { assert!(s.start_dyn_grip(h, 1, GripPart::At).is_err()); assert!(s.start_dyn_grip(h, 2, GripPart::Base).is_err()); } + +/// Width with its base grip too (grip 8, shown in the Open state). +fn with_base_grip(src: String) -> String { + let base = "0 BLOCKLINEARGRIP\n5 69\n330 61\n100 AcDbEvalExpr\n90 8\n98 33\n99 232\n100 AcDbBlockElement\n300 Base Grip\n98 33\n99 232\n1071 0\n"; + src.replace("95 9\n360 68", "95 9\n360 68\n92 -1\n92 -1\n92 -1\n92 -1\n91 7\n93 32\n95 8\n360 69") + .replace("95 2\n333 63\n333 68\n", "95 3\n333 63\n333 68\n333 69\n") + .replace("0 BLOCKFLIPGRIP", &format!("{base}0 BLOCKFLIPGRIP")) +} + +fn parts(s: &mut Session, h: Handle) -> Vec<(i64, String)> { + let g = s.execute("blocks.reference.grips", &json!({ "handle": h.hex() })).unwrap(); + g.as_array().unwrap().iter().map(|g| (g["node"].as_i64().unwrap(), g["part"].as_str().unwrap().to_string())).collect() +} + +#[test] +fn an_end_no_action_follows_has_no_grip() { + // Width's stretch follows its end only: its base grip, though the block has one, moves + // nothing and isn't shown (as AutoCAD shows the reference). + let (mut s, h) = session_with(with_base_grip); + assert_eq!(parts(&mut s, h), [(1, "at".to_string()), (2, "end".to_string())]); + // A stretch that follows the base instead: the base grip, not the end's. + let (mut s, h) = session_with(|src| with_base_grip(src).replace("301 EndXDelta\n302 EndYDelta", "301 BaseXDelta\n302 BaseYDelta")); + assert_eq!(parts(&mut s, h), [(1, "at".to_string()), (2, "base".to_string())]); +} + +#[test] +fn a_state_that_lists_a_parameter_shows_its_grips() { + // Open lists Width but not its grip: the grip shows (as AutoCAD shows it); Closed lists + // neither, and the grip doesn't. + let (mut s, h) = session_with(|src| src.replace("95 2\n333 63\n333 68\n", "95 1\n333 63\n")); + assert_eq!(parts(&mut s, h), [(1, "at".to_string()), (2, "end".to_string())]); + s.execute("blocks.reference.set", &json!({ "handle": h.hex(), "name": "Visibility1", "value": "Closed" })).unwrap(); + assert_eq!(parts(&mut s, h), [(1, "at".to_string())]); +}