Files
OneCAD-Draft/src/modules/annotate/qdim.rs
T
Hakan SevenandClaude Opus 5.5 e4d04ef132 Cargo: codec e023bc3 (acadrust sync), kernel abca4e8, graph c63556f
The codec now holds wide EntityType variants boxed (MText, Helix,
Dimension, Hatch, Viewport, MultiLeader, Surface, Table, Ole2Frame,
Light, Extended). Constructors box their entity; nested patterns reach
a dimension through entities::dimension::as_dimension(_mut). Also
follows BlockAction::action_ids, LineTypeComplexData::source_flags and
the decimal tab stop with an explicit character.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 09:17:01 +03:00

734 lines
26 KiB
Rust

//! Quick dimension creation from a selected set of drawing entities.
use codec::entities::{
Dimension, DimensionDiameter, DimensionLinear, DimensionOrdinate, DimensionRadius,
};
use codec::types::Vector3;
use codec::{EntityType, Handle};
use glam::DVec3;
use kernel::geom2d::{characteristic_points, intersect, Curve, SnapKind, Tolerance};
use crate::command::{
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionAssociationSource,
DimensionPreview, EntityTransform, SelectionEntity, WorkingPlane,
};
use crate::modules::{IconKind, ModuleEvent, ToolDef};
use crate::scene::model::wire_model::WireModel;
use crate::t;
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/qdim.svg"));
pub fn tool() -> ToolDef {
ToolDef {
id: "QDIM",
label: "Quick Dim",
icon: ICON,
event: ModuleEvent::Command("QDIM".to_string()),
}
}
#[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(selection: Vec<SelectionEntity>, dim_spacing: f64, snap_priority: u8) -> Self {
let step = if selection.is_empty() {
Step::Gathering
} else {
Step::Place
};
Self {
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),
// Intersections depend on two curves, but each
// extension origin stores one source.
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()
.find(|point| point.point.distance(datum) <= tolerance)
.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);
crate::entities::dimension::reset_automatic_text_position(&mut dimension.base);
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(Box::new(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(Box::new(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(Box::new(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(Box::new(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 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()
}
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),
}
}
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),
}
Some(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 {
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,
}
}
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 x_start = self.plane.to_world(candidate.point - DVec3::X * size);
let x_end = self.plane.to_world(candidate.point + DVec3::X * size);
let y_start = self.plane.to_world(candidate.point - DVec3::Y * size);
let y_end = self.plane.to_world(candidate.point + DVec3::Y * size);
[
WireModel::solid_f64(
format!("qdim_edit_{index}_x"),
vec![x_start.to_array(), x_end.to_array()],
WireModel::CYAN,
false,
),
WireModel::solid_f64(
format!("qdim_edit_{index}_y"),
vec![y_start.to_array(), y_end.to_array()],
WireModel::CYAN,
false,
),
]
})
.collect();
}
Vec::new()
}
fn dimension_preview(&self, cursor: DVec3) -> Option<Vec<DimensionPreview>> {
(self.step == Step::Place).then(|| {
self.build_dimensions(cursor)
.into_iter()
.map(|(entity, _)| DimensionPreview::current_style(entity))
.collect()
})
}
}
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)],
}
}
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"] });