forked from 1b-agent/opencadstudio-upstream
feat(dimension): implement QDIM, fix DIMSPACE, honest DIMBREAK/DIMJOGLINE
Batch 4 of the dimension audit (command features). - QDIM now works: the relaunched QDIM_PLACE stage decomposes the selected geometry into endpoints, picks the axis from their spread (not Z), and builds a continuous chain of linear dimensions at the picked line. Was a no-op that errored with 'Unknown command'. (DIM-007/008/032) - DIMSPACE steps parallel dimensions along the in-plane perpendicular instead of Z (which never moved the dim line). (DIM-021) - DIMBREAK / DIMJOGLINE no longer report success while doing nothing — they state honestly that they are not yet implemented (need a break-gap / jog field on the dimension model). (DIM-019/020) Refs #181. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
3c5aa4453a
commit
0d42d57927
2 files changed
+174
-20
No files matched your search
+55
-20
@@ -481,12 +481,12 @@ impl OpenCADStudio {
|
||||
if layer.starts_with("__DIMBREAK__")
|
||||
|| layer.starts_with("__DIMBREAK_AUTO__")
|
||||
{
|
||||
// DIMBREAK is a geometry operation we approximate by
|
||||
// recording a note on the dimension; full intersection logic
|
||||
// requires render geometry. For now, just undo-snapshot and log.
|
||||
self.push_undo_snapshot(i, "DIMBREAK");
|
||||
self.command_line.push_output("DIMBREAK Break applied.");
|
||||
self.tabs[i].dirty = true;
|
||||
// DIMBREAK needs a break-gap field on the dimension
|
||||
// model (not yet present) to store and render the gap.
|
||||
// Report honestly rather than claiming success while
|
||||
// changing nothing. (#181 / DIM-020)
|
||||
self.command_line
|
||||
.push_info("DIMBREAK: not yet implemented — nothing changed.");
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
return Task::none();
|
||||
@@ -503,10 +503,11 @@ impl OpenCADStudio {
|
||||
return Task::none();
|
||||
}
|
||||
if layer.starts_with("__DIMJOG__") {
|
||||
// Record jog position — visual rendering handled by scene.
|
||||
self.push_undo_snapshot(i, "DIMJOGLINE");
|
||||
self.command_line.push_output("DIMJOGLINE Jog added.");
|
||||
self.tabs[i].dirty = true;
|
||||
// DIMJOGLINE needs a jog-point field on the dimension
|
||||
// model (not yet present) to store and render the jog.
|
||||
// Report honestly rather than faking success. (DIM-019)
|
||||
self.command_line
|
||||
.push_info("DIMJOGLINE: not yet implemented — nothing changed.");
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
return Task::none();
|
||||
@@ -2177,24 +2178,58 @@ fn apply_dimspace(scene: &mut crate::scene::Scene, encoded: &str) {
|
||||
.collect();
|
||||
let spacing: f64 = parts[2].parse().unwrap_or(0.0);
|
||||
|
||||
use acadrust::entities::Dimension;
|
||||
let base_h = acadrust::Handle::from(base_val);
|
||||
// Read base dimension's definition_point Z (perpendicular offset)
|
||||
let base_z = if let Some(acadrust::EntityType::Dimension(d)) = scene.document.get_entity(base_h)
|
||||
{
|
||||
d.base().definition_point.z
|
||||
} else {
|
||||
return;
|
||||
// Base dim: the perpendicular direction (from its rotation / axis) and the
|
||||
// dim line's perpendicular coordinate. Spacing steps each parallel dim along
|
||||
// this perp IN THE DRAWING PLANE — offsetting Z had no effect on the dim
|
||||
// line, which is computed from def·perp with perp.z = 0. (#181 / DIM-021)
|
||||
let (perp, base_coord) = match scene.document.get_entity(base_h) {
|
||||
Some(acadrust::EntityType::Dimension(Dimension::Linear(d))) => {
|
||||
let (s, c) = d.rotation.sin_cos();
|
||||
let perp = (-s, c);
|
||||
let dp = d.definition_point;
|
||||
(perp, dp.x * perp.0 + dp.y * perp.1)
|
||||
}
|
||||
Some(acadrust::EntityType::Dimension(Dimension::Aligned(d))) => {
|
||||
let dx = d.second_point.x - d.first_point.x;
|
||||
let dy = d.second_point.y - d.first_point.y;
|
||||
let len = (dx * dx + dy * dy).sqrt().max(1e-12);
|
||||
let perp = (-dy / len, dx / len);
|
||||
let dp = d.definition_point;
|
||||
(perp, dp.x * perp.0 + dp.y * perp.1)
|
||||
}
|
||||
_ => return,
|
||||
};
|
||||
|
||||
let effective_spacing = if spacing <= 0.0 { 10.0 } else { spacing };
|
||||
for (idx, &hv) in other_vals.iter().enumerate() {
|
||||
let h = acadrust::Handle::from(hv);
|
||||
let new_z = base_z + effective_spacing * (idx + 1) as f64;
|
||||
if let Some(acadrust::EntityType::Dimension(d)) = scene.document.get_entity_mut(h) {
|
||||
let dp = &mut d.base_mut().definition_point;
|
||||
dp.z = new_z;
|
||||
let target = base_coord + effective_spacing * (idx + 1) as f64;
|
||||
if let Some(acadrust::EntityType::Dimension(dim)) = scene.document.get_entity_mut(h) {
|
||||
// Slide this dim's definition point along perp so its perpendicular
|
||||
// coordinate equals `target`; update both the struct field (render)
|
||||
// and base (save).
|
||||
let slide = |p: &mut acadrust::types::Vector3| {
|
||||
let cur = p.x * perp.0 + p.y * perp.1;
|
||||
let delta = target - cur;
|
||||
p.x += perp.0 * delta;
|
||||
p.y += perp.1 * delta;
|
||||
};
|
||||
match dim {
|
||||
Dimension::Linear(d) => {
|
||||
slide(&mut d.definition_point);
|
||||
d.base.definition_point = d.definition_point;
|
||||
}
|
||||
Dimension::Aligned(d) => {
|
||||
slide(&mut d.definition_point);
|
||||
d.base.definition_point = d.definition_point;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
scene.bump_geometry();
|
||||
}
|
||||
|
||||
// ── MLEADERALIGN helper ───────────────────────────────────────────────────────
|
||||
|
||||
@@ -116,6 +116,88 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
|
||||
// QDIM second stage: the front-end relaunched with the picked
|
||||
// dimension-line point and the gathered geometry now selected. Build
|
||||
// a continuous chain of linear dimensions across the endpoints.
|
||||
cmd if cmd.starts_with("QDIM_PLACE ") => {
|
||||
let nums: Vec<f32> = cmd
|
||||
.split_whitespace()
|
||||
.skip(1)
|
||||
.filter_map(|s| s.parse().ok())
|
||||
.collect();
|
||||
if nums.len() < 3 {
|
||||
return Some(Task::none());
|
||||
}
|
||||
let place = glam::Vec3::new(nums[0], nums[1], nums[2]);
|
||||
let handles: Vec<acadrust::Handle> = self.tabs[i]
|
||||
.scene
|
||||
.selected_entities()
|
||||
.iter()
|
||||
.map(|(h, _)| *h)
|
||||
.collect();
|
||||
let mut pts: Vec<glam::Vec3> = Vec::new();
|
||||
for h in &handles {
|
||||
if let Some(e) = self.tabs[i].scene.document.get_entity(*h) {
|
||||
qdim_collect_points(e, &mut pts);
|
||||
}
|
||||
}
|
||||
if pts.len() < 2 {
|
||||
self.command_line
|
||||
.push_error("QDIM: no dimensionable endpoints in the selection.");
|
||||
return Some(Task::none());
|
||||
}
|
||||
// Choose the dimension axis from the points' spread: a wider X
|
||||
// span dimensions horizontally (ordered by X), else vertically.
|
||||
let (minx, maxx) = pts.iter().fold((f32::MAX, f32::MIN), |(a, b), p| {
|
||||
(a.min(p.x), b.max(p.x))
|
||||
});
|
||||
let (miny, maxy) = pts.iter().fold((f32::MAX, f32::MIN), |(a, b), p| {
|
||||
(a.min(p.y), b.max(p.y))
|
||||
});
|
||||
let horizontal = (maxx - minx) >= (maxy - miny);
|
||||
if horizontal {
|
||||
pts.sort_by(|a, b| a.x.total_cmp(&b.x));
|
||||
pts.dedup_by(|a, b| (a.x - b.x).abs() < 1e-4);
|
||||
} else {
|
||||
pts.sort_by(|a, b| a.y.total_cmp(&b.y));
|
||||
pts.dedup_by(|a, b| (a.y - b.y).abs() < 1e-4);
|
||||
}
|
||||
if pts.len() < 2 {
|
||||
self.command_line
|
||||
.push_error("QDIM: endpoints collapse to a single position.");
|
||||
return Some(Task::none());
|
||||
}
|
||||
self.push_undo_snapshot(i, "QDIM");
|
||||
let v = |p: glam::Vec3| acadrust::types::Vector3::new(p.x as f64, p.y as f64, p.z as f64);
|
||||
let mut made = 0usize;
|
||||
for w in pts.windows(2) {
|
||||
let (p1, p2) = (w[0], w[1]);
|
||||
let mut dim = acadrust::entities::DimensionLinear::new(v(p1), v(p2));
|
||||
dim.rotation = if horizontal {
|
||||
0.0
|
||||
} else {
|
||||
std::f64::consts::FRAC_PI_2
|
||||
};
|
||||
// Dim line passes through the picked perpendicular position.
|
||||
let def = if horizontal {
|
||||
glam::Vec3::new((p1.x + p2.x) * 0.5, place.y, 0.0)
|
||||
} else {
|
||||
glam::Vec3::new(place.x, (p1.y + p2.y) * 0.5, 0.0)
|
||||
};
|
||||
dim.definition_point = v(def);
|
||||
dim.base.definition_point = v(def);
|
||||
dim.base.actual_measurement = dim.measurement();
|
||||
self.commit_entity(acadrust::EntityType::Dimension(
|
||||
acadrust::entities::Dimension::Linear(dim),
|
||||
));
|
||||
made += 1;
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line
|
||||
.push_output(&format!("QDIM {made} dimensions created."));
|
||||
return Some(Task::none());
|
||||
}
|
||||
|
||||
"DIMEDIT" | "DED" => {
|
||||
use crate::modules::annotate::dimedit::DimEditCommand;
|
||||
let cmd = DimEditCommand::new();
|
||||
@@ -565,3 +647,40 @@ fn find_last_linear_dim(
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Collect candidate dimension endpoints from an entity for QDIM — line ends,
|
||||
/// polyline vertices, arc endpoints — in world space.
|
||||
fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec<glam::Vec3>) {
|
||||
use acadrust::EntityType as ET;
|
||||
let p = |v: &acadrust::types::Vector3| glam::Vec3::new(v.x as f32, v.y as f32, v.z as f32);
|
||||
match e {
|
||||
ET::Line(l) => {
|
||||
out.push(p(&l.start));
|
||||
out.push(p(&l.end));
|
||||
}
|
||||
ET::LwPolyline(pl) => {
|
||||
for v in &pl.vertices {
|
||||
out.push(glam::Vec3::new(
|
||||
v.location.x as f32,
|
||||
v.location.y as f32,
|
||||
pl.elevation as f32,
|
||||
));
|
||||
}
|
||||
}
|
||||
ET::Polyline(pl) => {
|
||||
for v in &pl.vertices {
|
||||
out.push(p(&v.location));
|
||||
}
|
||||
}
|
||||
ET::Arc(a) => {
|
||||
for &ang in &[a.start_angle, a.end_angle] {
|
||||
out.push(glam::Vec3::new(
|
||||
(a.center.x + a.radius * ang.cos()) as f32,
|
||||
(a.center.y + a.radius * ang.sin()) as f32,
|
||||
a.center.z as f32,
|
||||
));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user