From 56d73ceb215a6910ff4ab531633bed55e48e1674 Mon Sep 17 00:00:00 2001 From: Coast Date: Mon, 19 Oct 2026 00:29:13 -0600 Subject: [PATCH] Dynamic blocks: show a distance's grip only at an end an action follows, and the grips of parameters a visibility state lists (#1090) * Dynamic blocks: a distance's grip shows only at an end an action follows A linear parameter's grip at an end no action is attached to moves nothing, and AutoCAD doesn't show it on the reference: a stretch that follows only the end's change (EndXDelta, EndYDelta) leaves the base without a grip, and the other way round. A connection to the distance itself follows both ends, as does a distance measured from its midpoint; a parameter no action is attached to keeps the grips the block gives it. (A block saved with one grip has no base grip at all, which was already honoured.) Co-Authored-By: Claude Opus 5.5 * Dynamic blocks: a visibility state that lists a parameter shows its grips AutoCAD shows a parameter's grips in a visibility state that lists the parameter, though the state doesn't list the grips themselves (a label block's Label state lists Position1 and Distance2 but not their grips, and AutoCAD shows the square and the arrow). A grip now shows when the state lists it or its parameter. Co-Authored-By: Claude Opus 5.5 --------- Co-authored-by: Cody Vogt <18035288+codyvogt@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 --- crates/doc/src/dynblock/grips.rs | 44 +++++++++++++++++++++++++--- crates/engine/tests/dynamic_grips.rs | 41 +++++++++++++++++++++++++- 2 files changed, 80 insertions(+), 5 deletions(-) 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())]); +}