forked from 1b-agent/opencadstudio-upstream
Merge pull request #1315 from ghost/ordered-perpendicular
This commit is contained in:
commit
7cd62a68f7
10 files changed
+809
-50
No files matched your search
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user