Merge pull request #1315 from ghost/ordered-perpendicular

This commit is contained in:
Hakan Seven committed 2026-09-17 07:00:52 +03:00
commit 7cd62a68f7
10 files changed
+809 -50

No files matched your search

+65
View File
@@ -3188,6 +3188,71 @@ impl OpenCADStudio {
self.commit_undo_delta(i, pd);
}
}
CmdResult::AddPerpendicularConstraint {
first,
second,
first_fixed,
second_start,
label,
} => {
use crate::scene::parametric_constraints::ConstraintKind;
let refs = vec![first, second];
if let Err(message) = self.tabs[i].scene.validate_parametric_constraint(
ConstraintKind::Perpendicular,
&refs,
None,
) {
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.command_line.push_error(message);
return Task::none();
}
let scope = self.tabs[i].current_parametric_scope();
let constraints_before = self.tabs[i]
.scene
.parametric_constraint_set(scope)
.cloned()
.unwrap_or_else(|| {
crate::scene::parametric_constraints::ParametricConstraintSet::new(scope)
});
let touched = if first.entity == second.entity {
vec![first.entity]
} else {
vec![first.entity, second.entity]
};
let pending = self.begin_undo(i, label, touched.len(), true);
self.tabs[i]
.scene
.record_undo_parametric_constraints_before(scope, constraints_before);
let id = self.tabs[i].scene.parametric_constraint_set_mut(scope).add(
ConstraintKind::Perpendicular,
refs,
None,
);
self.tabs[i].scene.note_parametric_constraint_applied(
scope,
id,
self.constraint_bar_display,
);
let changes = touched
.into_iter()
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
.collect::<Vec<_>>();
self.tabs[i].scene.bump_entities_with_parametric_policy(
&changes,
&[first_fixed, second_start],
true,
);
self.tabs[i].dirty = true;
self.tabs[i].active_cmd = None;
self.tabs[i].snap_result = None;
self.command_line.push_output("Constraint applied.");
self.refresh_properties();
if let Some(pd) = pending {
self.commit_undo_delta(i, pd);
}
}
CmdResult::AddCoincidentConstraint {
first,
second,
+40 -4
View File
@@ -1366,7 +1366,46 @@ impl OpenCADStudio {
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
}
"PCONSTRAINT" | "QCONSTRAINT" | "ECONSTRAINT" | "TCONSTRAINT" | "LCONSTRAINT"
"QCONSTRAINT" | "GCPERPENDICULAR" => {
use crate::command::CmdResult;
use crate::modules::parametric::PerpendicularConstraintCommand;
let handles = self.tabs[i].scene.selected_handles_in_order();
if handles.is_empty() {
let command = PerpendicularConstraintCommand::new();
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
} else if handles.len() != 2 {
self.command_line
.push_output("Select exactly two perpendicular-compatible objects.");
} else {
let picks = handles
.iter()
.filter_map(|handle| {
let entity = self.tabs[i].scene.document.get_entity(*handle)?;
PerpendicularConstraintCommand::preselected_reference(entity, *handle)
})
.collect::<Vec<_>>();
if picks.len() == 2 && picks[0].reference != picks[1].reference {
return Some(self.apply_cmd_result(CmdResult::AddPerpendicularConstraint {
first: picks[0].reference,
second: picks[1].reference,
first_fixed: picks[0].fixed_reference,
second_start: picks[1].start_reference,
label: "Perpendicular constraint",
}));
}
self.tabs[i].scene.deselect_all();
self.command_line.push_error(
"Invalid selection for Perpendicular. Select a line segment, polyline segment, text, MText, major or minor axis of ellipse or elliptical arc.",
);
let command = PerpendicularConstraintCommand::new();
self.command_line.push_info(&command.prompt());
self.tabs[i].active_cmd = Some(Box::new(command));
}
}
"PCONSTRAINT" | "ECONSTRAINT" | "TCONSTRAINT" | "LCONSTRAINT"
| "NRCONSTRAINT" => {
let handles = self.tabs[i].scene.selected_handles_in_order();
if handles.is_empty() {
@@ -1383,9 +1422,6 @@ impl OpenCADStudio {
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
let (kind, label) = match cmd {
"PCONSTRAINT" => (ConstraintKind::Parallel, "Parallel constraint"),
"QCONSTRAINT" => {
(ConstraintKind::Perpendicular, "Perpendicular constraint")
}
"TCONSTRAINT" => (ConstraintKind::Tangent, "Tangent constraint"),
"LCONSTRAINT" => (ConstraintKind::Colinear, "Colinear constraint"),
"NRCONSTRAINT" => (ConstraintKind::Normal, "Normal constraint"),
+11
View File
@@ -1477,6 +1477,16 @@ pub enum CmdResult {
/// Undo-history label, e.g. `"Horizontal constraint"`.
label: &'static str,
},
/// Adds an ordered perpendicular relation. The first picked direction and
/// the second direction's start point stay fixed during the initial solve;
/// those temporary anchors are not persisted as geometric constraints.
AddPerpendicularConstraint {
first: crate::scene::parametric_constraints::ParametricRef,
second: crate::scene::parametric_constraints::ParametricRef,
first_fixed: crate::scene::parametric_constraints::ParametricRef,
second_start: crate::scene::parametric_constraints::ParametricRef,
label: &'static str,
},
/// Opens the Auto Constrain settings dialog from the selection prompt.
OpenAutoConstrainSettings,
/// Adds an ordered Coincident relation. Point/point selections create a
@@ -2726,6 +2736,7 @@ mod constraint_registry_tests {
"VCONSTRAINT",
"PCONSTRAINT",
"QCONSTRAINT",
"GCPERPENDICULAR",
"ECONSTRAINT",
"TCONSTRAINT",
"NCONSTRAINT",
+1 -1
View File
@@ -16,7 +16,7 @@ impl GeomConstraintCommand {
Some(match keyword {
"H" | "HORIZONTAL" => "HCONSTRAINT",
"V" | "VERTICAL" => "VCONSTRAINT",
"P" | "PERPENDICULAR" => "QCONSTRAINT",
"P" | "PERPENDICULAR" => "GCPERPENDICULAR",
"PA" | "PARALLEL" => "PCONSTRAINT",
"T" | "TANGENT" => "TCONSTRAINT",
"SM" | "SMOOTH" => "SMOOTHCONSTRAINT",
+3
View File
@@ -5,12 +5,15 @@
mod coincident;
mod equal_distance;
mod geom_constraint;
#[path = "perpendicular.rs"]
mod perpendicular_command;
mod point_on_entity;
mod tools;
mod value;
pub use coincident::{coincident_tool, CoincidentConstraintCommand};
pub use equal_distance::{equal_distance_tool, EqualDistanceConstraintCommand};
pub use geom_constraint::GeomConstraintCommand;
pub use perpendicular_command::{PerpendicularConstraintCommand, PerpendicularPick};
pub use point_on_entity::{
center_point_tool, midpoint_tool, point_on_curve_tool, PointOnEntityConstraintCommand,
};
+235
View File
@@ -0,0 +1,235 @@
use acadrust::{EntityType, Handle};
use glam::DVec3;
use crate::command::{CadCommand, CmdResult};
use crate::scene::parametric_constraints::{directional_axis_endpoints, ParametricRef};
use crate::t;
#[derive(Clone, Copy)]
pub struct PerpendicularPick {
pub reference: ParametricRef,
pub fixed_reference: ParametricRef,
pub start_reference: ParametricRef,
}
pub struct PerpendicularConstraintCommand {
first: Option<PerpendicularPick>,
picked_entity: Option<EntityType>,
}
impl PerpendicularConstraintCommand {
pub fn new() -> Self {
Self {
first: None,
picked_entity: None,
}
}
fn invalid_selection() -> CmdResult {
CmdResult::ReportError(
t!("Invalid selection for Perpendicular. Select a line segment, polyline segment, text, MText, major or minor axis of ellipse or elliptical arc.")
.into_owned(),
)
}
fn distance_to_axis(point: DVec3, endpoints: [acadrust::types::Vector3; 2]) -> f64 {
let start = DVec3::new(endpoints[0].x, endpoints[0].y, endpoints[0].z);
let end = DVec3::new(endpoints[1].x, endpoints[1].y, endpoints[1].z);
let direction = end - start;
if direction.length_squared() <= 1.0e-24 {
return f64::INFINITY;
}
(point - start).cross(direction.normalize()).length()
}
fn ellipse_reference(entity: &EntityType, handle: Handle, point: DVec3) -> Option<ParametricRef> {
let major = ParametricRef::ellipse_major_axis(handle);
let minor = ParametricRef::ellipse_minor_axis(handle);
let major_distance = Self::distance_to_axis(point, directional_axis_endpoints(entity, major)?);
let minor_distance = Self::distance_to_axis(point, directional_axis_endpoints(entity, minor)?);
Some(if minor_distance < major_distance { minor } else { major })
}
fn picked_reference(entity: &EntityType, handle: Handle, point: DVec3) -> Option<PerpendicularPick> {
let (reference, fixed_reference, start_reference) = match entity {
EntityType::Line(_) => (
ParametricRef::whole(handle),
ParametricRef::whole(handle),
ParametricRef::point(handle, 0),
),
EntityType::LwPolyline(_) | EntityType::Polyline2D(_) => {
let (source, _, _) = crate::scene::centerline::picked_source(entity, handle, point)?;
let index = usize::try_from(source.segment_index).ok()?;
(
ParametricRef::segment(handle, index),
ParametricRef::segment(handle, index),
ParametricRef::point(handle, index as i32),
)
}
EntityType::Text(_) | EntityType::MText(_) => (
ParametricRef::text_baseline(handle),
ParametricRef::whole(handle),
ParametricRef::point(handle, 0),
),
EntityType::Ellipse(_) => (
Self::ellipse_reference(entity, handle, point)?,
ParametricRef::whole(handle),
ParametricRef::center(handle),
),
_ => return None,
};
Some(PerpendicularPick {
reference,
fixed_reference,
start_reference,
})
}
pub fn preselected_reference(entity: &EntityType, handle: Handle) -> Option<PerpendicularPick> {
match entity {
EntityType::Line(_) | EntityType::Text(_) | EntityType::MText(_) => {
Self::picked_reference(entity, handle, DVec3::ZERO)
}
EntityType::Ellipse(_) => Some(PerpendicularPick {
reference: ParametricRef::ellipse_major_axis(handle),
fixed_reference: ParametricRef::whole(handle),
start_reference: ParametricRef::center(handle),
}),
EntityType::LwPolyline(polyline) => {
let segment_count = if polyline.is_closed {
polyline.vertices.len()
} else {
polyline.vertices.len().saturating_sub(1)
};
(segment_count == 1 && polyline.vertices.first()?.bulge.abs() <= 1.0e-12)
.then_some(PerpendicularPick {
reference: ParametricRef::segment(handle, 0),
fixed_reference: ParametricRef::segment(handle, 0),
start_reference: ParametricRef::point(handle, 0),
})
}
EntityType::Polyline2D(polyline) => {
let segment_count = if polyline.is_closed() {
polyline.vertices.len()
} else {
polyline.vertices.len().saturating_sub(1)
};
(segment_count == 1 && polyline.vertices.first()?.bulge.abs() <= 1.0e-12)
.then_some(PerpendicularPick {
reference: ParametricRef::segment(handle, 0),
fixed_reference: ParametricRef::segment(handle, 0),
start_reference: ParametricRef::point(handle, 0),
})
}
_ => None,
}
}
}
impl CadCommand for PerpendicularConstraintCommand {
fn name(&self) -> &'static str {
"GCPERPENDICULAR"
}
fn prompt(&self) -> String {
if self.first.is_some() {
t!("GCPERPENDICULAR Select second object:").into_owned()
} else {
t!("GCPERPENDICULAR Select first object:").into_owned()
}
}
fn needs_entity_pick(&self) -> bool {
true
}
fn entity_pick_highlights_hover(&self) -> bool {
true
}
fn inject_before_entity_pick(&self) -> bool {
true
}
fn inject_picked_entity(&mut self, entity: EntityType) {
self.picked_entity = Some(entity);
}
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
if handle.is_null() {
return CmdResult::NeedPoint;
}
let Some(entity) = self.picked_entity.take() else {
return CmdResult::NeedPoint;
};
let Some(pick) = Self::picked_reference(&entity, handle, point) else {
return Self::invalid_selection();
};
let Some(first) = self.first else {
self.first = Some(pick);
return CmdResult::NeedPoint;
};
if first.reference == pick.reference {
return Self::invalid_selection();
}
CmdResult::AddPerpendicularConstraint {
first: first.reference,
second: pick.reference,
first_fixed: first.fixed_reference,
second_start: pick.start_reference,
label: "Perpendicular constraint",
}
}
fn on_point(&mut self, _point: DVec3) -> CmdResult {
CmdResult::NeedPoint
}
fn on_enter(&mut self) -> CmdResult {
CmdResult::Cancel
}
fn on_escape(&mut self) -> CmdResult {
CmdResult::Cancel
}
}
#[cfg(test)]
mod tests {
use super::*;
use acadrust::entities::Line;
use acadrust::types::Vector3;
#[test]
fn command_preserves_pick_order_for_the_solver() {
let first = Handle::new(7);
let second = Handle::new(9);
let mut command = PerpendicularConstraintCommand::new();
command.inject_picked_entity(EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::new(4.0, 0.0, 0.0),
)));
assert!(matches!(
command.on_entity_pick(first, DVec3::ZERO),
CmdResult::NeedPoint
));
command.inject_picked_entity(EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::new(1.0, 2.0, 0.0),
)));
let CmdResult::AddPerpendicularConstraint {
first: first_ref,
second: second_ref,
first_fixed,
second_start,
..
} = command.on_entity_pick(second, DVec3::ZERO)
else {
panic!("second pick must create the constraint");
};
assert_eq!(first_ref, ParametricRef::whole(first));
assert_eq!(second_ref, ParametricRef::whole(second));
assert_eq!(first_fixed, ParametricRef::whole(first));
assert_eq!(second_start, ParametricRef::point(second, 0));
}
}
+2 -1
View File
@@ -53,7 +53,7 @@ pub mod perpendicular {
icon: IconKind::Svg(include_bytes!(
"../../../assets/icons/constrain/perpendicular.svg"
)),
event: ModuleEvent::Command("QCONSTRAINT".to_string()),
event: ModuleEvent::Command("GCPERPENDICULAR".to_string()),
}
}
}
@@ -164,6 +164,7 @@ inventory::submit!(crate::command::CommandRegistration {
"VCONSTRAINT",
"PCONSTRAINT",
"QCONSTRAINT",
"GCPERPENDICULAR",
"ECONSTRAINT",
"TCONSTRAINT",
"NCONSTRAINT",
+110 -1
View File
@@ -43,6 +43,7 @@ struct EntityNodes {
geometry_node_id: i32,
points: FxHashMap<i32, i32>,
segments: FxHashMap<usize, i32>,
axes: FxHashMap<i32, i32>,
}
/// Builds one scope's `Assoc2dConstraintGroup` graph incrementally,
@@ -396,6 +397,52 @@ impl<'a> GroupBuilder<'a> {
Some(node_id)
}
/// Returns a derived line node for a text baseline or ellipse axis. These
/// are real native curve nodes tied to the source entity dependency, so
/// the selected direction survives save/reopen instead of collapsing to
/// the entity insertion/center point.
fn directional_axis_node(&mut self, reference: ParametricRef) -> Option<i32> {
let marker = reference.marker?;
reference.directional_axis()?;
if let Some(node_id) = self
.entities
.get(&reference.entity)
.and_then(|entity| entity.axes.get(&marker))
{
return Some(*node_id);
}
let entity = self.document.get_entity(reference.entity)?;
let [start, end] =
super::parametric_constraints::directional_axis_endpoints(entity, reference)?;
let delta = end - start;
if delta.length_squared() <= 1.0e-24 {
return None;
}
let node_id = self.alloc_node_id();
self.push_node(
node_id,
"AcConstrainedBoundedLine",
AssocConstraintNodeData::BoundedLine {
geometry_dependency: Handle::NULL,
geometry_node_id: node_id,
point: start,
direction: delta.normalize(),
is_ray: false,
start_point: start,
end_point: end,
},
);
self.entities
.entry(reference.entity)
.or_default()
.axes
.insert(marker, node_id);
if !self.referenced_entities.contains(&reference.entity) {
self.referenced_entities.push(reference.entity);
}
Some(node_id)
}
/// Returns the point-node id for a line endpoint or curve center,
/// creating it the first time the marker is referenced.
fn point_node(&mut self, handle: Handle, marker: i32) -> Option<i32> {
@@ -498,6 +545,9 @@ impl<'a> GroupBuilder<'a> {
/// The node id `ParametricRef` resolves to: a point node for a marked
/// reference, the whole geometry node otherwise.
fn ref_node(&mut self, r: ParametricRef) -> Option<i32> {
if r.directional_axis().is_some() {
return self.directional_axis_node(r);
}
if r.segment_midpoint_index().is_some() {
return self.point_node(r.entity, r.marker?);
}
@@ -1553,6 +1603,10 @@ fn dependency_entity(
),
AssocConstraintNodeData::Point { .. },
) => true,
(
Some(EntityType::Text(_) | EntityType::MText(_) | EntityType::Ellipse(_)),
AssocConstraintNodeData::BoundedLine { .. },
) => true,
(Some(EntityType::Line(_)), AssocConstraintNodeData::BoundedLine { is_ray, .. }) => !is_ray,
(
Some(EntityType::LwPolyline(_) | EntityType::Polyline2D(_)),
@@ -1643,6 +1697,52 @@ fn polyline_segment_reference(
.map(|index| ParametricRef::segment(entity, index))
}
fn directional_axis_reference(
document: &CadDocument,
entity: Handle,
data: &AssocConstraintNodeData,
work_plane: &[Vector3; 3],
) -> Option<ParametricRef> {
let AssocConstraintNodeData::BoundedLine {
start_point,
end_point,
..
} = data
else {
return None;
};
match document.get_entity(entity)? {
EntityType::Text(_) | EntityType::MText(_) => {
Some(ParametricRef::text_baseline(entity))
}
EntityType::Ellipse(ellipse) => {
let [origin, axis_x, axis_y] = *work_plane;
let normal = Vector3::new(
axis_x.y * axis_y.z - axis_x.z * axis_y.y,
axis_x.z * axis_y.x - axis_x.x * axis_y.z,
axis_x.x * axis_y.y - axis_x.y * axis_y.x,
);
let to_world = |point: Vector3| {
origin + axis_x * point.x + axis_y * point.y + normal * point.z
};
let direction = (to_world(*end_point) - to_world(*start_point)).normalize();
let major = ellipse.major_axis.normalize();
let ellipse_normal = ellipse.normal.normalize();
let minor = Vector3::new(
ellipse_normal.y * major.z - ellipse_normal.z * major.y,
ellipse_normal.z * major.x - ellipse_normal.x * major.z,
ellipse_normal.x * major.y - ellipse_normal.y * major.x,
);
if direction.dot(&minor).abs() > direction.dot(&major).abs() {
Some(ParametricRef::ellipse_minor_axis(entity))
} else {
Some(ParametricRef::ellipse_major_axis(entity))
}
}
_ => None,
}
}
fn numeric_value(value: &AssocEvalVariant) -> Option<f64> {
match value.value {
AssocEvalValue::Real(value) => Some(value),
@@ -1927,6 +2027,14 @@ pub(super) fn native_constraint_set(
if let Some(entity) = dependency_entity(document, dependency, &node.data) {
let reference =
polyline_segment_reference(document, entity, &node.data, &group.work_plane)
.or_else(|| {
directional_axis_reference(
document,
entity,
&node.data,
&group.work_plane,
)
})
.unwrap_or_else(|| {
if matches!(node.data, AssocConstraintNodeData::Point { .. }) {
ParametricRef::point(entity, 0)
@@ -2337,7 +2445,7 @@ fn materialize_scope(
continue;
};
if entity_nodes.geometry_node_id == 0 {
if entity_nodes.segments.is_empty() {
if entity_nodes.segments.is_empty() && entity_nodes.axes.is_empty() {
continue;
}
}
@@ -2346,6 +2454,7 @@ fn materialize_scope(
for node_id in std::iter::once(entity_nodes.geometry_node_id)
.filter(|node_id| *node_id != 0)
.chain(entity_nodes.segments.values().copied())
.chain(entity_nodes.axes.values().copied())
{
if let Some(node) = nodes.iter_mut().find(|node| node.node_id == node_id) {
set_geometry_dependency(&mut node.data, dep_handle);
+117 -1
View File
@@ -28,6 +28,16 @@ pub struct ParametricRef {
const POLYLINE_SEGMENT_MARKER_BASE: i32 = -1_000_000;
const POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE: i32 = -2_000_000;
const ELLIPSE_MAJOR_AXIS_MARKER: i32 = -4;
const ELLIPSE_MINOR_AXIS_MARKER: i32 = -5;
const TEXT_BASELINE_MARKER: i32 = -6;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DirectionalAxis {
TextBaseline,
EllipseMajor,
EllipseMinor,
}
impl ParametricRef {
pub fn whole(entity: Handle) -> Self {
@@ -81,6 +91,99 @@ impl ParametricRef {
(marker <= POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE)
.then(|| (POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE - marker) as usize)
}
/// Select the displayed baseline of a Text or MText entity.
pub fn text_baseline(entity: Handle) -> Self {
Self {
entity,
marker: Some(TEXT_BASELINE_MARKER),
}
}
/// Select the major axis of an ellipse or elliptical arc.
pub fn ellipse_major_axis(entity: Handle) -> Self {
Self {
entity,
marker: Some(ELLIPSE_MAJOR_AXIS_MARKER),
}
}
/// Select the minor axis of an ellipse or elliptical arc.
pub fn ellipse_minor_axis(entity: Handle) -> Self {
Self {
entity,
marker: Some(ELLIPSE_MINOR_AXIS_MARKER),
}
}
pub(crate) fn directional_axis(self) -> Option<DirectionalAxis> {
match self.marker? {
TEXT_BASELINE_MARKER => Some(DirectionalAxis::TextBaseline),
ELLIPSE_MAJOR_AXIS_MARKER => Some(DirectionalAxis::EllipseMajor),
ELLIPSE_MINOR_AXIS_MARKER => Some(DirectionalAxis::EllipseMinor),
_ => None,
}
}
}
/// The finite guide used to pick, display and serialize a directional text or
/// ellipse reference. Constraint equations treat the guide as an infinite
/// line; its finite length only makes selection and native persistence stable.
pub(crate) fn directional_axis_endpoints(
entity: &acadrust::EntityType,
reference: ParametricRef,
) -> Option<[Vector3; 2]> {
match (entity, reference.directional_axis()?) {
(acadrust::EntityType::Text(text), DirectionalAxis::TextBaseline) => {
use acadrust::entities::TextHorizontalAlignment as Alignment;
if matches!(
text.horizontal_alignment,
Alignment::Aligned | Alignment::Fit
) {
if let Some(end) = text.alignment_point.filter(|end| {
(*end - text.insertion_point).length_squared() > 1.0e-18
}) {
return Some([text.insertion_point, end]);
}
}
let length = text.height.abs().max(1.0);
Some([
text.insertion_point,
text.insertion_point
+ Vector3::new(text.rotation.cos(), text.rotation.sin(), 0.0) * length,
])
}
(acadrust::EntityType::MText(text), DirectionalAxis::TextBaseline) => {
let length = text.height.abs().max(1.0);
Some([
text.insertion_point,
text.insertion_point
+ Vector3::new(text.rotation.cos(), text.rotation.sin(), 0.0) * length,
])
}
(acadrust::EntityType::Ellipse(ellipse), axis) => {
let major_length = ellipse.major_axis.length();
if major_length <= 1.0e-12 {
return None;
}
let vector = match axis {
DirectionalAxis::EllipseMajor => ellipse.major_axis,
DirectionalAxis::EllipseMinor => {
let major = ellipse.major_axis / major_length;
let normal = ellipse.normal.normalize();
Vector3::new(
normal.y * major.z - normal.z * major.y,
normal.z * major.x - normal.x * major.z,
normal.x * major.y - normal.y * major.x,
) * (major_length * ellipse.minor_axis_ratio)
}
DirectionalAxis::TextBaseline => return None,
};
(vector.length_squared() > 1.0e-24).then_some([ellipse.center, ellipse.center + vector])
}
_ => None,
}
}
/// Grabbed points are exact kernel inputs; the solver anchors the remaining
@@ -702,6 +805,11 @@ fn glyph_placement_for_reference(
.unwrap_or(Vector3::UNIT_Y)
};
let line_normal = |line: &acadrust::entities::Line| segment_normal(line.start, line.end);
if r.directional_axis().is_some() {
let [start, end] = directional_axis_endpoints(entity, r)?;
let anchor = (start + end) * 0.5;
return Some((anchor, segment_normal(start, end)));
}
if let Some(segment) = r.segment_index() {
let anchor = resolve_point(
entity,
@@ -826,7 +934,15 @@ fn constraint_reference_curve_xy(
document: &acadrust::CadDocument,
reference: ParametricRef,
) -> Option<cadkernel::geom2d::Curve> {
let curve = crate::entities::curve::entity_curve_xy(document.get_entity(reference.entity)?)?;
let entity = document.get_entity(reference.entity)?;
if reference.directional_axis().is_some() {
let [start, end] = directional_axis_endpoints(entity, reference)?;
return Some(cadkernel::geom2d::Curve::Line(cadkernel::geom2d::Line {
start: [start.x, start.y],
end: [end.x, end.y],
}));
}
let curve = crate::entities::curve::entity_curve_xy(entity)?;
reference
.segment_index()
.map(|index| curve.segments().into_iter().nth(index))
+225 -42
View File
@@ -41,6 +41,10 @@ use super::{ChangeKind, Scene};
#[derive(Clone)]
enum EntityGeom {
Point(GPoint),
/// A text insertion point plus its displayed baseline direction. The
/// second point is a solver guide only; write-back converts it to the
/// entity's rotation/alignment fields.
TextLine(GLine),
Line(GLine),
Ray(GLine),
XLine(GLine),
@@ -78,6 +82,7 @@ impl EntityGeom {
fn point_for_marker(&self, marker: i32) -> Option<GPoint> {
match (self, marker) {
(EntityGeom::Point(point), 0) => Some(*point),
(EntityGeom::TextLine(line), 0) => Some(line.p1),
(EntityGeom::Line(l), 0) => Some(l.p1),
(EntityGeom::Line(l), 1) => Some(l.p2),
(EntityGeom::Ray(l), 0) => Some(l.p1),
@@ -142,7 +147,10 @@ fn as_circle_or_line(g: EntityGeom, reference: ParametricRef) -> CircleOrLine {
match g {
EntityGeom::Circle(c) => CircleOrLine::Circle(c),
EntityGeom::Arc(a) => CircleOrLine::Circle(a.circle),
EntityGeom::Line(l) | EntityGeom::Ray(l) | EntityGeom::XLine(l) => CircleOrLine::Line(l),
EntityGeom::TextLine(l)
| EntityGeom::Line(l)
| EntityGeom::Ray(l)
| EntityGeom::XLine(l) => CircleOrLine::Line(l),
EntityGeom::Ellipse(ellipse) => CircleOrLine::Ellipse(ellipse),
EntityGeom::Point(_) => CircleOrLine::Other,
EntityGeom::Spline { .. } => CircleOrLine::Other,
@@ -185,14 +193,20 @@ fn register_entity(
sys.add_param(insert.insert_point.x, false),
sys.add_param(insert.insert_point.y, false),
))),
EntityType::Text(text) => Some(EntityGeom::Point(GPoint::new(
sys.add_param(text.insertion_point.x, false),
sys.add_param(text.insertion_point.y, false),
))),
EntityType::MText(text) => Some(EntityGeom::Point(GPoint::new(
sys.add_param(text.insertion_point.x, false),
sys.add_param(text.insertion_point.y, false),
))),
EntityType::Text(_) | EntityType::MText(_) => {
let reference = ParametricRef::text_baseline(handle);
let [start, end] = super::parametric_constraints::directional_axis_endpoints(
entity,
reference,
)?;
Some(EntityGeom::TextLine(GLine {
p1: GPoint::new(
sys.add_param(start.x, false),
sys.add_param(start.y, false),
),
p2: GPoint::new(sys.add_param(end.x, false), sys.add_param(end.y, false)),
}))
}
EntityType::AttributeDefinition(attribute) => Some(EntityGeom::Point(GPoint::new(
sys.add_param(attribute.insertion_point.x, false),
sys.add_param(attribute.insertion_point.y, false),
@@ -876,6 +890,7 @@ fn build_constraint(
EntityGeom::Circle(circ) => Some(circ),
EntityGeom::Arc(a) => Some(a.circle),
EntityGeom::Point(_)
| EntityGeom::TextLine(_)
| EntityGeom::Line(_)
| EntityGeom::Ray(_)
| EntityGeom::XLine(_)
@@ -957,16 +972,53 @@ fn build_constraint(
let [a, b] = c.refs.as_slice() else {
return Vec::new();
};
let (Some(fixed), Some(moving)) =
(whole_line(sys, cache, *a), whole_line(sys, cache, *b))
let directional_line =
|sys: &mut System, cache: &mut HashMap<_, _>, r: ParametricRef| {
let geometry = resolve_ref(document, sys, cache, r)?;
let minor_axis = matches!(
r.directional_axis(),
Some(super::parametric_constraints::DirectionalAxis::EllipseMinor)
);
let line = match geometry {
EntityGeom::TextLine(line)
if matches!(
r.directional_axis(),
Some(super::parametric_constraints::DirectionalAxis::TextBaseline)
) => line,
EntityGeom::Line(line) | EntityGeom::Ray(line) | EntityGeom::XLine(line)
if r.marker.is_none() => line,
EntityGeom::Polyline { .. } => {
geometry.line_segment(r.segment_index()?)?
}
EntityGeom::Ellipse(ellipse)
if matches!(
r.directional_axis(),
Some(
super::parametric_constraints::DirectionalAxis::EllipseMajor
| super::parametric_constraints::DirectionalAxis::EllipseMinor
)
) => GLine {
p1: ellipse.center,
p2: ellipse.focus1,
},
_ => return None,
};
Some((line, minor_axis))
};
let (Some((fixed, fixed_minor)), Some((moving, moving_minor))) =
(directional_line(sys, cache, *a), directional_line(sys, cache, *b))
else {
return Vec::new();
};
vec![Rc::new(PerpendicularConstraint::new(
sys.store(),
moving,
fixed,
))]
if fixed_minor ^ moving_minor {
vec![Rc::new(ParallelConstraint::new(sys.store(), moving, fixed))]
} else {
vec![Rc::new(PerpendicularConstraint::new(
sys.store(),
moving,
fixed,
))]
}
}
ConstraintKind::Equal => {
let [a, b] = c.refs.as_slice() else {
@@ -1059,7 +1111,10 @@ fn build_constraint(
return Vec::new();
};
match geom {
EntityGeom::Line(l) | EntityGeom::Ray(l) | EntityGeom::XLine(l) => {
EntityGeom::TextLine(l)
| EntityGeom::Line(l)
| EntityGeom::Ray(l)
| EntityGeom::XLine(l) => {
vec![Rc::new(PointOnLine::new(p, l))]
}
EntityGeom::Circle(circ) => {
@@ -1200,7 +1255,10 @@ fn build_constraint(
})
.collect()
}
EntityGeom::Line(l) | EntityGeom::Ray(l) | EntityGeom::XLine(l) => {
EntityGeom::TextLine(l)
| EntityGeom::Line(l)
| EntityGeom::Ray(l)
| EntityGeom::XLine(l) => {
let (x1, y1, x2, y2) = {
let store = sys.store();
(
@@ -1731,6 +1789,12 @@ type SolveResult = (
fn retained_line_length(entity: &EntityType, segment: Option<usize>) -> Option<f64> {
match (entity, segment) {
(EntityType::Line(line), None) => Some((line.end - line.start).length()),
(EntityType::Text(_) | EntityType::MText(_), None) => {
let reference = ParametricRef::text_baseline(entity.common().handle);
let [start, end] =
super::parametric_constraints::directional_axis_endpoints(entity, reference)?;
Some((end - start).length())
}
(EntityType::LwPolyline(polyline), Some(index)) => {
let world = crate::entities::curve::lwpolyline_world_xy(polyline)?;
let a = world.vertices.get(index)?.location;
@@ -2027,7 +2091,7 @@ fn solve_scope(
if retain_size {
for (handle, geom) in &cache {
match geom {
EntityGeom::Line(line) if retain_lengths => {
EntityGeom::Line(line) | EntityGeom::TextLine(line) if retain_lengths => {
let current_length = {
let store = sys.store();
let dx = store.get(line.p2.x) - store.get(line.p1.x);
@@ -2088,6 +2152,26 @@ fn solve_scope(
let target = sys.add_param(radius, true);
sys.add_constraint(Rc::new(Equal::new(arc.circle.rad, target, 1.0)));
}
EntityGeom::Ellipse(ellipse) => {
let (focus_distance, minor_radius) = {
let store = sys.store();
let dx = store.get(ellipse.focus1.x) - store.get(ellipse.center.x);
let dy = store.get(ellipse.focus1.y) - store.get(ellipse.center.y);
((dx * dx + dy * dy).sqrt(), store.get(ellipse.radmin))
};
let focus_target = sys.add_param(focus_distance, true);
let minor_target = sys.add_param(minor_radius, true);
sys.add_constraint(Rc::new(P2PDistance::new(
ellipse.center,
ellipse.focus1,
focus_target,
)));
sys.add_constraint(Rc::new(Equal::new(
ellipse.radmin,
minor_target,
1.0,
)));
}
_ => {}
}
}
@@ -2156,13 +2240,10 @@ fn solve_scope(
// `focus1 - center` (which is what `major_axis` direction/length and
// `minor_axis_ratio` are actually computed from on write-back) changes
// anyway, making the ellipse appear to rotate and reshape even though
// no shape parameter was itself pulled on. Pinning `focus1 - center` to
// its pre-solve (seed) value forces `focus1` to translate rigidly with
// `center`, keeping orientation/eccentricity invariant unless something
// actually constrains `focus1`/`radmin` directly (only `Fixed` does
// today). `radmin` needs no equivalent rule — as a lone scalar it isn't
// coupled to `center`'s movement, so it already stays put on its own.
for geom in cache.values() {
// no shape parameter was itself pulled on. Most ellipse relations keep
// that vector exact. A directional axis relation instead preserves its
// length while allowing the vector to rotate into the requested angle.
for (handle, geom) in &cache {
let EntityGeom::Ellipse(el) = geom else {
continue;
};
@@ -2175,10 +2256,67 @@ fn solve_scope(
store.get(el.focus1.y),
)
};
let dx = sys.add_param(fx - cx, true);
let dy = sys.add_param(fy - cy, true);
sys.add_constraint(Rc::new(Difference::new(el.center.x, el.focus1.x, dx)));
sys.add_constraint(Rc::new(Difference::new(el.center.y, el.focus1.y, dy)));
let directional = set.constraints.iter().any(|constraint| {
constraint.enabled
&& constraint.kind == ConstraintKind::Perpendicular
&& constraint.refs.iter().any(|reference| {
reference.entity == *handle && reference.directional_axis().is_some()
})
});
if directional && !retain_size {
let distance = sys.add_param((fx - cx).hypot(fy - cy), true);
sys.add_constraint(Rc::new(P2PDistance::new(
el.center,
el.focus1,
distance,
)));
let minor_value = sys.store().get(el.radmin);
let minor = sys.add_param(minor_value, true);
sys.add_constraint(Rc::new(Equal::new(el.radmin, minor, 1.0)));
} else if !directional {
let dx = sys.add_param(fx - cx, true);
let dy = sys.add_param(fy - cy, true);
sys.add_constraint(Rc::new(Difference::new(el.center.x, el.focus1.x, dx)));
sys.add_constraint(Rc::new(Difference::new(el.center.y, el.focus1.y, dy)));
}
}
// Text keeps its displayed baseline rigid during ordinary point edits.
// A perpendicular baseline reference keeps only the span length so the
// command can rotate the text about its insertion point.
for (handle, geom) in &cache {
let EntityGeom::TextLine(line) = geom else {
continue;
};
let (x1, y1, x2, y2) = {
let store = sys.store();
(
store.get(line.p1.x),
store.get(line.p1.y),
store.get(line.p2.x),
store.get(line.p2.y),
)
};
let directional = set.constraints.iter().any(|constraint| {
constraint.enabled
&& constraint.kind == ConstraintKind::Perpendicular
&& constraint.refs.iter().any(|reference| {
reference.entity == *handle
&& matches!(
reference.directional_axis(),
Some(super::parametric_constraints::DirectionalAxis::TextBaseline)
)
})
});
if directional && !retain_size {
let length = sys.add_param((x2 - x1).hypot(y2 - y1), true);
sys.add_constraint(Rc::new(P2PDistance::new(line.p1, line.p2, length)));
} else if !directional {
let dx = sys.add_param(x2 - x1, true);
let dy = sys.add_param(y2 - y1, true);
sys.add_constraint(Rc::new(Difference::new(line.p1.x, line.p2.x, dx)));
sys.add_constraint(Rc::new(Difference::new(line.p1.y, line.p2.y, dy)));
}
}
// Temporarily make each requested anchor a fixed kernel input. Marking the
@@ -2205,7 +2343,8 @@ fn solve_scope(
} else {
match resolve_ref(document, &mut sys, &mut cache, *reference) {
Some(EntityGeom::Point(point)) => vec![point.x, point.y],
Some(EntityGeom::Line(line))
Some(EntityGeom::TextLine(line))
| Some(EntityGeom::Line(line))
| Some(EntityGeom::Ray(line))
| Some(EntityGeom::XLine(line)) => {
vec![line.p1.x, line.p1.y, line.p2.x, line.p2.y]
@@ -2261,7 +2400,10 @@ fn solve_scope(
.values()
.map(|g| match g {
EntityGeom::Point(_) => 2,
EntityGeom::Line(_) | EntityGeom::Ray(_) | EntityGeom::XLine(_) => 4,
EntityGeom::TextLine(_)
| EntityGeom::Line(_)
| EntityGeom::Ray(_)
| EntityGeom::XLine(_) => 4,
EntityGeom::Polyline { points, arcs, .. } => {
points.len() * 2 + arcs.iter().flatten().count() * 5
}
@@ -2343,31 +2485,72 @@ fn solve_scope(
results.push((handle, EntityType::Insert(updated)));
}
}
(EntityType::Text(text), EntityGeom::Point(geometry)) => {
(EntityType::Text(text), EntityGeom::TextLine(geometry)) => {
let mut updated = text.clone();
let next = Vector3::new(
store.get(geometry.x),
store.get(geometry.y),
store.get(geometry.p1.x),
store.get(geometry.p1.y),
text.insertion_point.z,
);
let end = Vector3::new(
store.get(geometry.p2.x),
store.get(geometry.p2.y),
text.insertion_point.z,
);
let delta = next - text.insertion_point;
if delta.length() > MOVE_EPS {
let direction = end - next;
let rotation = direction.y.atan2(direction.x);
let aligned = matches!(
text.horizontal_alignment,
acadrust::entities::TextHorizontalAlignment::Aligned
| acadrust::entities::TextHorizontalAlignment::Fit
);
let current_rotation = if aligned {
text.alignment_point
.filter(|point| (*point - text.insertion_point).length_squared() > 1.0e-18)
.map(|point| {
(point.y - text.insertion_point.y)
.atan2(point.x - text.insertion_point.x)
})
.unwrap_or(text.rotation)
} else {
text.rotation
};
if delta.length() > MOVE_EPS
|| (rotation - current_rotation).abs() > MOVE_EPS
{
updated.insertion_point = next;
if let Some(alignment) = &mut updated.alignment_point {
*alignment = *alignment + delta;
if aligned {
updated.alignment_point = Some(end);
} else {
if let Some(alignment) = &mut updated.alignment_point {
*alignment = *alignment + delta;
}
updated.rotation = rotation;
}
results.push((handle, EntityType::Text(updated)));
}
}
(EntityType::MText(text), EntityGeom::Point(geometry)) => {
(EntityType::MText(text), EntityGeom::TextLine(geometry)) => {
let mut updated = text.clone();
let next = Vector3::new(
store.get(geometry.x),
store.get(geometry.y),
store.get(geometry.p1.x),
store.get(geometry.p1.y),
text.insertion_point.z,
);
if (next - text.insertion_point).length() > MOVE_EPS {
let dx = store.get(geometry.p2.x) - next.x;
let dy = store.get(geometry.p2.y) - next.y;
let rotation = dy.atan2(dx);
if (next - text.insertion_point).length() > MOVE_EPS
|| (rotation - text.rotation).abs() > MOVE_EPS
{
updated.insertion_point = next;
updated.rotation = rotation;
updated.dwg_x_direction = Some(Vector3::new(
rotation.cos(),
rotation.sin(),
0.0,
));
results.push((handle, EntityType::MText(updated)));
}
}