Complete QDIM dimension workflows

This commit is contained in:
ramox81 committed 2026-08-27 16:19:23 +03:00
1 parent ba91095bcf
commit a5d2ff741c
7 files changed
+524 -246

No files matched your search

+126
View File
@@ -1400,6 +1400,132 @@ impl OpenCADStudio {
self.commit_undo_delta(i, pd);
}
}
CmdResult::CommitDimensionsAndExit(dimensions) => {
let association_mode = self.tabs[i]
.scene
.document
.header
.dimension_associativity;
let made = dimensions.len();
let pending = if association_mode == 0 {
let mut pieces = Vec::new();
for (mut entity, _) in dimensions {
let layer = self.tabs[i].active_layer.clone();
if layer != "0" || entity.as_entity().layer().is_empty() {
entity.as_entity_mut().set_layer(layer);
}
crate::scene::view::dispatch::apply_color(
&mut entity,
self.ribbon.active_color,
);
crate::scene::view::dispatch::apply_common_prop(
&mut entity,
"linetype",
&self.ribbon.active_linetype.clone(),
);
crate::scene::view::dispatch::apply_line_weight(
&mut entity,
self.ribbon.active_lineweight,
);
let celtscale = self.tabs[i]
.scene
.document
.header
.current_entity_linetype_scale;
if (celtscale - 1.0).abs() > 1e-9 && celtscale.abs() > 1e-9 {
entity.common_mut().linetype_scale = celtscale;
}
crate::scene::creation_style::apply_current_creation_styles(
&self.tabs[i].scene.document,
&mut entity,
);
pieces.extend(
crate::modules::draw::modify::explode::explode_entity(
&entity,
&self.tabs[i].scene.document,
),
);
}
let delta_safe = pieces
.iter()
.all(|piece| self.delta_add_safe(i, piece));
let pending = self.begin_undo(i, "QDIM", pieces.len(), delta_safe);
for piece in pieces {
self.tabs[i].scene.add_entity(piece);
}
pending
} else {
let delta_safe = dimensions
.iter()
.all(|(entity, _)| self.delta_add_safe(i, entity));
let pending = self.begin_undo(i, "QDIM", dimensions.len(), delta_safe);
for (entity, association) in dimensions {
let Some(handle) = self.commit_entity_handle(entity) else {
continue;
};
if association_mode != 2 {
continue;
}
let sources = match association {
crate::command::DimensionAssociationInput::Infer(source) => {
source.map_or_else(
|| {
self.tabs[i]
.scene
.infer_dimension_sources(handle)
.into_iter()
.map(|source| {
source.map(
crate::command::DimensionAssociationSource::inferred,
)
})
.collect()
},
|source| {
vec![Some(
crate::command::DimensionAssociationSource::inferred(
source,
),
)]
},
)
}
crate::command::DimensionAssociationInput::Explicit(sources) => {
sources
}
};
self.tabs[i]
.scene
.attach_dimension_association_sources(handle, sources.clone());
let mut changes = vec![(handle, crate::scene::ChangeKind::Modified)];
changes.extend(sources.into_iter().flatten().map(|source| {
(source.handle, crate::scene::ChangeKind::Modified)
}));
changes.sort_by_key(|(handle, _)| handle.value());
changes.dedup_by_key(|(handle, _)| handle.value());
self.tabs[i].scene.bump_entities(&changes);
}
pending
};
self.tabs[i].dirty = made > 0;
self.tabs[i].scene.clear_preview_wire();
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.restore_pre_cmd_tangent();
self.command_line
.push_output(crate::tf!("QDIM {made} dimensions created.").as_ref());
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
CmdResult::SetQuickDimensionSnapPriority(priority) => {
self.quick_dimension_snap_priority = priority.min(1);
self.persist_settings_if_changed();
let prompt = self.tabs[i].active_cmd.as_ref().map(|command| command.prompt());
if let Some(prompt) = prompt {
self.command_line.push_info(&prompt);
}
}
CmdResult::CommitSolid {
entity,
solid,
+25 -141
View File
@@ -682,102 +682,34 @@ impl OpenCADStudio {
"QDIM" => {
use crate::modules::annotate::qdim::QdimCommand;
let cmd = QdimCommand::new();
self.command_line.push_info(&cmd.prompt());
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<f64> = cmd
.split_whitespace()
.skip(1)
.filter_map(|s| s.parse().ok())
.collect();
if nums.len() < 3 {
return Some(Task::none());
}
let plane = if self.tabs[i].editing_model_space() {
self.tabs[i].ucs_xform().working_plane()
} else {
crate::command::WorkingPlane::default()
};
let place = plane.to_local(glam::DVec3::new(nums[0], nums[1], nums[2]));
let handles: Vec<acadrust::Handle> = self.tabs[i]
let document = &self.tabs[i].scene.document;
let dim_spacing = document
.dim_styles
.iter()
.find(|style| {
style
.name
.eq_ignore_ascii_case(&document.header.current_dimstyle_name)
})
.map(|style| style.dimdli)
.unwrap_or(1.5);
let selection = self.tabs[i]
.scene
.selected_entities()
.iter()
.map(|(h, _)| *h)
.into_iter()
.map(|(handle, entity)| crate::command::SelectionEntity {
handle,
entity: entity.clone(),
surface_area: None,
})
.collect();
let mut pts: Vec<glam::DVec3> = Vec::new();
for h in &handles {
if let Some(e) = self.tabs[i].scene.document.get_entity(*h) {
qdim_collect_points(e, &mut pts);
}
}
for point in &mut pts {
*point = plane.to_local(*point);
}
if pts.len() < 2 {
self.command_line
.push_error(crate::t!("QDIM: no dimensionable endpoints in the selection.").as_ref());
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((f64::MAX, f64::MIN), |(a, b), p| (a.min(p.x), b.max(p.x)));
let (miny, maxy) = pts
.iter()
.fold((f64::MAX, f64::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(crate::t!("QDIM: endpoints collapse to a single position.").as_ref());
return Some(Task::none());
}
self.push_undo_snapshot(i, "QDIM");
let v = |p: glam::DVec3| {
acadrust::types::Vector3::new(p.x, p.y, p.z)
};
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::DVec3::new((p1.x + p2.x) * 0.5, place.y, 0.0)
} else {
glam::DVec3::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();
let entity = acadrust::EntityType::Dimension(
acadrust::entities::Dimension::Linear(dim),
);
self.commit_entity(plane.place_entity(entity));
made += 1;
}
self.tabs[i].dirty = true;
self.command_line
.push_output(crate::tf!("QDIM {made} dimensions created.").as_ref());
return Some(Task::none());
let cmd = QdimCommand::new(
selection,
dim_spacing,
self.quick_dimension_snap_priority,
);
self.command_line.push_info(&cmd.prompt());
self.tabs[i].active_cmd = Some(Box::new(cmd));
}
"DIMEDIT" => {
@@ -1442,54 +1374,6 @@ 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::DVec3>) {
use acadrust::EntityType as ET;
let p = |v: &acadrust::types::Vector3| glam::DVec3::new(v.x, v.y, v.z);
let ocs = |point: (f64, f64, f64), normal: acadrust::types::Vector3| {
let world = crate::scene::view::transform::ocs_point_to_wcs(
point,
(normal.x, normal.y, normal.z),
);
glam::DVec3::new(world.0, world.1, world.2)
};
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(ocs((v.location.x, v.location.y, pl.elevation), pl.normal));
}
}
ET::Polyline2D(pl) => {
for v in &pl.vertices {
out.push(ocs((v.location.x, v.location.y, pl.elevation), pl.normal));
}
}
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(ocs(
(
a.center.x + a.radius * ang.cos(),
a.center.y + a.radius * ang.sin(),
a.center.z,
),
a.normal,
));
}
}
_ => {}
}
}
// ── TCASE helpers ──────────────────────────────────────────────────────────
fn sentence_case(s: &str) -> String {
let mut c = s.chars();
+3
View File
@@ -465,6 +465,8 @@ pub(super) struct OpenCADStudio {
options_tab: crate::ui::window::options::OptionsTab,
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
pub texteditmode: bool,
/// QDIM extension-origin priority: 0 = endpoints, 1 = intersections.
pub quick_dimension_snap_priority: u8,
/// When true (default), saving over an existing file first writes a `.bak`
/// copy of it for recovery (#205). Toggle with the ISAVEBAK command.
pub backup_on_save: bool,
@@ -3210,6 +3212,7 @@ impl OpenCADStudio {
dyn_input: true,
options_tab: crate::ui::window::options::OptionsTab::General,
texteditmode: false,
quick_dimension_snap_priority: 0,
backup_on_save: true,
file_assoc_enabled: true,
savetime_min: 10,
+4
View File
@@ -198,6 +198,9 @@ pub struct UserSettings {
pub osmode: i32,
/// Controls whether the TEXTEDIT command repeats automatically (0 = Multiple, 1 = Single).
pub texteditmode: bool,
/// QDIM extension-origin priority: 0 = endpoints, 1 = intersections.
#[serde(default)]
pub quick_dimension_snap_priority: u8,
/// TEXTFILL: fill TrueType glyphs (true) or draw them hollow (false).
pub textfill: bool,
/// When true, saving over an existing file first copies it to a sibling
@@ -284,6 +287,7 @@ impl Default for UserSettings {
// off (suppress bit 16384).
osmode: 575 | OSMODE_SUPPRESS,
texteditmode: false,
quick_dimension_snap_priority: 0,
textfill: true,
backup_on_save: true,
file_assoc_enabled: true,
+2
View File
@@ -382,6 +382,7 @@ impl OpenCADStudio {
self.snapper.snap_enabled,
),
texteditmode: self.texteditmode,
quick_dimension_snap_priority: self.quick_dimension_snap_priority,
textfill: crate::scene::text::sdf_atlas::textfill(),
backup_on_save: self.backup_on_save,
file_assoc_enabled: self.file_assoc_enabled,
@@ -440,6 +441,7 @@ impl OpenCADStudio {
self.snapper.enabled = modes.into_iter().collect();
self.snapper.snap_enabled = snap_enabled;
self.texteditmode = s.texteditmode;
self.quick_dimension_snap_priority = s.quick_dimension_snap_priority.min(1);
crate::scene::text::sdf_atlas::set_textfill(s.textfill);
self.backup_on_save = s.backup_on_save;
self.file_assoc_enabled = s.file_assoc_enabled;
+6
View File
@@ -1249,6 +1249,12 @@ pub enum CmdResult {
entity: EntityType,
association: DimensionAssociationInput,
},
/// Commit several dimensions with independent association sources in one
/// undo step, then end the command.
CommitDimensionsAndExit(Vec<(EntityType, DimensionAssociationInput)>),
/// Persist the quick-dimension extension-origin priority while keeping the
/// active command at its current prompt (0 = endpoints, 1 = intersections).
SetQuickDimensionSnapPriority(u8),
/// Commit a Model-tab 3D solid: the acadrust entity (for selection /
/// persistence) plus its B-rep (cached for boolean ops + shaded
/// rendering). Ends the command.
+358 -105
View File
@@ -1,17 +1,15 @@
// QDIM command — quick dimension: select objects then pick placement line.
//
// Workflow:
// 1. Select objects (window/click, Enter to finish selection)
// 2. Pick a point that defines the dimension-line position
//
// Creates one linear dimension per detected endpoint pair on the selected objects.
//! Quick dimension creation from a selected set of drawing entities.
use acadrust::entities::{Dimension, DimensionLinear};
use acadrust::entities::{Dimension, DimensionDiameter, DimensionLinear, DimensionOrdinate, DimensionRadius};
use acadrust::types::Vector3;
use acadrust::{EntityType, Handle};
use cadkernel::geom2d::{characteristic_points, intersect, Curve, SnapKind, Tolerance};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
use crate::command::{
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionAssociationSource,
EntityTransform, SelectionEntity, WorkingPlane,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use crate::t;
@@ -27,129 +25,384 @@ pub fn tool() -> ToolDef {
}
}
enum Step {
Gathering,
PlaceLine { handles: Vec<Handle> },
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Step { Gathering, Place, Datum, Edit, Settings }
#[derive(Clone, Copy, PartialEq, Eq)]
enum Mode { Continuous, Staggered, Baseline, Ordinate, Radius, Diameter }
#[derive(Clone, Copy, PartialEq, Eq)]
enum SnapPriority { Endpoints, Intersections }
#[derive(Clone)]
struct SourceCurve { handle: Handle, curve: Curve }
#[derive(Clone)]
struct Candidate { point: DVec3, sources: Vec<DimensionAssociationSource> }
pub struct QdimCommand {
step: Step,
mode: Mode,
snap_priority: SnapPriority,
plane: WorkingPlane,
selection: Vec<SelectionEntity>,
datum: Option<DVec3>,
excluded: Vec<DVec3>,
dim_spacing: f64,
}
impl QdimCommand {
pub fn new() -> Self {
pub fn new(selection: Vec<SelectionEntity>, dim_spacing: f64, snap_priority: u8) -> Self {
let step = if selection.is_empty() { Step::Gathering } else { Step::Place };
Self {
step: Step::Gathering,
step,
mode: Mode::Continuous,
snap_priority: if snap_priority == 1 { SnapPriority::Intersections } else { SnapPriority::Endpoints },
plane: WorkingPlane::default(),
selection,
datum: None,
excluded: Vec::new(),
dim_spacing: dim_spacing.abs().max(1e-9),
}
}
fn local_sources(&self) -> Vec<SourceCurve> {
let transform = EntityTransform::Affine(self.plane.to_local_transform());
self.selection.iter().filter_map(|selected| {
let mut entity = selected.entity.clone();
crate::scene::view::dispatch::apply_transform(&mut entity, &transform);
crate::entities::curve::entity_curve_xy(&entity)
.map(|curve| SourceCurve { handle: selected.handle, curve })
}).collect()
}
fn candidates(&self) -> Vec<Candidate> {
let curves = self.local_sources();
let mut candidates = if self.snap_priority == SnapPriority::Intersections {
let mut hits = Vec::new();
for first in 0..curves.len() {
for second in first + 1..curves.len() {
for crossing in intersect(&curves[first].curve, &curves[second].curve, Tolerance::default()) {
hits.push(Candidate {
point: DVec3::new(crossing.point[0], crossing.point[1], 0.0),
// An intersection depends on two source curves. The
// current association record stores one source per
// extension origin, so do not bind it to an
// unrelated endpoint marker.
sources: Vec::new(),
});
}
}
}
hits
} else { Vec::new() };
if candidates.is_empty() {
for source in &curves {
for point in characteristic_points(&source.curve).into_iter().filter(|point| point.kind == SnapKind::Endpoint) {
candidates.push(Candidate {
point: DVec3::new(point.point[0], point.point[1], 0.0),
sources: vec![DimensionAssociationSource::inferred(source.handle)],
});
}
}
}
for selected in &self.selection {
if let EntityType::Dimension(dimension) = &selected.entity {
for point in dimension_reference_points(dimension) {
candidates.push(Candidate {
point: self.plane.to_local(point),
sources: vec![DimensionAssociationSource::inferred(selected.handle)],
});
}
}
}
let tolerance = candidate_tolerance(candidates.iter().map(|candidate| candidate.point));
let mut unique: Vec<Candidate> = Vec::new();
for candidate in candidates {
if let Some(existing) = unique.iter_mut().find(|existing| existing.point.distance(candidate.point) <= tolerance) {
for source in candidate.sources {
if !existing.sources.iter().any(|current| current.handle == source.handle) {
existing.sources.push(source);
}
}
} else {
unique.push(candidate);
}
}
unique.retain(|candidate| !self.excluded.iter().any(|point| point.distance(candidate.point) <= tolerance));
unique
}
fn radial_sources(&self) -> Vec<(Handle, Curve)> {
self.local_sources().into_iter()
.filter(|source| matches!(source.curve, Curve::Circle(_) | Curve::Arc(_)))
.map(|source| (source.handle, source.curve)).collect()
}
fn orientation(&self, points: &[Candidate], place: DVec3) -> bool {
let min_x = points.iter().map(|point| point.point.x).fold(f64::INFINITY, f64::min);
let max_x = points.iter().map(|point| point.point.x).fold(f64::NEG_INFINITY, f64::max);
let min_y = points.iter().map(|point| point.point.y).fold(f64::INFINITY, f64::min);
let max_y = points.iter().map(|point| point.point.y).fold(f64::NEG_INFINITY, f64::max);
let outside_y = (place.y - max_y).max(min_y - place.y).max(0.0);
let outside_x = (place.x - max_x).max(min_x - place.x).max(0.0);
if (outside_x - outside_y).abs() > 1e-9 { outside_y >= outside_x } else { max_x - min_x >= max_y - min_y }
}
fn build_dimensions(&self, place_world: DVec3) -> Vec<(EntityType, DimensionAssociationInput)> {
let place = self.plane.to_local(place_world);
if matches!(self.mode, Mode::Radius | Mode::Diameter) {
return self.build_radial_dimensions(place);
}
let mut points = self.candidates();
if points.is_empty() { return Vec::new(); }
let horizontal = self.orientation(&points, place);
points.sort_by(|first, second| if horizontal { first.point.x.total_cmp(&second.point.x) } else { first.point.y.total_cmp(&second.point.y) });
let tolerance = candidate_tolerance(points.iter().map(|point| point.point));
points.dedup_by(|first, second| {
let delta = if horizontal { first.point.x - second.point.x } else { first.point.y - second.point.y };
delta.abs() <= tolerance
});
match self.mode {
Mode::Continuous | Mode::Staggered => points.windows(2).enumerate().map(|(index, pair)| {
let offset = if self.mode == Mode::Staggered { index as f64 * self.dim_spacing } else { 0.0 };
let direction = if horizontal {
if place.y >= (pair[0].point.y + pair[1].point.y) * 0.5 { 1.0 } else { -1.0 }
} else if place.x >= (pair[0].point.x + pair[1].point.x) * 0.5 { 1.0 } else { -1.0 };
let shifted = if horizontal { DVec3::new(place.x, place.y + direction * offset, 0.0) } else { DVec3::new(place.x + direction * offset, place.y, 0.0) };
self.linear_dimension(&pair[0], &pair[1], shifted, horizontal)
}).collect(),
Mode::Baseline => {
let datum = self.datum.unwrap_or(points[0].point);
let datum_candidate = Candidate {
point: datum,
sources: points.iter().min_by(|first, second| first.point.distance(datum).total_cmp(&second.point.distance(datum)))
.map(|point| point.sources.clone()).unwrap_or_default(),
};
points.iter().filter(|point| point.point.distance(datum) > tolerance)
.map(|point| self.linear_dimension(&datum_candidate, point, place, horizontal)).collect()
}
Mode::Ordinate => points.iter().map(|point| self.ordinate_dimension(point, place, horizontal)).collect(),
Mode::Radius | Mode::Diameter => Vec::new(),
}
}
fn linear_dimension(&self, first: &Candidate, second: &Candidate, place: DVec3, horizontal: bool) -> (EntityType, DimensionAssociationInput) {
let mut dimension = DimensionLinear::new(v3(first.point), v3(second.point));
dimension.rotation = if horizontal { 0.0 } else { std::f64::consts::FRAC_PI_2 };
let definition = if horizontal { DVec3::new(second.point.x, place.y, 0.0) } else { DVec3::new(place.x, second.point.y, 0.0) };
let text = if horizontal { DVec3::new((first.point.x + second.point.x) * 0.5, place.y, 0.0) } else { DVec3::new(place.x, (first.point.y + second.point.y) * 0.5, 0.0) };
dimension.definition_point = v3(definition);
dimension.base.definition_point = v3(definition);
dimension.base.text_middle_point = v3(text);
dimension.base.insertion_point = v3(text);
dimension.base.actual_measurement = dimension.measurement();
let association = DimensionAssociationInput::Explicit(vec![first.sources.first().copied(), second.sources.first().copied()]);
(self.plane.place_entity(EntityType::Dimension(Dimension::Linear(dimension))), association)
}
fn ordinate_dimension(&self, point: &Candidate, place: DVec3, horizontal: bool) -> (EntityType, DimensionAssociationInput) {
let datum = self.datum.unwrap_or(DVec3::ZERO);
let leader = if horizontal { DVec3::new(point.point.x, place.y, 0.0) } else { DVec3::new(place.x, point.point.y, 0.0) };
let mut dimension = DimensionOrdinate::new(v3(point.point), v3(leader), horizontal);
dimension.definition_point = v3(datum);
dimension.base.definition_point = v3(datum);
dimension.base.text_middle_point = v3(leader);
dimension.base.insertion_point = v3(leader);
dimension.refresh_measurement();
(self.plane.place_entity(EntityType::Dimension(Dimension::Ordinate(dimension))), DimensionAssociationInput::Explicit(vec![point.sources.first().copied()]))
}
fn build_radial_dimensions(&self, place: DVec3) -> Vec<(EntityType, DimensionAssociationInput)> {
self.radial_sources().into_iter().enumerate().map(|(index, (handle, curve))| {
let (center, radius) = match curve { Curve::Circle(circle) => (circle.centre, circle.radius), Curve::Arc(arc) => (arc.centre, arc.radius), _ => unreachable!() };
let center = DVec3::new(center[0], center[1], 0.0);
let mut direction = place - center;
direction.z = 0.0;
direction = if direction.length_squared() <= f64::EPSILON { DVec3::X } else { direction.normalize() };
let chord = center + direction * radius;
let text = place + DVec3::new(0.0, index as f64 * self.dim_spacing, 0.0);
let association = DimensionAssociationInput::Explicit(vec![Some(DimensionAssociationSource::inferred(handle))]);
let entity = if self.mode == Mode::Radius {
let mut dimension = DimensionRadius::new(v3(center), v3(chord));
dimension.base.definition_point = v3(chord);
dimension.base.text_middle_point = v3(text);
dimension.base.insertion_point = v3(text);
dimension.base.text_user_positioned = true;
dimension.leader_length = chord.distance(text);
dimension.base.actual_measurement = dimension.measurement();
EntityType::Dimension(Dimension::Radius(dimension))
} else {
let opposite = center - direction * radius;
let mut dimension = DimensionDiameter::new(v3(chord), v3(opposite));
dimension.base.definition_point = v3(opposite);
dimension.base.text_middle_point = v3(text);
dimension.base.insertion_point = v3(text);
dimension.base.text_user_positioned = true;
dimension.leader_length = chord.distance(text);
dimension.base.actual_measurement = dimension.measurement();
EntityType::Dimension(Dimension::Diameter(dimension))
};
(self.plane.place_entity(entity), association)
}).collect()
}
fn remove_candidate(&mut self, point_world: DVec3) {
let point = self.plane.to_local(point_world);
let all = self.candidates();
let Some(nearest) = all.iter().min_by(|first, second| first.point.distance(point).total_cmp(&second.point.distance(point))) else { return; };
self.excluded.push(nearest.point);
}
fn preview_dimensions(&self, point: DVec3) -> Vec<WireModel> {
self.build_dimensions(point).into_iter().enumerate().flat_map(|(index, (entity, _))| preview_entity(index, &entity)).collect()
}
}
impl CadCommand for QdimCommand {
fn name(&self) -> &'static str {
"QDIM"
}
fn set_working_plane(&mut self, plane: WorkingPlane) { self.plane = plane; }
fn name(&self) -> &'static str { "QDIM" }
fn prompt(&self) -> String {
match &self.step {
Step::Gathering => {
t!("QDIM Select geometry to dimension (Enter when done):").into_owned()
match self.step {
Step::Gathering => t!("QDIM Select geometry to dimension (Enter when done):").into_owned(),
Step::Place => t!("QDIM Specify dimension line position or choose an option:").into_owned(),
Step::Datum => t!("QDIM Specify datum point:").into_owned(),
Step::Edit => t!("QDIM Select a point to remove (Enter when done):").into_owned(),
Step::Settings => t!("QDIM Choose extension origin priority [Endpoints/Intersections]:").into_owned(),
}
}
fn options(&self) -> Vec<CmdOption> {
match self.step {
Step::Place => vec![
CmdOption::new("Continuous", "CONTINUOUS"), CmdOption::new("Staggered", "STAGGERED"),
CmdOption::new("Baseline", "BASELINE"), CmdOption::new("Ordinate", "ORDINATE"),
CmdOption::new("Radius", "RADIUS"), CmdOption::new("Diameter", "DIAMETER"),
CmdOption::new("Datum point", "DATUM"), CmdOption::new("Edit", "EDIT"),
CmdOption::new("Settings", "SETTINGS"),
],
Step::Settings => vec![CmdOption::new("Endpoints", "ENDPOINTS"), CmdOption::new("Intersections", "INTERSECTIONS")],
_ => Vec::new(),
}
}
fn is_selection_gathering(&self) -> bool { self.step == Step::Gathering }
fn selection_forces_add(&self) -> bool { self.step == Step::Gathering }
fn on_selection_complete(&mut self, _handles: Vec<Handle>) -> CmdResult { CmdResult::NeedPoint }
fn inject_selection_entities(&mut self, entities: Vec<SelectionEntity>) { self.selection = entities; }
fn wants_text_input(&self) -> bool { matches!(self.step, Step::Place | Step::Settings) }
fn point_step_accepts_keywords(&self) -> bool { self.step == Step::Place }
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
let keyword = text.trim().to_ascii_uppercase();
if self.step == Step::Settings {
match keyword.as_str() {
"E" | "ENDPOINT" | "ENDPOINTS" => { self.snap_priority = SnapPriority::Endpoints; self.step = Step::Place; return Some(CmdResult::SetQuickDimensionSnapPriority(0)); }
"I" | "INTERSECTION" | "INTERSECTIONS" => { self.snap_priority = SnapPriority::Intersections; self.step = Step::Place; return Some(CmdResult::SetQuickDimensionSnapPriority(1)); }
_ => return Some(CmdResult::NeedPoint),
}
Step::PlaceLine { .. } => t!("QDIM Specify dimension line position:").into_owned(),
}
}
fn is_selection_gathering(&self) -> bool {
matches!(self.step, Step::Gathering)
}
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
if handles.is_empty() {
return CmdResult::Cancel;
match keyword.as_str() {
"C" | "CONTINUOUS" => self.mode = Mode::Continuous,
"S" | "STAGGERED" => self.mode = Mode::Staggered,
"B" | "BASELINE" => self.mode = Mode::Baseline,
"O" | "ORDINATE" => self.mode = Mode::Ordinate,
"R" | "RADIUS" => self.mode = Mode::Radius,
"DI" | "DIAMETER" => self.mode = Mode::Diameter,
"DA" | "DATUM" | "DATUMPOINT" => { self.step = Step::Datum; return Some(CmdResult::NeedPoint); }
"E" | "EDIT" => { self.step = Step::Edit; return Some(CmdResult::NeedPoint); }
"SE" | "SETTINGS" => { self.step = Step::Settings; return Some(CmdResult::NeedPoint); }
_ => return Some(CmdResult::NeedPoint),
}
self.step = Step::PlaceLine { handles };
CmdResult::NeedPoint
Some(CmdResult::NeedPoint)
}
fn on_point(&mut self, pt: DVec3) -> CmdResult {
if let Step::PlaceLine { handles } = &self.step {
CmdResult::Relaunch(
// Keep the picked placement point in full f64 precision so the
// committed dimension coordinates are not quantized to f32.
format!("QDIM_PLACE {:.6} {:.6} {:.6}", pt.x, pt.y, pt.z),
handles.clone(),
)
} else {
CmdResult::NeedPoint
fn on_point(&mut self, point: DVec3) -> CmdResult {
match self.step {
Step::Gathering | Step::Settings => CmdResult::NeedPoint,
Step::Datum => { self.datum = Some(self.plane.to_local(point)); self.step = Step::Place; CmdResult::NeedPoint }
Step::Edit => { self.remove_candidate(point); CmdResult::NeedPoint }
Step::Place => {
let dimensions = self.build_dimensions(point);
if dimensions.is_empty() { CmdResult::NeedPoint } else { CmdResult::CommitDimensionsAndExit(dimensions) }
}
}
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> { let pt = pt.as_vec3();
if !matches!(self.step, Step::PlaceLine { .. }) {
return None;
match self.step {
Step::Gathering if !self.selection.is_empty() => { self.step = Step::Place; CmdResult::NeedPoint }
Step::Edit | Step::Datum | Step::Settings => { self.step = Step::Place; CmdResult::NeedPoint }
_ => CmdResult::Cancel,
}
let d = 0.5_f32;
Some(WireModel {
point_marker: None,
taper_widths: Vec::new(),
pattern_stations: Vec::new(),
world_width: 0.0,
depth_override: None,
display_visible: true,
plot_visible: true,
fill_is_3d: false,
fill_is_2d_solid: false,
render_instance: None,
pick_tris: Vec::new(),
pick_tris_low: Vec::new(),
dash_from_start: false,
dash_align_end: None,
text_verts: Vec::new(),
name: "qdim_preview".into(),
points: vec![[pt.x - d * 3.0, pt.y, pt.z], [pt.x + d * 3.0, pt.y, pt.z]],
points_low: Vec::new(),
color: WireModel::CYAN,
selected: false,
pattern_length: 0.0,
pattern: [0.0; 8],
line_weight_px: 1.0,
snap_pts: vec![],
tangent_geoms: vec![],
aci: 0,
key_vertices: vec![],
aabb: WireModel::UNBOUNDED_AABB,
plinegen: true,
fill_tris: vec![],
fill_tris_low: Vec::new(),
})
}
fn on_preview_wires(&mut self, point: DVec3) -> Vec<WireModel> {
if self.step == Step::Place { return self.preview_dimensions(point); }
if self.step == Step::Edit {
let size = self.dim_spacing * 0.12;
return self.candidates().into_iter().enumerate().flat_map(|(index, candidate)| {
let point = self.plane.to_world(candidate.point);
[
WireModel::solid_f64(format!("qdim_edit_{index}_x"), vec![
[point.x - size, point.y, point.z], [point.x + size, point.y, point.z],
], WireModel::CYAN, false),
WireModel::solid_f64(format!("qdim_edit_{index}_y"), vec![
[point.x, point.y - size, point.z], [point.x, point.y + size, point.z],
], WireModel::CYAN, false),
]
}).collect();
}
Vec::new()
}
}
/// Build a linear dimension between two points at the given dimension-line position.
#[allow(dead_code)]
pub fn make_linear_dim(p1: DVec3, p2: DVec3, dim_pt: DVec3) -> EntityType {
let v = |p: DVec3| Vector3::new(p.x, p.y, p.z);
let mut dim = DimensionLinear::new(v(p1), v(p2));
// Determine axis: horizontal if Δx > Δz, else vertical
let dx = (p2.x - p1.x).abs();
let dz = (p2.z - p1.z).abs();
dim.rotation = if dz > dx {
std::f64::consts::FRAC_PI_2
} else {
0.0
};
dim.definition_point = v(dim_pt);
dim.base.definition_point = v(dim_pt);
let text_pos = (p1 + p2) * 0.5;
dim.base.text_middle_point = v(text_pos);
dim.base.insertion_point = v(text_pos);
dim.base.actual_measurement = dim.measurement();
EntityType::Dimension(Dimension::Linear(dim))
fn v3(point: DVec3) -> Vector3 { Vector3::new(point.x, point.y, point.z) }
fn d3(point: Vector3) -> DVec3 { DVec3::new(point.x, point.y, point.z) }
fn candidate_tolerance(points: impl Iterator<Item = DVec3>) -> f64 {
let magnitude = points.map(|point| point.x.abs().max(point.y.abs()).max(point.z.abs())).fold(1.0_f64, f64::max);
Tolerance::default().linear().max(magnitude * f64::EPSILON * 64.0)
}
fn dimension_reference_points(dimension: &Dimension) -> Vec<DVec3> {
match dimension {
Dimension::Linear(value) => vec![d3(value.first_point), d3(value.second_point)],
Dimension::Aligned(value) => vec![d3(value.first_point), d3(value.second_point)],
Dimension::Radius(value) => vec![d3(value.angle_vertex), d3(value.definition_point)],
Dimension::Diameter(value) => vec![d3(value.angle_vertex), d3(value.definition_point)],
Dimension::Ordinate(value) => vec![d3(value.feature_location)],
Dimension::Angular2Ln(value) => vec![d3(value.first_point), d3(value.second_point), d3(value.angle_vertex), d3(value.definition_point)],
Dimension::Angular3Pt(value) => vec![d3(value.angle_vertex), d3(value.first_point), d3(value.second_point)],
Dimension::Arc(value) => vec![d3(value.center_point), d3(value.first_extension_point), d3(value.second_extension_point)],
Dimension::LargeRadial(value) => vec![d3(value.definition_point), d3(value.chord_point)],
}
}
// ── Autocomplete registry ─────────────────────────────────
inventory::submit!(crate::command::CommandRegistration { names: &["QDIM"] }); // QdimCommand
fn preview_entity(index: usize, entity: &EntityType) -> Vec<WireModel> {
let EntityType::Dimension(dimension) = entity else { return Vec::new(); };
let mut lines: Vec<Vec<[f64; 3]>> = Vec::new();
match dimension {
Dimension::Linear(value) => {
let first = d3(value.first_point); let second = d3(value.second_point); let definition = d3(value.definition_point);
let horizontal = value.rotation.cos().abs() >= value.rotation.sin().abs();
let first_line = if horizontal { DVec3::new(first.x, definition.y, definition.z) } else { DVec3::new(definition.x, first.y, definition.z) };
let second_line = if horizontal { DVec3::new(second.x, definition.y, definition.z) } else { DVec3::new(definition.x, second.y, definition.z) };
lines.push(vec![first.to_array(), first_line.to_array()]);
lines.push(vec![second.to_array(), second_line.to_array()]);
lines.push(vec![first_line.to_array(), second_line.to_array()]);
}
Dimension::Ordinate(value) => lines.push(vec![d3(value.feature_location).to_array(), d3(value.leader_endpoint).to_array()]),
Dimension::Radius(value) => lines.push(vec![d3(value.angle_vertex).to_array(), d3(value.definition_point).to_array(), d3(value.base.text_middle_point).to_array()]),
Dimension::Diameter(value) => lines.push(vec![d3(value.definition_point).to_array(), d3(value.angle_vertex).to_array(), d3(value.base.text_middle_point).to_array()]),
_ => {}
}
lines.into_iter().enumerate().map(|(part, points)| WireModel::solid_f64(format!("qdim_preview_{index}_{part}"), points, WireModel::CYAN, false)).collect()
}
inventory::submit!(crate::command::CommandRegistration { names: &["QDIM"] });