Merge pull request #1415 from ramox81/codex/aligned-constraint-parity

Aligned dimensional constraint: Point & line, 2Lines, value and property parity

# Conflicts:
#	src/app/command_driver.rs
#	src/app/update/viewport.rs
#	src/entities/dimension.rs
#	src/scene/parametric_constraints.rs
This commit is contained in:
Hakan Seven committed 2026-09-21 22:41:42 +03:00
commit 7490b2bebd
12 files changed
+1292 -167

No files matched your search

+344 -24
View File
@@ -16,6 +16,29 @@ pub enum DimConstraintAxis {
Aligned,
}
/// A picked line (a line entity or one polyline segment) an Aligned
/// constraint measures perpendicular to.
#[derive(Clone, Copy)]
struct LineTarget {
line: ParametricRef,
/// Unit direction from its start to its end.
dir: DVec3,
/// Its two ends and where they are.
ends: [(ParametricRef, DVec3); 2],
}
impl LineTarget {
/// The end nearest to `point` — the end the reference measures from.
fn nearest_end(&self, point: DVec3) -> (ParametricRef, DVec3) {
let [a, b] = self.ends;
if (b.1 - point).length_squared() < (a.1 - point).length_squared() {
b
} else {
a
}
}
}
#[derive(Clone, Copy)]
enum Step {
/// `Specify first constraint point or [Object] <Object>:`
@@ -27,12 +50,37 @@ enum Step {
first: ParametricRef,
first_point: DVec3,
},
/// Aligned's Point & line: `Specify constraint point or [Line] <Line>:`
PointLinePoint,
/// `Select line:` after the constraint point.
PointLineLine {
point: ParametricRef,
point_pos: DVec3,
},
/// `Select line:` first (the Line option).
LineFirst,
/// `Select constraint point:` after the line.
LinePoint { line: LineTarget },
/// Aligned's 2Lines: `Select first line:`
TwoLinesFirst,
/// `Select second line to make parallel:`
TwoLinesSecond { line: LineTarget },
/// The host is making the second line parallel; `accept_parallel_line`
/// brings its solved ends.
TwoLinesParallel {
first: LineTarget,
second: LineTarget,
/// Where the second line was picked.
pick: DVec3,
},
/// `Specify dimension line location:`
Location {
first: ParametricRef,
second: ParametricRef,
first_point: DVec3,
second_point: DVec3,
/// The line the distance is measured perpendicular to.
direction: Option<LineTarget>,
},
/// `Enter value or name and value <d1=100>:`
Value {
@@ -44,6 +92,7 @@ enum Step {
axis: DVec3,
kind: ConstraintKind,
measured: f64,
direction: Option<LineTarget>,
},
}
@@ -63,6 +112,7 @@ impl DimConstraintCommand {
pub const NO_POINT: &'static str = "No valid constraint point found.";
pub const SAME_POINT: &'static str =
"The object or point is already selected. Select a different object or constraint point.";
pub const INVALID_LINE: &'static str = "Invalid selection for Aligned. Select a line segment, polyline segment, text, MText, major or minor axis of ellipse or elliptical arc.";
pub fn new(axis: DimConstraintAxis, default_name: String) -> Self {
Self {
@@ -127,9 +177,108 @@ impl DimConstraintCommand {
Some(DVec3::new(point.x, point.y, point.z))
}
/// The line a Point & line or 2Lines pick measures against: a line
/// entity as a whole, or the picked polyline segment.
fn line_target(entity: &EntityType, handle: Handle, point: DVec3) -> Option<LineTarget> {
let (line, first, second) = match entity {
EntityType::Line(_) => (
ParametricRef::whole(handle),
ParametricRef::point(handle, 0),
ParametricRef::point(handle, 1),
),
EntityType::LwPolyline(_) | EntityType::Polyline2D(_) => {
let (source, _, _) =
crate::scene::centerline::picked_source(entity, handle, point)?;
let index = source.segment_index;
if index < 0 {
return None;
}
let end = if resolve_point(entity, index + 1).is_some() {
index + 1
} else {
0
};
(
ParametricRef::segment(handle, index as usize),
ParametricRef::point(handle, index),
ParametricRef::point(handle, end),
)
}
// A text's baseline or an ellipse's axis is a line whose one
// constraint point is the insertion point / the center.
EntityType::Text(_) | EntityType::MText(_) => {
return Self::axis_target(entity, ParametricRef::text_baseline(handle), ParametricRef::point(handle, 0));
}
EntityType::Ellipse(_) => {
let center = ParametricRef::point(handle, -3);
let major = Self::axis_target(entity, ParametricRef::ellipse_major_axis(handle), center);
let minor = Self::axis_target(entity, ParametricRef::ellipse_minor_axis(handle), center);
let gap = |target: &LineTarget| {
let offset = point - target.ends[0].1;
(offset.x * target.dir.y - offset.y * target.dir.x).abs()
};
return match (major, minor) {
(Some(major), Some(minor)) => Some(if gap(&minor) < gap(&major) { minor } else { major }),
(major, minor) => major.or(minor),
};
}
_ => return None,
};
let start = Self::world(entity, first)?;
let finish = Self::world(entity, second)?;
let dir = (finish - start).try_normalize()?;
Some(LineTarget {
line,
dir,
ends: [(first, start), (second, finish)],
})
}
/// A directional axis (text baseline, ellipse axis) as a line target
/// whose only constraint point is `anchor`.
fn axis_target(entity: &EntityType, axis: ParametricRef, anchor: ParametricRef) -> Option<LineTarget> {
let [start, end] =
crate::scene::parametric_constraints::directional_axis_endpoints(entity, axis)?;
let (start, end) = (
DVec3::new(start.x, start.y, start.z),
DVec3::new(end.x, end.y, end.z),
);
let dir = (end - start).try_normalize()?;
let anchor_point = Self::world(entity, anchor)?;
Some(LineTarget {
line: axis,
dir,
ends: [(anchor, anchor_point), (anchor, anchor_point)],
})
}
/// A picked line for the current line step, or the message to show.
fn pick_line(&mut self, handle: Handle, point: DVec3) -> Result<LineTarget, &'static str> {
if handle.is_null() {
return Err(Self::NO_OBJECT);
}
let Some(entity) = self.picked_entity.take() else {
return Err("");
};
Self::line_target(&entity, handle, point).ok_or(Self::INVALID_LINE)
}
/// The measured kind and axis: Linear reads the dimension line location
/// the way a linear dimension does, the others are fixed.
fn decide(&self, first: DVec3, second: DVec3, location: DVec3) -> (ConstraintKind, DVec3) {
/// the way a linear dimension does, the others are fixed; a Point & line
/// or 2Lines distance runs perpendicular to its line.
fn decide(
&self,
first: DVec3,
second: DVec3,
location: DVec3,
direction: Option<LineTarget>,
) -> (ConstraintKind, DVec3) {
if let Some(line) = direction {
return (
ConstraintKind::DistanceDirected,
DVec3::new(line.dir.y, -line.dir.x, 0.0),
);
}
match self.axis {
DimConstraintAxis::Linear => {
let axis =
@@ -149,10 +298,11 @@ impl DimConstraintCommand {
}
}
fn measured(kind: ConstraintKind, first: DVec3, second: DVec3) -> f64 {
fn measured(kind: ConstraintKind, first: DVec3, second: DVec3, axis: DVec3) -> f64 {
match kind {
ConstraintKind::DistanceX => (second.x - first.x).abs(),
ConstraintKind::DistanceY => (second.y - first.y).abs(),
ConstraintKind::DistanceDirected => (second - first).dot(axis).abs(),
_ => (second - first).length(),
}
}
@@ -166,6 +316,7 @@ impl DimConstraintCommand {
location,
axis,
kind,
direction,
..
} = self.step
else {
@@ -182,6 +333,7 @@ impl DimConstraintCommand {
second_point,
location,
axis,
direction: direction.map(|line| line.line),
name,
expression,
label: match self.axis {
@@ -202,11 +354,23 @@ impl CadCommand for DimConstraintCommand {
fn prompt(&self) -> String {
let name = self.command_name();
match self.step {
Step::First if self.axis == DimConstraintAxis::Aligned => format!(
"{name} Specify first constraint point or [Object/Point & line/2Lines] <Object>:"
),
Step::First => {
format!("{name} Specify first constraint point or [Object] <Object>:")
}
Step::Object => format!("{name} Select object:"),
Step::Second { .. } => format!("{name} Specify second constraint point:"),
Step::PointLinePoint => {
format!("{name} Specify constraint point or [Line] <Line>:")
}
Step::PointLineLine { .. } | Step::LineFirst => format!("{name} Select line:"),
Step::LinePoint { .. } => format!("{name} Select constraint point:"),
Step::TwoLinesFirst => format!("{name} Select first line:"),
Step::TwoLinesSecond { .. } | Step::TwoLinesParallel { .. } => {
format!("{name} Select second line to make parallel:")
}
Step::Location { .. } => format!("{name} Specify dimension line location:"),
Step::Value { measured, .. } => format!(
"{name} Enter value or name and value <{}={}>:",
@@ -217,9 +381,16 @@ impl CadCommand for DimConstraintCommand {
}
fn options(&self) -> Vec<CmdOption> {
matches!(self.step, Step::First)
.then(|| vec![CmdOption::new("Object", "O")])
.unwrap_or_default()
match self.step {
Step::First if self.axis == DimConstraintAxis::Aligned => vec![
CmdOption::new("Object", "O"),
CmdOption::new("Point & line", "P"),
CmdOption::new("2Lines", "2L"),
],
Step::First => vec![CmdOption::new("Object", "O")],
Step::PointLinePoint => vec![CmdOption::new("Line", "L")],
_ => Vec::new(),
}
}
fn wants_text_input(&self) -> bool {
@@ -227,15 +398,31 @@ impl CadCommand for DimConstraintCommand {
}
fn point_step_accepts_keywords(&self) -> bool {
matches!(self.step, Step::First | Step::Value { .. })
matches!(
self.step,
Step::First | Step::PointLinePoint | Step::Value { .. }
)
}
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
match self.step {
Step::First => {
let keyword = text.trim().trim_start_matches('_').to_ascii_uppercase();
if matches!(keyword.as_str(), "O" | "OBJECT") {
self.step = Step::Object;
let aligned = self.axis == DimConstraintAxis::Aligned;
match keyword.as_str() {
"O" | "OBJECT" => self.step = Step::Object,
"P" | "POINT" | "POINT & LINE" | "POINT&LINE" if aligned => {
self.step = Step::PointLinePoint
}
"2L" | "2LINES" if aligned => self.step = Step::TwoLinesFirst,
_ => return None,
}
Some(CmdResult::NeedPoint)
}
Step::PointLinePoint => {
let keyword = text.trim().trim_start_matches('_').to_ascii_uppercase();
if matches!(keyword.as_str(), "L" | "LINE") {
self.step = Step::LineFirst;
return Some(CmdResult::NeedPoint);
}
None
@@ -253,7 +440,18 @@ impl CadCommand for DimConstraintCommand {
}
fn needs_entity_pick(&self) -> bool {
matches!(self.step, Step::First | Step::Object | Step::Second { .. })
matches!(
self.step,
Step::First
| Step::Object
| Step::Second { .. }
| Step::PointLinePoint
| Step::PointLineLine { .. }
| Step::LineFirst
| Step::LinePoint { .. }
| Step::TwoLinesFirst
| Step::TwoLinesSecond { .. }
)
}
fn entity_pick_accepts_points(&self) -> bool {
@@ -280,11 +478,72 @@ impl CadCommand for DimConstraintCommand {
match self.step {
// The host resolves the constraint point and hands it back
// through `accept_constraint_point`.
Step::First | Step::Second { .. } => CmdResult::CheckConstraintPoint(CoincidentPick {
handle: (!handle.is_null()).then_some(handle),
point,
whole_curve: false,
}),
Step::First | Step::Second { .. } | Step::PointLinePoint | Step::LinePoint { .. } => {
CmdResult::CheckConstraintPoint(CoincidentPick {
handle: (!handle.is_null()).then_some(handle),
point,
whole_curve: false,
})
}
Step::PointLineLine { point: first, point_pos } => {
match self.pick_line(handle, point) {
Ok(line) => {
if line.line.entity == first.entity {
return Self::report(Self::SAME_POINT);
}
let (second, second_point) = line.nearest_end(point_pos);
self.step = Step::Location {
first,
second,
first_point: point_pos,
second_point,
direction: Some(line),
};
CmdResult::NeedPoint
}
Err("") => CmdResult::NeedPoint,
Err(message) => Self::report(message),
}
}
Step::LineFirst => match self.pick_line(handle, point) {
Ok(line) => {
self.step = Step::LinePoint { line };
CmdResult::NeedPoint
}
Err("") => CmdResult::NeedPoint,
Err(message) => Self::report(message),
},
Step::TwoLinesFirst => match self.pick_line(handle, point) {
Ok(line) => {
self.step = Step::TwoLinesSecond { line };
CmdResult::NeedPoint
}
Err("") => CmdResult::NeedPoint,
Err(message) => Self::report(message),
},
Step::TwoLinesSecond { line: first_line } => match self.pick_line(handle, point) {
Ok(second_line) => {
if second_line.line == first_line.line {
return Self::report(Self::SAME_POINT);
}
// The host makes the second line parallel first; the
// distance is measured on the solved geometry.
self.step = Step::TwoLinesParallel {
first: first_line,
second: second_line,
pick: point,
};
CmdResult::MakeParallel {
first_line: first_line.line,
first_ends: [first_line.ends[0].0, first_line.ends[1].0],
second_line: second_line.line,
second_ends: [second_line.ends[0].0, second_line.ends[1].0],
}
}
Err("") => CmdResult::NeedPoint,
Err(message) => Self::report(message),
},
Step::TwoLinesParallel { .. } => CmdResult::NeedPoint,
Step::Object => {
if handle.is_null() {
return Self::report(Self::NO_OBJECT);
@@ -308,6 +567,7 @@ impl CadCommand for DimConstraintCommand {
second,
first_point,
second_point,
direction: None,
};
CmdResult::NeedPoint
}
@@ -315,6 +575,32 @@ impl CadCommand for DimConstraintCommand {
}
}
fn accept_parallel_line(&mut self, ends: [(ParametricRef, DVec3); 2]) -> CmdResult {
let Step::TwoLinesParallel { first, second, pick } = self.step else {
return CmdResult::NeedPoint;
};
let Some(dir) = (ends[1].1 - ends[0].1).try_normalize() else {
return Self::report(Self::NO_POINT);
};
let second = LineTarget {
line: second.line,
dir,
ends,
};
// The first line's end nearest the second pick, then the second
// line's end nearest that end.
let (first_ref, first_point) = first.nearest_end(pick);
let (second_ref, second_point) = second.nearest_end(first_point);
self.step = Step::Location {
first: first_ref,
second: second_ref,
first_point,
second_point,
direction: Some(first),
};
CmdResult::NeedPoint
}
fn accept_constraint_point(&mut self, reference: ParametricRef, point: DVec3) -> CmdResult {
match self.step {
Step::First => {
@@ -333,6 +619,28 @@ impl CadCommand for DimConstraintCommand {
second: reference,
first_point,
second_point: point,
direction: None,
};
CmdResult::NeedPoint
}
Step::PointLinePoint => {
self.step = Step::PointLineLine {
point: reference,
point_pos: point,
};
CmdResult::NeedPoint
}
Step::LinePoint { line } => {
if line.line.entity == reference.entity {
return Self::report(Self::SAME_POINT);
}
let (first, first_point) = line.nearest_end(point);
self.step = Step::Location {
first,
second: reference,
first_point,
second_point: point,
direction: Some(line),
};
CmdResult::NeedPoint
}
@@ -342,20 +650,27 @@ impl CadCommand for DimConstraintCommand {
fn on_point(&mut self, point: DVec3) -> CmdResult {
match self.step {
Step::First | Step::Second { .. } => CmdResult::CheckConstraintPoint(CoincidentPick {
handle: None,
point,
whole_curve: false,
}),
Step::Object => Self::report(Self::NO_OBJECT),
Step::First | Step::Second { .. } | Step::PointLinePoint | Step::LinePoint { .. } => {
CmdResult::CheckConstraintPoint(CoincidentPick {
handle: None,
point,
whole_curve: false,
})
}
Step::Object
| Step::PointLineLine { .. }
| Step::LineFirst
| Step::TwoLinesFirst
| Step::TwoLinesSecond { .. } => Self::report(Self::NO_OBJECT),
Step::Location {
first,
second,
first_point,
second_point,
direction,
} => {
let (kind, axis) = self.decide(first_point, second_point, point);
let measured = Self::measured(kind, first_point, second_point);
let (kind, axis) = self.decide(first_point, second_point, point, direction);
let measured = Self::measured(kind, first_point, second_point, axis);
self.step = Step::Value {
first,
second,
@@ -365,10 +680,11 @@ impl CadCommand for DimConstraintCommand {
axis,
kind,
measured,
direction,
};
CmdResult::ReportMeasurement(format!("Dimension text = {measured:.4}"))
}
Step::Value { .. } => CmdResult::NeedPoint,
Step::TwoLinesParallel { .. } | Step::Value { .. } => CmdResult::NeedPoint,
}
}
@@ -378,6 +694,10 @@ impl CadCommand for DimConstraintCommand {
self.step = Step::Object;
CmdResult::NeedPoint
}
Step::PointLinePoint => {
self.step = Step::LineFirst;
CmdResult::NeedPoint
}
Step::Value { measured, .. } => self
.build(self.default_name.clone(), measured_expression(measured))
.unwrap_or(CmdResult::Cancel),