Xref attach: placement preview, relative paths on the first save, no-match messages

The reference follows the cursor while its insertion point, scale or
rotation is asked (preview-only PScale/PX/PY/PZ/PRotate included); a
relative path chosen before the drawing had a file becomes relative on the
first save, and relative paths start from "./" at or below the drawing's
folder; -XREF name options report "No matching xref names found.".

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
ramox81andClaude Opus 5.5 committed 2026-09-23 15:34:06 +03:00
1 parent 6bd55bc32e
commit 498a97df0f
8 files changed
+204 -7

No files matched your search

+67 -3
View File
@@ -136,6 +136,13 @@ pub struct XAttachCommand {
/// Degrees.
rotation: f64,
rotation_fixed: bool,
/// Ghost of the reference's model space in its own coordinates.
preview: Vec<WireModel>,
/// Drawing units per reference unit; the INSERT's scale carries it.
unit: f64,
/// PScale / PX / PY / PZ / PRotate: preview-only values.
preview_scale: [f64; 3],
preview_rotation: f64,
}
const NONZERO: &str = "Value must be nonzero.";
@@ -154,6 +161,10 @@ impl XAttachCommand {
scale_fixed: placement.scale.is_some(),
rotation: placement.rotation.unwrap_or(0.0),
rotation_fixed: placement.rotation.is_some(),
preview: Vec::new(),
unit: 1.0,
preview_scale: [1.0; 3],
preview_rotation: 0.0,
};
if command.point.is_some() {
command.step = command.next_step().unwrap_or(Step::Rotation);
@@ -175,6 +186,44 @@ impl XAttachCommand {
)
}
/// Show `wires` (the reference's model space) while placing it.
pub fn with_preview(mut self, wires: Vec<WireModel>, unit: f64) -> Self {
self.preview = wires;
self.unit = if unit.is_finite() && unit != 0.0 { unit } else { 1.0 };
self
}
/// Placement shown for the cursor at `cursor` (local): insertion point,
/// scale and rotation (degrees) as far as they are known at this step.
fn preview_placement(&self, cursor: DVec3) -> (DVec3, [f64; 3], f64) {
let rotation = if self.rotation_fixed {
self.rotation
} else {
self.preview_rotation
};
let base = self.point.unwrap_or(cursor);
match self.step {
Step::Insert | Step::Preset(_) => {
let scale = if self.scale_fixed { self.scale } else { self.preview_scale };
(cursor, scale, rotation)
}
Step::XScale | Step::Corner | Step::XyzX => {
let (dx, dy) = (cursor.x - base.x, cursor.y - base.y);
let scale = if dx.abs() > 1e-12 && dy.abs() > 1e-12 {
[dx, dy, dx.abs()]
} else {
[1.0; 3]
};
(base, scale, rotation)
}
Step::Rotation => {
let angle = (cursor.y - base.y).atan2(cursor.x - base.x).to_degrees();
(base, self.scale, angle)
}
_ => (base, self.scale, rotation),
}
}
/// The finished INSERT when nothing is left to ask (all values fixed in
/// the dialog).
pub fn immediate(&self) -> Option<EntityType> {
@@ -357,7 +406,11 @@ impl CadCommand for XAttachCommand {
}
// Preview-only values: the reference asks the real ones
// after the insertion point as usual.
Preset::PScale | Preset::PX | Preset::PY | Preset::PZ | Preset::PRotate => {}
Preset::PScale => self.preview_scale = [value; 3],
Preset::PX => self.preview_scale[0] = value,
Preset::PY => self.preview_scale[1] = value,
Preset::PZ => self.preview_scale[2] = value,
Preset::PRotate => self.preview_rotation = value,
}
self.step = Step::Insert;
Some(CmdResult::NeedPoint)
@@ -451,8 +504,19 @@ impl CadCommand for XAttachCommand {
}
}
fn on_preview_wires(&mut self, _pt: DVec3) -> Vec<WireModel> {
vec![]
fn on_preview_wires(&mut self, pt: DVec3) -> Vec<WireModel> {
if self.preview.is_empty() {
return Vec::new();
}
let (origin, scale, degrees) = self.preview_placement(self.plane.to_local(pt));
let (sin, cos) = degrees.to_radians().sin_cos();
let unit = self.unit;
let plane = self.plane;
let place = |p: DVec3| {
let (x, y, z) = (p.x * scale[0] * unit, p.y * scale[1] * unit, p.z * scale[2] * unit);
plane.to_world(origin + DVec3::new(x * cos - y * sin, x * sin + y * cos, z))
};
self.preview.iter().map(|wire| wire.mapped(place)).collect()
}
fn xattach_request(&self) -> Option<XrefAttachRequest> {
+18 -2
View File
@@ -36,6 +36,13 @@ impl XrefCommand {
}
}
/// Whether any reference name matches `pattern` (`*` / `?` wildcards).
fn matches(&self, pattern: &str) -> bool {
self.saved_paths
.iter()
.any(|(name, _)| crate::io::xref_model::wildcard_match(name, pattern))
}
fn choose(&mut self, token: &str) -> CmdResult {
self.step = match token {
"?" => Step::List,
@@ -118,6 +125,15 @@ impl CadCommand for XrefCommand {
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let value = text.trim().trim_matches('"').to_string();
let token = value.to_uppercase();
let names_step = matches!(
self.step,
Step::Names(_) | Step::PathTypeNames | Step::ShowNames
);
if names_step && !self.matches(&value) {
return Some(CmdResult::Measurement(
"No matching xref names found.".to_string(),
));
}
Some(match self.step.clone() {
Step::Option => self.choose(&token),
Step::List => CmdResult::Dispatch(format!("XREFLIST {value}")),
@@ -133,7 +149,7 @@ impl CadCommand for XrefCommand {
.find(|(name, _)| crate::io::xref_model::wildcard_match(name, &value))
.cloned()
else {
return Some(CmdResult::CancelWithMessage(
return Some(CmdResult::Measurement(
"No matching xref names found.".to_string(),
));
};
@@ -171,7 +187,7 @@ impl CadCommand for XrefCommand {
Step::Option => self.choose("A"),
Step::List => CmdResult::Dispatch("XREFLIST *".to_string()),
Step::NewPath { .. } => {
CmdResult::CancelWithMessage("Path unchanged.".to_string())
CmdResult::Measurement("Path unchanged.".to_string())
}
Step::PathTypeKind { .. } => CmdResult::NeedPoint,
_ => CmdResult::Cancel,