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 <noreply@anthropic.com>

* 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 <noreply@anthropic.com>

---------

Co-authored-by: Cody Vogt <18035288+codyvogt@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Coast
2026-10-19 02:29:13 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Cody Vogt
parent 691bdbe0b8
commit 56d73ceb21
2 changed files with 80 additions and 5 deletions
+40 -4
View File
@@ -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<String>,
}
/// 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<S: serde::Serializer>(p: &Vec2, s: S) -> Result<S::Ok, S::Error> {
[p.x, p.y].serialize(s)
}
@@ -112,8 +135,15 @@ pub fn grips(d: &Drawing, insert: Handle) -> Vec<DynGrip> {
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<i32, Handle> = 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<String, Vec<String>> =
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<DynGrip> {
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() });
}
+40 -1
View File
@@ -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())]);
}