forked from 1b-agent/opencadstudio-upstream
1) FXCONSTRAINT (and the new GCFIX name) prompts "Select point or [Object] <Object>:" — a pick fixes that constraint point; Enter or O asks for one line, polyline segment, circle, arc, ellipse or spline; misses and unsupported objects report the reference messages and re-prompt. 2) Point, block, text, MText, attribute and table insertion points are addressable constraint points. 3) Re-applying Fixed to the same point or object reports that the constraint already exists instead of adding a duplicate. 4) A persistent Fixed holds its pre-edit geometry through MOVE, ROTATE, SCALE, STRETCH, grip and Properties edits: fixed points stay, free points of a partly fixed entity follow the edit, and size retention skips fixed entities. 5) Object mode on a polyline fixes the picked segment (natively persisted) instead of the whole polyline; arc segments pin endpoints, center and radius. 6) Ribbon label "Fix" with a padlock icon. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2512 lines
97 KiB
Rust
2512 lines
97 KiB
Rust
//! Runtime parametric-constraint data and scope management.
|
|
|
|
use super::named_parameters::DrivingValue;
|
|
use acadrust::types::{Handle, Vector3};
|
|
|
|
/// One endpoint a constraint attaches to: an entity plus which sub-element
|
|
/// of it.
|
|
///
|
|
/// Reuses the GsMarker convention `AssocDimensionReference::main_gs_marker`
|
|
/// already carries for associative-dimension endpoints
|
|
/// (`src/scene/dimension_assoc.rs`), rather than inventing a second
|
|
/// sub-element addressing scheme: `marker` indexes into
|
|
/// [`dimension_assoc::source_points`](super::dimension_assoc::source_points)'s
|
|
/// ordered per-entity-type point list when non-negative (0/1 = a line's
|
|
/// start/end, ...), or names a special case when negative (-3 = a
|
|
/// circle/arc's center; -2 = a bounded curve's midpoint). Polyline segment
|
|
/// segment-midpoint, and curved-segment-center references use private
|
|
/// negative ranges so they stay distinct from vertex markers.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct ParametricRef {
|
|
pub entity: Handle,
|
|
/// `None` addresses the entity as a whole — what a Radius, Length, or
|
|
/// whole-curve constraint (Parallel, Perpendicular, Equal, Horizontal,
|
|
/// Vertical) needs; a point-level constraint (Coincident, Distance
|
|
/// between two points, Angle at a shared vertex) sets `Some(marker)`.
|
|
pub marker: Option<i32>,
|
|
}
|
|
|
|
const POLYLINE_SEGMENT_MARKER_BASE: i32 = -1_000_000;
|
|
const POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE: i32 = -2_000_000;
|
|
const POLYLINE_SEGMENT_CENTER_MARKER_BASE: i32 = -3_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 {
|
|
Self {
|
|
entity,
|
|
marker: None,
|
|
}
|
|
}
|
|
|
|
pub fn point(entity: Handle, marker: i32) -> Self {
|
|
Self {
|
|
entity,
|
|
marker: Some(marker),
|
|
}
|
|
}
|
|
|
|
/// The circle/arc-center special case (`marker == -3`), broken out as
|
|
/// its own constructor since `-3` alone reads as a magic number
|
|
/// everywhere it would otherwise appear.
|
|
pub fn center(entity: Handle) -> Self {
|
|
Self {
|
|
entity,
|
|
marker: Some(-3),
|
|
}
|
|
}
|
|
|
|
/// Select one straight segment of a polyline.
|
|
pub fn segment(entity: Handle, index: usize) -> Self {
|
|
Self {
|
|
entity,
|
|
marker: Some(POLYLINE_SEGMENT_MARKER_BASE - index as i32),
|
|
}
|
|
}
|
|
|
|
pub fn segment_index(self) -> Option<usize> {
|
|
let marker = self.marker?;
|
|
(marker <= POLYLINE_SEGMENT_MARKER_BASE && marker > POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE)
|
|
.then(|| (POLYLINE_SEGMENT_MARKER_BASE - marker) as usize)
|
|
}
|
|
|
|
/// Select the midpoint of one straight polyline segment.
|
|
pub fn segment_midpoint(entity: Handle, index: usize) -> Self {
|
|
Self {
|
|
entity,
|
|
marker: Some(POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE - index as i32),
|
|
}
|
|
}
|
|
|
|
pub fn segment_midpoint_index(self) -> Option<usize> {
|
|
let marker = self.marker?;
|
|
(marker <= POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE
|
|
&& marker > POLYLINE_SEGMENT_CENTER_MARKER_BASE)
|
|
.then(|| (POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE - marker) as usize)
|
|
}
|
|
|
|
/// Select the center of one curved polyline segment.
|
|
pub fn segment_center(entity: Handle, index: usize) -> Self {
|
|
Self {
|
|
entity,
|
|
marker: Some(POLYLINE_SEGMENT_CENTER_MARKER_BASE - index as i32),
|
|
}
|
|
}
|
|
|
|
pub fn segment_center_index(self) -> Option<usize> {
|
|
let marker = self.marker?;
|
|
(marker <= POLYLINE_SEGMENT_CENTER_MARKER_BASE)
|
|
.then(|| (POLYLINE_SEGMENT_CENTER_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
|
|
/// endpoint coordinates according to the line's directional constraints.
|
|
pub(crate) fn grip_solve_anchor_refs(
|
|
entity: &acadrust::EntityType,
|
|
handle: Handle,
|
|
grip_id: usize,
|
|
) -> Vec<ParametricRef> {
|
|
match entity {
|
|
acadrust::EntityType::Line(_) if grip_id <= 1 => {
|
|
vec![ParametricRef::point(handle, grip_id as i32)]
|
|
}
|
|
acadrust::EntityType::LwPolyline(polyline) if grip_id < polyline.vertices.len() => {
|
|
vec![ParametricRef::point(handle, grip_id as i32)]
|
|
}
|
|
acadrust::EntityType::LwPolyline(polyline) => {
|
|
let segment = grip_id - polyline.vertices.len();
|
|
if polyline.vertices.get(segment).is_some_and(|vertex| vertex.bulge.abs() < 1e-9)
|
|
&& (segment + 1 < polyline.vertices.len() || polyline.is_closed)
|
|
{
|
|
vec![ParametricRef::segment(handle, segment)]
|
|
} else {
|
|
Vec::new()
|
|
}
|
|
}
|
|
acadrust::EntityType::Polyline2D(polyline) if grip_id < polyline.vertices.len() => {
|
|
vec![ParametricRef::point(handle, grip_id as i32)]
|
|
}
|
|
acadrust::EntityType::Arc(_) => match grip_id {
|
|
0 => vec![ParametricRef::center(handle)],
|
|
1 => vec![ParametricRef::point(handle, 0)],
|
|
2 => vec![ParametricRef::point(handle, 1)],
|
|
_ => Vec::new(),
|
|
},
|
|
acadrust::EntityType::Circle(_) if grip_id == 0 => {
|
|
vec![ParametricRef::center(handle)]
|
|
}
|
|
acadrust::EntityType::Point(_)
|
|
| acadrust::EntityType::Insert(_)
|
|
| acadrust::EntityType::Text(_)
|
|
| acadrust::EntityType::MText(_)
|
|
if grip_id == 0 =>
|
|
{
|
|
vec![ParametricRef::point(handle, 0)]
|
|
}
|
|
_ => Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// The friendly, user-facing constraint types — the "what button did they
|
|
/// click" vocabulary, one layer above the `cadkernel_constraints` primitives each maps
|
|
/// onto (that mapping is `constraint_map`, a later stage; see the design
|
|
/// doc §2). Named and grouped the same way the existing one-shot ribbon
|
|
/// tools are (`crate::modules::parametric::tools`), plus the
|
|
/// endpoint-picking kinds (`Coincident`, `Radius`, `Tangent`) that one-shot
|
|
/// group never needed.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum ConstraintKind {
|
|
Coincident,
|
|
Horizontal,
|
|
Vertical,
|
|
Parallel,
|
|
Perpendicular,
|
|
Equal,
|
|
/// Distance between two points, or a single line's length, or a
|
|
/// circle/arc's diameter — which reading applies depends on `refs`'
|
|
/// shape, mirroring how `DistanceConstraintCommand`
|
|
/// (`src/modules/draw/constrain/value.rs`) already infers it from the
|
|
/// selected entity.
|
|
Distance,
|
|
Angle,
|
|
/// Angle defined by two rays sharing the middle point.
|
|
Angle3Point,
|
|
Radius,
|
|
Tangent,
|
|
/// An open spline endpoint remains curvature-continuous with another bounded curve endpoint.
|
|
Smooth,
|
|
/// Two circles/arcs share a center — `refs`: `[center(a), center(b)]`.
|
|
/// Solves identically to `Coincident` (`parametric_solve.rs` broadens that
|
|
/// match arm rather than duplicating it) — only the DWG-native class
|
|
/// name (`ACCONCENTRICCONSTRAINT` vs `ACPOINTCOINCIDENCECONSTRAINT`)
|
|
/// and the UI entry point differ.
|
|
Concentric,
|
|
/// A point sits at a circle/arc's center — `refs`: `[point, center(circle)]`.
|
|
/// Same solver math as `Coincident`/`Concentric`, different DWG class
|
|
/// name (`ACCENTERPOINTCONSTRAINT`).
|
|
CenterPoint,
|
|
/// Two lines share the same infinite line — `refs`: `[whole(a), whole(b)]`.
|
|
Colinear,
|
|
/// A point sits at another line's midpoint — `refs`: `[point, whole(line)]`.
|
|
Midpoint,
|
|
/// Locks a whole entity at its current position — `refs`: `[whole(entity)]`.
|
|
/// No `driving_param`: the target is the entity's own live geometry at
|
|
/// solve time, not a typed value (see `parametric_solve.rs`'s `Fixed` arm).
|
|
Fixed,
|
|
/// A point lies anywhere along a line's or circle's curve (not
|
|
/// restricted to an endpoint/center) — `refs`: `[point, whole(entity)]`.
|
|
PointOnCurve,
|
|
/// The distance between one point pair equals the distance between
|
|
/// another — `refs`: `[p1, p2, p3, p4]` (`dist(p1,p2) == dist(p3,p4)`).
|
|
/// For the supported entity types, equal curvature is already covered
|
|
/// by the circle/circle radius branch of `Equal`.
|
|
EqualDistance,
|
|
/// Two circles/arcs are mirror images of each other across a line —
|
|
/// `refs`: `[center(a), center(b), whole(mirror_line)]`. Scoped to the
|
|
/// circle-center-pair case (unambiguous with whole-entity selection);
|
|
/// point-symmetry about a point, and symmetry between two lines, aren't
|
|
/// modeled.
|
|
Symmetric,
|
|
/// A circle/arc's diameter (twice `Radius`'s target) — `refs`:
|
|
/// `[whole(circle_or_arc)]`. Same DWG class as `Radius`
|
|
/// (`ACRADIUSDIAMETERCONSTRAINT`), distinguished only by the
|
|
/// `RadiusDiameterConstrType` mode byte
|
|
/// (`dwg_native_constraints.rs`), rather than a distinct object type.
|
|
Diameter,
|
|
/// The X-only (resp. Y-only) component of the distance between two
|
|
/// points — `refs`: `[p1, p2]`, same shape as `Distance`. Same DWG
|
|
/// class as `Distance` (`ACDISTANCECONSTRAINT`) with its
|
|
/// `DirectionType` set to a fixed `(1,0,0)`/`(0,1,0)` direction.
|
|
DistanceX,
|
|
DistanceY,
|
|
/// Signed point-to-point distance along an arbitrary fixed direction,
|
|
/// or along a direction derived from a third line reference.
|
|
DistanceDirected,
|
|
/// A line perpendicular to a circle/arc's tangent at their point of
|
|
/// contact — `refs`: `[whole(a), whole(b)]`, either order. For the
|
|
/// Line/Circle-only entity model this is equivalent to "the line
|
|
/// passes through the circle's center" (a circle's radius is always
|
|
/// normal to its own tangent), so it solves via the same `PointOnLine`
|
|
/// primitive `PointOnCurve` already uses. Distinct from
|
|
/// `Perpendicular` (line-to-line only). Line-Line has no meaning here
|
|
/// (that's plain `Perpendicular`) and isn't buildable.
|
|
Normal,
|
|
/// A standard rigid geometry container. Its referenced points keep their
|
|
/// original relative distances while the whole set may translate or rotate.
|
|
RigidSet,
|
|
}
|
|
|
|
pub type ConstraintId = u32;
|
|
|
|
pub(crate) mod distance_direction_type {
|
|
pub const UNDIRECTED: u8 = 0;
|
|
pub const FIXED: u8 = 1;
|
|
pub const PARALLEL_TO_LINE: u8 = 2;
|
|
pub const PERPENDICULAR_TO_LINE: u8 = 3;
|
|
}
|
|
|
|
pub(crate) mod angle_sector {
|
|
pub const PARALLEL_COUNTERCLOCKWISE: u8 = 0;
|
|
pub const ANTIPARALLEL_CLOCKWISE: u8 = 1;
|
|
pub const PARALLEL_CLOCKWISE: u8 = 2;
|
|
pub const ANTIPARALLEL_COUNTERCLOCKWISE: u8 = 3;
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) struct NativeConstraintOrigin {
|
|
pub(crate) group: Handle,
|
|
pub(crate) node: i32,
|
|
}
|
|
|
|
/// One runtime constraint: a friendly [`ConstraintKind`], the
|
|
/// entities/points it relates, and — for a dimensional kind — the value
|
|
/// driving it.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct ParametricConstraint {
|
|
pub id: ConstraintId,
|
|
pub kind: ConstraintKind,
|
|
pub refs: Vec<ParametricRef>,
|
|
/// The target for a dimensional constraint (a `Distance`'s length, an
|
|
/// `Angle`'s degrees, a `Radius`'s radius) — a literal number or a
|
|
/// named-parameter reference resolved through `Scene::named_parameters` at solve time by
|
|
/// `parametric_solve::build_constraint`). `None` for every purely-geometric
|
|
/// kind (Coincident, Horizontal, Vertical, Parallel, Perpendicular,
|
|
/// Equal, Tangent).
|
|
pub driving_param: Option<DrivingValue>,
|
|
/// Lets a user suppress a constraint without losing it — a re-solve
|
|
/// skips a disabled constraint entirely.
|
|
pub enabled: bool,
|
|
/// Standard graph node retained in place because its group also contains
|
|
/// graph features that are not safe to rebuild independently.
|
|
pub(crate) native_origin: Option<NativeConstraintOrigin>,
|
|
/// Original point positions for a standard rigid set. This is rebuilt
|
|
/// from the associative graph on open and never stored separately.
|
|
pub(crate) rigid_points: Vec<(ParametricRef, Vector3)>,
|
|
/// Standard distance direction mode and vector. A third entry in `refs`
|
|
/// identifies the direction line for the line-relative modes.
|
|
pub(crate) distance_direction_type: u8,
|
|
pub(crate) distance_direction: Option<Vector3>,
|
|
/// World-space datum direction captured for Horizontal/Vertical. Native
|
|
/// files store the same vector on the connected constrained datum line.
|
|
/// `None` keeps legacy world-X/world-Y behavior.
|
|
pub(crate) axis_direction: Option<Vector3>,
|
|
/// Which of the four directed sectors an angular constraint measures.
|
|
pub(crate) angle_sector: u8,
|
|
}
|
|
|
|
/// A constraint scope: model space or one block definition.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum ParametricScope {
|
|
ModelSpace,
|
|
/// A block definition's `BlockRecord` handle — matches
|
|
/// `BlockEditSession::br_handle`
|
|
/// (`src/modules/draw/modify/block_edit.rs`).
|
|
Block(Handle),
|
|
}
|
|
|
|
impl ParametricScope {
|
|
/// The owner handle under which this scope is persisted.
|
|
pub fn owner_handle(&self, document: &acadrust::CadDocument) -> Handle {
|
|
match self {
|
|
ParametricScope::ModelSpace => document.header.model_space_block_handle,
|
|
ParametricScope::Block(handle) => *handle,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Every constraint decoded from one standard graph scope. Solver state is rebuilt
|
|
/// on demand and is not stored here.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ParametricConstraintSet {
|
|
pub scope: ParametricScope,
|
|
pub constraints: Vec<ParametricConstraint>,
|
|
next_id: ConstraintId,
|
|
/// Parameters owned by this standard block scope. Model-space and
|
|
/// command-created constraints continue to use the drawing parameter
|
|
/// table; block definitions need their own namespace because identical
|
|
/// parameter names may legitimately occur in different blocks.
|
|
pub(crate) local_parameters: super::named_parameters::ParameterTable,
|
|
pub(crate) retained_standard_groups: Vec<Handle>,
|
|
/// Cached total remaining degrees of freedom, summed across every
|
|
/// independent solve partition in this scope — updated by
|
|
/// `parametric_solve::solve_scope` each time this set is resolved. `None`
|
|
/// until the first resolve (e.g. right after loading from disk, before
|
|
/// any edit has touched this scope yet). The status badge reads this
|
|
/// rather than recomputing it every frame. Not persisted:
|
|
/// it's a derived cache, not real constraint state.
|
|
pub dof: Option<usize>,
|
|
/// Cached redundant/conflicting constraints found by the last resolve.
|
|
/// Also a derived cache, not
|
|
/// persisted; a `ConflictResolverPanel` reads this rather than calling
|
|
/// `cadkernel_constraints::diagnosis::classify_redundant` itself.
|
|
pub conflicts: Vec<(
|
|
ConstraintId,
|
|
cadkernel_constraints::diagnosis::RedundancyKind,
|
|
)>,
|
|
}
|
|
|
|
impl ParametricConstraintSet {
|
|
pub fn new(scope: ParametricScope) -> Self {
|
|
Self {
|
|
scope,
|
|
constraints: Vec::new(),
|
|
next_id: 0,
|
|
local_parameters: super::named_parameters::ParameterTable::new(),
|
|
retained_standard_groups: Vec::new(),
|
|
dof: None,
|
|
conflicts: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Appends a constraint, assigning it a fresh id unique within this set.
|
|
pub fn add(
|
|
&mut self,
|
|
kind: ConstraintKind,
|
|
refs: Vec<ParametricRef>,
|
|
driving_param: Option<DrivingValue>,
|
|
) -> ConstraintId {
|
|
let id = self.next_id;
|
|
self.next_id += 1;
|
|
self.constraints.push(ParametricConstraint {
|
|
id,
|
|
kind,
|
|
refs,
|
|
driving_param,
|
|
enabled: true,
|
|
native_origin: None,
|
|
rigid_points: Vec::new(),
|
|
distance_direction_type: 0,
|
|
distance_direction: None,
|
|
axis_direction: None,
|
|
angle_sector: angle_sector::PARALLEL_COUNTERCLOCKWISE,
|
|
});
|
|
id
|
|
}
|
|
|
|
/// Appends a Horizontal/Vertical constraint tied to an explicit datum
|
|
/// direction rather than silently interpreting it in world coordinates.
|
|
pub fn add_axis_constraint(
|
|
&mut self,
|
|
kind: ConstraintKind,
|
|
refs: Vec<ParametricRef>,
|
|
direction: Vector3,
|
|
) -> ConstraintId {
|
|
let fallback = if kind == ConstraintKind::Vertical {
|
|
Vector3::UNIT_Y
|
|
} else {
|
|
Vector3::UNIT_X
|
|
};
|
|
let direction = if direction.length_squared() > 1.0e-24 {
|
|
direction.normalize()
|
|
} else {
|
|
fallback
|
|
};
|
|
let id = self.add(kind, refs, None);
|
|
if let Some(constraint) = self.constraints.last_mut() {
|
|
constraint.axis_direction = Some(direction);
|
|
}
|
|
id
|
|
}
|
|
|
|
pub fn contains_axis_constraint(
|
|
&self,
|
|
kind: ConstraintKind,
|
|
refs: &[ParametricRef],
|
|
direction: Vector3,
|
|
) -> bool {
|
|
let fallback = if kind == ConstraintKind::Vertical {
|
|
Vector3::UNIT_Y
|
|
} else {
|
|
Vector3::UNIT_X
|
|
};
|
|
let direction = if direction.length_squared() > 1.0e-24 {
|
|
direction.normalize()
|
|
} else {
|
|
fallback
|
|
};
|
|
self.constraints.iter().any(|constraint| {
|
|
if !constraint.enabled || constraint.kind != kind {
|
|
return false;
|
|
}
|
|
let same_refs = constraint.refs == refs
|
|
|| (refs.len() == 2
|
|
&& constraint.refs.len() == 2
|
|
&& constraint.refs[0] == refs[1]
|
|
&& constraint.refs[1] == refs[0]);
|
|
if !same_refs {
|
|
return false;
|
|
}
|
|
let existing = constraint.axis_direction.unwrap_or(fallback);
|
|
existing.length_squared() > 1.0e-24
|
|
&& existing.normalize().dot(&direction).abs() >= 1.0 - 1.0e-10
|
|
})
|
|
}
|
|
|
|
/// Removes a constraint by id. Returns whether one was actually removed.
|
|
pub fn remove(&mut self, id: ConstraintId) -> bool {
|
|
let before = self.constraints.len();
|
|
self.constraints.retain(|c| c.id != id);
|
|
self.constraints.len() != before
|
|
}
|
|
|
|
pub fn get(&self, id: ConstraintId) -> Option<&ParametricConstraint> {
|
|
self.constraints.iter().find(|c| c.id == id)
|
|
}
|
|
|
|
/// Every enabled constraint referencing `entity`.
|
|
pub fn constraints_touching(
|
|
&self,
|
|
entity: Handle,
|
|
) -> impl Iterator<Item = &ParametricConstraint> {
|
|
self.constraints
|
|
.iter()
|
|
.filter(move |c| c.enabled && c.refs.iter().any(|r| r.entity == entity))
|
|
}
|
|
|
|
/// Drops every constraint that references `entity` and returns the
|
|
/// removed constraint ids.
|
|
pub fn remove_all_touching(&mut self, entity: Handle) -> Vec<ConstraintId> {
|
|
let (removed, kept): (Vec<_>, Vec<_>) = self
|
|
.constraints
|
|
.drain(..)
|
|
.partition(|c| c.refs.iter().any(|r| r.entity == entity));
|
|
self.constraints = kept;
|
|
removed.into_iter().map(|c| c.id).collect()
|
|
}
|
|
}
|
|
|
|
/// Resolves a [`ParametricRef`] to its current world-space point, for building
|
|
/// an `cadkernel_constraints` `ParamStore` from live document geometry — the constraint
|
|
/// endpoint's equivalent of `dimension_assoc::resolve_reference`, restricted
|
|
/// to the marker conventions constraint endpoints actually use (whole-entity
|
|
/// `None`, an ordinary `source_points()` index, the `-3` center case, or a
|
|
/// bounded curve/segment midpoint, or a curved polyline-segment center).
|
|
///
|
|
/// Solver-side registration reads raw entity fields directly. This helper is
|
|
/// for UI-side consumers that need the current world-space position.
|
|
/// The single addressable point (marker `0`) of a point-like entity: the
|
|
/// node, insertion, or table origin the solver registers as its
|
|
/// `EntityGeom::Point` / text baseline start. `None` for curve entities,
|
|
/// whose marker `0` is a `source_points()` endpoint.
|
|
pub(crate) fn insertion_point(entity: &acadrust::EntityType) -> Option<Vector3> {
|
|
match entity {
|
|
acadrust::EntityType::Point(point) => Some(point.location),
|
|
acadrust::EntityType::Insert(insert) => Some(insert.insert_point),
|
|
acadrust::EntityType::Text(text) => Some(text.insertion_point),
|
|
acadrust::EntityType::MText(text) => Some(text.insertion_point),
|
|
acadrust::EntityType::AttributeDefinition(attribute) => Some(attribute.insertion_point),
|
|
acadrust::EntityType::AttributeEntity(attribute) => Some(attribute.insertion_point),
|
|
acadrust::EntityType::Table(table) => Some(table.insertion_point),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub(crate) fn resolve_point(entity: &acadrust::EntityType, marker: i32) -> Option<Vector3> {
|
|
if marker == 0 {
|
|
if let Some(point) = insertion_point(entity) {
|
|
return Some(point);
|
|
}
|
|
}
|
|
if marker == -3 {
|
|
return match entity {
|
|
acadrust::EntityType::Circle(circle) => Some(circle.center_wcs()),
|
|
acadrust::EntityType::Arc(arc) => Some(arc.center_wcs()),
|
|
acadrust::EntityType::Ellipse(ellipse) => Some(ellipse.center),
|
|
_ => None,
|
|
};
|
|
}
|
|
if marker == -2 {
|
|
let curve = crate::entities::curve::entity_curve(entity)?;
|
|
if curve.is_closed() {
|
|
return None;
|
|
}
|
|
let point = curve.point_at(0.5);
|
|
return Some(Vector3::new(point[0], point[1], point[2]));
|
|
}
|
|
let reference = ParametricRef {
|
|
entity: Handle::NULL,
|
|
marker: Some(marker),
|
|
};
|
|
if let Some(segment) = reference.segment_center_index() {
|
|
let planar = crate::entities::curve::entity_curve(entity)?;
|
|
let curve = planar.curve.segments().into_iter().nth(segment)?;
|
|
let cadkernel::geom2d::Curve::Arc(arc) = curve else {
|
|
return None;
|
|
};
|
|
let point = planar.plane.point_at(arc.centre);
|
|
return Some(Vector3::new(point[0], point[1], point[2]));
|
|
}
|
|
if let Some(segment) = reference.segment_midpoint_index() {
|
|
let planar = crate::entities::curve::entity_curve(entity)?;
|
|
let curve = planar.curve.segments().into_iter().nth(segment)?;
|
|
let point = planar.plane.point_at(curve.point_at(0.5));
|
|
return Some(Vector3::new(point[0], point[1], point[2]));
|
|
}
|
|
if marker < 0 {
|
|
return None;
|
|
}
|
|
super::dimension_assoc::source_points(entity)
|
|
.get(marker as usize)
|
|
.copied()
|
|
}
|
|
|
|
/// Below this squared distance (1e-6 world units), two points count as
|
|
/// already coincident for [`nearest_parametric_point`]'s purposes.
|
|
const COINCIDENT_EPSILON_SQ: f64 = 1.0e-12;
|
|
|
|
/// Addressable constraint points for one entity, in the same marker space
|
|
/// used by persistent constraint references.
|
|
pub(crate) fn parametric_point_candidates(
|
|
entity: &acadrust::EntityType,
|
|
) -> Vec<(i32, Vector3)> {
|
|
let mut points: Vec<_> = super::dimension_assoc::source_points(entity)
|
|
.into_iter()
|
|
.enumerate()
|
|
.map(|(marker, point)| (marker as i32, point))
|
|
.collect();
|
|
// A node / insertion point is a constraint point too (the reference
|
|
// fixes a POINT via NODe and TEXT via INSert), and these entities have
|
|
// no `source_points()` to collide with marker `0`.
|
|
if let Some(point) = insertion_point(entity) {
|
|
points.push((0, point));
|
|
}
|
|
match entity {
|
|
acadrust::EntityType::Circle(circle) => points.push((-3, circle.center_wcs())),
|
|
acadrust::EntityType::Arc(arc) => points.push((-3, arc.center_wcs())),
|
|
acadrust::EntityType::Ellipse(ellipse) => points.push((-3, ellipse.center)),
|
|
_ => {}
|
|
}
|
|
|
|
if matches!(
|
|
entity,
|
|
acadrust::EntityType::Line(_)
|
|
| acadrust::EntityType::Arc(_)
|
|
| acadrust::EntityType::Spline(_)
|
|
| acadrust::EntityType::Ellipse(_)
|
|
) {
|
|
if let Some(curve) = crate::entities::curve::entity_curve(entity) {
|
|
if !curve.is_closed() {
|
|
let point = curve.point_at(0.5);
|
|
points.push((-2, Vector3::new(point[0], point[1], point[2])));
|
|
}
|
|
}
|
|
}
|
|
|
|
if matches!(
|
|
entity,
|
|
acadrust::EntityType::LwPolyline(_) | acadrust::EntityType::Polyline2D(_)
|
|
) {
|
|
if let Some(planar) = crate::entities::curve::entity_curve(entity) {
|
|
points.extend(planar.curve.segments().into_iter().enumerate().map(
|
|
|(index, curve)| {
|
|
let point = planar.plane.point_at(curve.point_at(0.5));
|
|
(
|
|
POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE - index as i32,
|
|
Vector3::new(point[0], point[1], point[2]),
|
|
)
|
|
},
|
|
));
|
|
}
|
|
}
|
|
points
|
|
}
|
|
|
|
pub(crate) fn is_parametric_point_near(entity: &acadrust::EntityType, point: Vector3) -> bool {
|
|
parametric_point_candidates(entity)
|
|
.into_iter()
|
|
.any(|(_, candidate)| (candidate - point).length_squared() <= COINCIDENT_EPSILON_SQ)
|
|
}
|
|
|
|
/// Finds the addressable entity point nearest a snapped world position.
|
|
/// Returns `None` when no point in the scope is within the coincidence
|
|
/// tolerance.
|
|
pub(crate) fn nearest_parametric_point(
|
|
document: &acadrust::CadDocument,
|
|
scope: ParametricScope,
|
|
world_point: Vector3,
|
|
exclude: Option<Handle>,
|
|
) -> Option<ParametricRef> {
|
|
let owner = scope.owner_handle(document);
|
|
let mut best: Option<(f64, ParametricRef)> = None;
|
|
let mut consider = |handle: Handle, marker: i32, point: Vector3| {
|
|
let dx = point.x - world_point.x;
|
|
let dy = point.y - world_point.y;
|
|
let dz = point.z - world_point.z;
|
|
let dist_sq = dx * dx + dy * dy + dz * dz;
|
|
if dist_sq <= COINCIDENT_EPSILON_SQ && best.as_ref().is_none_or(|(d, _)| dist_sq < *d) {
|
|
best = Some((dist_sq, ParametricRef::point(handle, marker)));
|
|
}
|
|
};
|
|
for candidate in document.entities() {
|
|
let common = candidate.common();
|
|
if common.owner_handle != owner || Some(common.handle) == exclude {
|
|
continue;
|
|
}
|
|
for (marker, point) in parametric_point_candidates(candidate) {
|
|
consider(common.handle, marker, point);
|
|
}
|
|
}
|
|
best.map(|(_, r)| r)
|
|
}
|
|
|
|
/// Resolve a point pick within one explicitly selected entity. This keeps two
|
|
/// different endpoints at the same world coordinate distinguishable.
|
|
pub(crate) fn nearest_parametric_point_on_entity(
|
|
document: &acadrust::CadDocument,
|
|
scope: ParametricScope,
|
|
handle: Handle,
|
|
world_point: Vector3,
|
|
) -> Option<ParametricRef> {
|
|
let entity = document.get_entity(handle)?;
|
|
if entity.common().owner_handle != scope.owner_handle(document) {
|
|
return None;
|
|
}
|
|
parametric_point_candidates(entity)
|
|
.into_iter()
|
|
.filter_map(|(marker, point)| {
|
|
let distance = (point - world_point).length_squared();
|
|
(distance <= COINCIDENT_EPSILON_SQ)
|
|
.then_some((distance, ParametricRef::point(handle, marker)))
|
|
})
|
|
.min_by(|(a, _), (b, _)| a.total_cmp(b))
|
|
.map(|(_, reference)| reference)
|
|
}
|
|
|
|
/// Resolve the whole curve or the picked polyline segment used by a
|
|
/// point-to-curve Coincident relation.
|
|
pub(crate) fn parametric_curve_ref_for_pick(
|
|
document: &acadrust::CadDocument,
|
|
scope: ParametricScope,
|
|
handle: Handle,
|
|
world_point: Vector3,
|
|
) -> Option<ParametricRef> {
|
|
let entity = document.get_entity(handle)?;
|
|
if entity.common().owner_handle != scope.owner_handle(document) {
|
|
return None;
|
|
}
|
|
match entity {
|
|
acadrust::EntityType::Line(_)
|
|
| acadrust::EntityType::Circle(_)
|
|
| acadrust::EntityType::Arc(_)
|
|
| acadrust::EntityType::Ellipse(_)
|
|
| acadrust::EntityType::Spline(_) => Some(ParametricRef::whole(handle)),
|
|
acadrust::EntityType::LwPolyline(_) | acadrust::EntityType::Polyline2D(_) => {
|
|
let segments = crate::entities::curve::entity_curve_xy(entity)?.segments();
|
|
cadkernel::geom2d::nearest_of(segments.iter(), [world_point.x, world_point.y])
|
|
.map(|(index, _)| ParametricRef::segment(handle, index))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
impl ConstraintKind {
|
|
/// Per-kind visibility bit used by CONSTRAINTBARMODE for the standard
|
|
/// geometric-constraint family. Helper relations which have no standard
|
|
/// bit stay visible under the normal display policy.
|
|
pub const fn bar_mode_bit(self) -> Option<i16> {
|
|
match self {
|
|
Self::Horizontal => Some(1),
|
|
Self::Vertical => Some(2),
|
|
Self::Perpendicular => Some(4),
|
|
Self::Parallel => Some(8),
|
|
Self::Tangent => Some(16),
|
|
Self::Smooth => Some(32),
|
|
Self::Coincident => Some(64),
|
|
Self::Concentric => Some(128),
|
|
Self::Colinear => Some(256),
|
|
Self::Symmetric => Some(512),
|
|
Self::Equal => Some(1024),
|
|
Self::Fixed => Some(2048),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
pub const fn label(self) -> &'static str {
|
|
match self {
|
|
Self::Coincident => "Coincident",
|
|
Self::Horizontal => "Horizontal",
|
|
Self::Vertical => "Vertical",
|
|
Self::Parallel => "Parallel",
|
|
Self::Perpendicular => "Perpendicular",
|
|
Self::Equal => "Equal",
|
|
Self::Distance => "Distance",
|
|
Self::Angle => "Angle",
|
|
Self::Angle3Point => "3-point angle",
|
|
Self::Radius => "Radius",
|
|
Self::Tangent => "Tangent",
|
|
Self::Smooth => "Smooth",
|
|
Self::Concentric => "Concentric",
|
|
Self::CenterPoint => "Center point",
|
|
Self::Colinear => "Collinear",
|
|
Self::Midpoint => "Midpoint",
|
|
Self::Fixed => "Fixed",
|
|
Self::PointOnCurve => "Point on curve",
|
|
Self::EqualDistance => "Equal distance",
|
|
Self::Symmetric => "Symmetric",
|
|
Self::Diameter => "Diameter",
|
|
Self::DistanceX => "Horizontal distance",
|
|
Self::DistanceY => "Vertical distance",
|
|
Self::DistanceDirected => "Directed distance",
|
|
Self::Normal => "Normal",
|
|
Self::RigidSet => "Rigid set",
|
|
}
|
|
}
|
|
|
|
/// The short symbol a constraint glyph shows — matches the existing
|
|
/// ribbon icons (`crate::modules::parametric::{tools,value}`) for
|
|
/// the kinds that have a one-click button, so the same glyph means the
|
|
/// same thing in both places.
|
|
pub fn glyph_symbol(&self) -> &'static str {
|
|
match self {
|
|
ConstraintKind::Coincident => "≡",
|
|
ConstraintKind::Horizontal => "—",
|
|
ConstraintKind::Vertical => "│",
|
|
ConstraintKind::Parallel => "∥",
|
|
ConstraintKind::Perpendicular => "⊥",
|
|
ConstraintKind::Equal => "=",
|
|
ConstraintKind::Distance => "↔",
|
|
ConstraintKind::Angle => "∠",
|
|
ConstraintKind::Angle3Point => "∠₃",
|
|
ConstraintKind::Radius => "R",
|
|
ConstraintKind::Tangent => "T",
|
|
ConstraintKind::Smooth => "G²",
|
|
ConstraintKind::Concentric => "◎",
|
|
ConstraintKind::CenterPoint => "⊕",
|
|
ConstraintKind::Colinear => "L",
|
|
ConstraintKind::Midpoint => "M",
|
|
ConstraintKind::Fixed => "F",
|
|
ConstraintKind::PointOnCurve => "∈",
|
|
ConstraintKind::EqualDistance => "≐",
|
|
ConstraintKind::Symmetric => "S",
|
|
ConstraintKind::Diameter => "⌀",
|
|
ConstraintKind::DistanceX => "↔ₓ",
|
|
ConstraintKind::DistanceY => "↔ᵧ",
|
|
ConstraintKind::DistanceDirected => "↗",
|
|
ConstraintKind::Normal => "⊾",
|
|
ConstraintKind::RigidSet => "▣",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The full glyph text for one constraint: its symbol, plus the driving
|
|
/// value for a dimensional kind (Distance/Angle/Radius).
|
|
pub(crate) fn glyph_label(constraint: &ParametricConstraint) -> String {
|
|
match (constraint.kind, &constraint.driving_param) {
|
|
(ConstraintKind::Angle, Some(DrivingValue::Literal(value))) => {
|
|
format!("{} {value:.1}°", constraint.kind.glyph_symbol())
|
|
}
|
|
(_, Some(DrivingValue::Literal(value))) => {
|
|
format!("{} {value:.2}", constraint.kind.glyph_symbol())
|
|
}
|
|
// A named reference has no single resolved number to show without
|
|
// threading `ParameterTable` into every glyph-render call site
|
|
// (`src/ui/overlay.rs`) — showing the name itself is enough for now;
|
|
// stage 4's parameters panel is the natural place to reconsider this
|
|
// once a named `driving_param` can actually be authored through the
|
|
// UI (nothing can yet — this arm exists so the match is exhaustive
|
|
// and correct ahead of that UI, not because it's reachable today).
|
|
(_, Some(DrivingValue::Named(name))) => {
|
|
format!("{} {name}", constraint.kind.glyph_symbol())
|
|
}
|
|
(_, None) => constraint.kind.glyph_symbol().to_string(),
|
|
}
|
|
}
|
|
|
|
/// World-space anchor and outward direction for a constraint glyph.
|
|
pub(crate) fn glyph_placement(
|
|
document: &acadrust::CadDocument,
|
|
constraint: &ParametricConstraint,
|
|
) -> Option<(Vector3, Vector3)> {
|
|
glyph_placements(document, constraint).into_iter().next()
|
|
}
|
|
|
|
fn glyph_placement_for_reference(
|
|
document: &acadrust::CadDocument,
|
|
r: ParametricRef,
|
|
) -> Option<(Vector3, Vector3)> {
|
|
let entity = document.get_entity(r.entity)?;
|
|
let line_midpoint = |line: &acadrust::entities::Line| {
|
|
Vector3::new(
|
|
(line.start.x + line.end.x) * 0.5,
|
|
(line.start.y + line.end.y) * 0.5,
|
|
(line.start.z + line.end.z) * 0.5,
|
|
)
|
|
};
|
|
let segment_normal = |start: Vector3, end: Vector3| {
|
|
let direction = Vector3::new(-(end.y - start.y), end.x - start.x, 0.0);
|
|
(direction.length_squared() > 1e-24)
|
|
.then_some(direction)
|
|
.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_center_index() {
|
|
let planar = crate::entities::curve::entity_curve(entity)?;
|
|
let curve = planar.curve.segments().into_iter().nth(segment)?;
|
|
let on_arc = planar.plane.point_at(curve.point_at(0.5));
|
|
let cadkernel::geom2d::Curve::Arc(arc) = curve else {
|
|
return None;
|
|
};
|
|
let center = planar.plane.point_at(arc.centre);
|
|
let center = Vector3::new(center[0], center[1], center[2]);
|
|
let on_arc = Vector3::new(on_arc[0], on_arc[1], on_arc[2]);
|
|
return Some((on_arc, on_arc - center));
|
|
}
|
|
if let Some(segment) = r.segment_index() {
|
|
let anchor = resolve_point(
|
|
entity,
|
|
POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE - segment as i32,
|
|
)?;
|
|
let [start, end] = constraint_segment_endpoints(document, r)?;
|
|
return Some((anchor, segment_normal(start, end)));
|
|
}
|
|
match (entity, r.marker) {
|
|
(acadrust::EntityType::Line(line), None) => Some((line_midpoint(line), line_normal(line))),
|
|
(acadrust::EntityType::Circle(circle), None | Some(-3)) => {
|
|
let center = circle.center_wcs();
|
|
let anchor = circle.point_at_angle_wcs(0.0);
|
|
Some((anchor, anchor - center))
|
|
}
|
|
(acadrust::EntityType::Arc(arc), None | Some(-3)) => {
|
|
let center = arc.center_wcs();
|
|
let anchor = arc.midpoint_wcs();
|
|
Some((anchor, anchor - center))
|
|
}
|
|
(acadrust::EntityType::Line(line), Some(marker)) => {
|
|
let anchor = resolve_point(entity, marker)?;
|
|
let direction = anchor - line_midpoint(line);
|
|
Some((
|
|
anchor,
|
|
(direction.length_squared() > 1e-24)
|
|
.then_some(direction)
|
|
.unwrap_or_else(|| line_normal(line)),
|
|
))
|
|
}
|
|
(acadrust::EntityType::Arc(arc), Some(marker)) => {
|
|
let anchor = resolve_point(entity, marker)?;
|
|
let direction = anchor - arc.center_wcs();
|
|
Some((
|
|
anchor,
|
|
(direction.length_squared() > 1e-24)
|
|
.then_some(direction)
|
|
.unwrap_or(Vector3::UNIT_Y),
|
|
))
|
|
}
|
|
(_, Some(marker)) => {
|
|
let anchor = resolve_point(entity, marker)?;
|
|
Some((anchor, Vector3::UNIT_Y))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn glyph_placements(
|
|
document: &acadrust::CadDocument,
|
|
constraint: &ParametricConstraint,
|
|
) -> Vec<(Vector3, Vector3)> {
|
|
if constraint.kind == ConstraintKind::Parallel {
|
|
return constraint
|
|
.refs
|
|
.iter()
|
|
.filter_map(|reference| glyph_placement_for_reference(document, *reference))
|
|
.collect();
|
|
}
|
|
|
|
let Some(first) = constraint.refs.first().copied() else {
|
|
return Vec::new();
|
|
};
|
|
let fallback = glyph_placement_for_reference(document, first);
|
|
if matches!(
|
|
constraint.kind,
|
|
ConstraintKind::Perpendicular | ConstraintKind::Tangent
|
|
) {
|
|
if let [first, second, ..] = constraint.refs.as_slice() {
|
|
if let (Some(first_curve), Some(second_curve), Some((anchor, outward))) = (
|
|
constraint_reference_curve_xy(document, *first),
|
|
constraint_reference_curve_xy(document, *second),
|
|
fallback,
|
|
) {
|
|
if let Some(crossing) = cadkernel::geom2d::intersect(
|
|
&first_curve,
|
|
&second_curve,
|
|
cadkernel::geom2d::Tolerance::default(),
|
|
)
|
|
.into_iter()
|
|
.next()
|
|
{
|
|
return vec![(
|
|
Vector3::new(crossing.point[0], crossing.point[1], anchor.z),
|
|
outward,
|
|
)];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fallback.into_iter().collect()
|
|
}
|
|
|
|
/// World-space locations that explain what a hovered constraint acts on.
|
|
/// Point constraints expose their referenced point directly; curve relations
|
|
/// expose the contact or intersection that makes the relation visible.
|
|
fn constraint_segment_endpoints(
|
|
document: &acadrust::CadDocument,
|
|
reference: ParametricRef,
|
|
) -> Option<[Vector3; 2]> {
|
|
let segment = reference.segment_index()?;
|
|
let entity = document.get_entity(reference.entity)?;
|
|
let points = super::dimension_assoc::source_points(entity);
|
|
let closed = match entity {
|
|
acadrust::EntityType::LwPolyline(polyline) => polyline.is_closed,
|
|
acadrust::EntityType::Polyline2D(polyline) => polyline.is_closed(),
|
|
_ => return None,
|
|
};
|
|
let first = *points.get(segment)?;
|
|
let second = if segment + 1 < points.len() {
|
|
points[segment + 1]
|
|
} else if closed {
|
|
*points.first()?
|
|
} else {
|
|
return None;
|
|
};
|
|
Some([first, second])
|
|
}
|
|
|
|
fn constraint_reference_curve_xy(
|
|
document: &acadrust::CadDocument,
|
|
reference: ParametricRef,
|
|
) -> Option<cadkernel::geom2d::Curve> {
|
|
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))
|
|
.unwrap_or(Some(curve))
|
|
}
|
|
|
|
pub(crate) fn constraint_hover_points(
|
|
document: &acadrust::CadDocument,
|
|
constraint: &ParametricConstraint,
|
|
) -> Vec<Vector3> {
|
|
let mut points = Vec::new();
|
|
let push_unique = |points: &mut Vec<Vector3>, point: Vector3| {
|
|
if point.x.is_finite()
|
|
&& point.y.is_finite()
|
|
&& point.z.is_finite()
|
|
&& !points
|
|
.iter()
|
|
.any(|existing| (*existing - point).length_squared() <= 1.0e-12)
|
|
{
|
|
points.push(point);
|
|
}
|
|
};
|
|
|
|
for reference in &constraint.refs {
|
|
let Some(marker) = reference.marker else {
|
|
continue;
|
|
};
|
|
let Some(entity) = document.get_entity(reference.entity) else {
|
|
continue;
|
|
};
|
|
if let Some(point) = resolve_point(entity, marker) {
|
|
push_unique(&mut points, point);
|
|
}
|
|
}
|
|
|
|
if matches!(
|
|
constraint.kind,
|
|
ConstraintKind::Horizontal | ConstraintKind::Vertical
|
|
) {
|
|
for reference in &constraint.refs {
|
|
let Some(entity) = document.get_entity(reference.entity) else {
|
|
continue;
|
|
};
|
|
if let Some(endpoints) = constraint_segment_endpoints(document, *reference) {
|
|
for point in endpoints {
|
|
push_unique(&mut points, point);
|
|
}
|
|
} else if matches!(entity, acadrust::EntityType::Line(_)) {
|
|
for point in super::dimension_assoc::source_points(entity) {
|
|
push_unique(&mut points, point);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if matches!(
|
|
constraint.kind,
|
|
ConstraintKind::Perpendicular | ConstraintKind::Tangent
|
|
) {
|
|
if let [first, second, ..] = constraint.refs.as_slice() {
|
|
if let (Some(first_curve), Some(second_curve)) = (
|
|
constraint_reference_curve_xy(document, *first),
|
|
constraint_reference_curve_xy(document, *second),
|
|
) {
|
|
let elevation = glyph_placement(document, constraint)
|
|
.map(|(anchor, _)| anchor.z)
|
|
.unwrap_or(0.0);
|
|
for crossing in cadkernel::geom2d::intersect(
|
|
&first_curve,
|
|
&second_curve,
|
|
cadkernel::geom2d::Tolerance::default(),
|
|
) {
|
|
push_unique(
|
|
&mut points,
|
|
Vector3::new(crossing.point[0], crossing.point[1], elevation),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
points
|
|
}
|
|
|
|
impl super::Scene {
|
|
/// Expand through enabled constraints. Whole-object translations follow
|
|
/// point connections; grip previews include curve relations as well.
|
|
pub(crate) fn parametric_connected_handles(
|
|
&self,
|
|
scope: ParametricScope,
|
|
seeds: &[Handle],
|
|
include_curve_relations: bool,
|
|
) -> Vec<Handle> {
|
|
let mut ordered = seeds.to_vec();
|
|
let mut found: std::collections::HashSet<_> = seeds.iter().copied().collect();
|
|
let Some(set) = self.parametric_constraint_set(scope) else {
|
|
return ordered;
|
|
};
|
|
loop {
|
|
let mut added = false;
|
|
for constraint in set.constraints.iter().filter(|constraint| {
|
|
constraint.enabled
|
|
&& (include_curve_relations || matches!(
|
|
constraint.kind,
|
|
ConstraintKind::Coincident | ConstraintKind::PointOnCurve
|
|
))
|
|
}) {
|
|
if !constraint
|
|
.refs
|
|
.iter()
|
|
.any(|reference| found.contains(&reference.entity))
|
|
{
|
|
continue;
|
|
}
|
|
for reference in &constraint.refs {
|
|
if found.insert(reference.entity) {
|
|
ordered.push(reference.entity);
|
|
added = true;
|
|
}
|
|
}
|
|
}
|
|
if !added {
|
|
break;
|
|
}
|
|
}
|
|
ordered
|
|
}
|
|
|
|
pub fn is_parametric_constraint_visible(
|
|
&self,
|
|
scope: ParametricScope,
|
|
id: ConstraintId,
|
|
) -> bool {
|
|
!self.hidden_parametric_constraints.contains(&(scope, id))
|
|
}
|
|
|
|
pub fn should_display_parametric_constraint(
|
|
&self,
|
|
scope: ParametricScope,
|
|
id: ConstraintId,
|
|
kind: ConstraintKind,
|
|
related_entity_selected: bool,
|
|
display_mode: i16,
|
|
bar_mode: i16,
|
|
) -> bool {
|
|
self.is_parametric_constraint_visible(scope, id)
|
|
&& kind
|
|
.bar_mode_bit()
|
|
.is_none_or(|bit| bar_mode & bit != 0)
|
|
&& (self.shown_parametric_constraints.contains(&(scope, id))
|
|
|| (display_mode & 2 != 0 && related_entity_selected))
|
|
}
|
|
|
|
/// Apply display bit 1 at creation; bit 2 follows the current selection.
|
|
pub fn note_parametric_constraint_applied(
|
|
&mut self,
|
|
scope: ParametricScope,
|
|
id: ConstraintId,
|
|
display_mode: i16,
|
|
) {
|
|
self.hidden_parametric_constraints.remove(&(scope, id));
|
|
if display_mode & 1 != 0 {
|
|
self.shown_parametric_constraints.insert((scope, id));
|
|
} else {
|
|
self.shown_parametric_constraints.remove(&(scope, id));
|
|
}
|
|
}
|
|
|
|
pub fn set_parametric_constraint_visibility(
|
|
&mut self,
|
|
scope: ParametricScope,
|
|
handles: Option<&[Handle]>,
|
|
dimensional: bool,
|
|
visible: bool,
|
|
) -> usize {
|
|
let ids: Vec<_> = self
|
|
.parametric_constraint_set(scope)
|
|
.into_iter()
|
|
.flat_map(|set| set.constraints.iter())
|
|
.filter(|constraint| constraint.driving_param.is_some() == dimensional)
|
|
.filter(|constraint| {
|
|
handles.is_none_or(|handles| {
|
|
constraint
|
|
.refs
|
|
.iter()
|
|
.any(|reference| handles.contains(&reference.entity))
|
|
})
|
|
})
|
|
.map(|constraint| constraint.id)
|
|
.collect();
|
|
for id in &ids {
|
|
if visible {
|
|
self.hidden_parametric_constraints.remove(&(scope, *id));
|
|
self.shown_parametric_constraints.insert((scope, *id));
|
|
} else {
|
|
self.hidden_parametric_constraints.insert((scope, *id));
|
|
self.shown_parametric_constraints.remove(&(scope, *id));
|
|
}
|
|
}
|
|
ids.len()
|
|
}
|
|
|
|
/// Infers relations already present in the selected geometry.
|
|
pub fn inferred_parametric_constraints(
|
|
&self,
|
|
scope: ParametricScope,
|
|
handles: &[Handle],
|
|
settings: &crate::app::settings::AutoConstrainSettings,
|
|
) -> Vec<(ConstraintKind, Vec<ParametricRef>)> {
|
|
use cadkernel::geom2d::{
|
|
infer_constraints_with_settings, Arc, Circle, ConstraintEndpoint, InferenceKind,
|
|
InferenceSettings, InferredConstraint, Line, ParametricPrimitive,
|
|
};
|
|
struct Source {
|
|
handle: Handle,
|
|
primitive: ParametricPrimitive,
|
|
whole: ParametricRef,
|
|
endpoints: [ParametricRef; 2],
|
|
}
|
|
let mut sources = Vec::new();
|
|
for handle in handles {
|
|
match self.document.get_entity(*handle) {
|
|
Some(acadrust::EntityType::Line(line)) => sources.push(Source {
|
|
handle: *handle,
|
|
primitive: ParametricPrimitive::Line(Line {
|
|
start: [line.start.x, line.start.y],
|
|
end: [line.end.x, line.end.y],
|
|
}),
|
|
whole: ParametricRef::whole(*handle),
|
|
endpoints: [
|
|
ParametricRef::point(*handle, 0),
|
|
ParametricRef::point(*handle, 1),
|
|
],
|
|
}),
|
|
Some(acadrust::EntityType::Circle(circle)) => sources.push(Source {
|
|
handle: *handle,
|
|
primitive: ParametricPrimitive::Circle(Circle {
|
|
centre: [circle.center.x, circle.center.y],
|
|
radius: circle.radius,
|
|
}),
|
|
whole: ParametricRef::whole(*handle),
|
|
endpoints: [
|
|
ParametricRef::center(*handle),
|
|
ParametricRef::center(*handle),
|
|
],
|
|
}),
|
|
Some(acadrust::EntityType::Arc(arc)) => sources.push(Source {
|
|
handle: *handle,
|
|
primitive: ParametricPrimitive::Arc(Arc {
|
|
centre: [arc.center.x, arc.center.y],
|
|
radius: arc.radius,
|
|
start_angle: arc.start_angle,
|
|
end_angle: arc.end_angle,
|
|
}),
|
|
whole: ParametricRef::whole(*handle),
|
|
endpoints: [
|
|
ParametricRef::point(*handle, 0),
|
|
ParametricRef::point(*handle, 1),
|
|
],
|
|
}),
|
|
Some(acadrust::EntityType::LwPolyline(polyline)) => {
|
|
let Some(world) = crate::entities::curve::lwpolyline_world_xy(polyline) else {
|
|
continue;
|
|
};
|
|
let count = world.vertices.len();
|
|
for index in 0..count {
|
|
let next = index + 1;
|
|
if next >= count && !world.is_closed {
|
|
break;
|
|
}
|
|
if world.vertices[index].bulge.abs() > 1e-9 {
|
|
continue;
|
|
}
|
|
let a = world.vertices[index].location;
|
|
let b = world.vertices[next % count].location;
|
|
sources.push(Source {
|
|
handle: *handle,
|
|
primitive: ParametricPrimitive::Line(Line {
|
|
start: [a.x, a.y],
|
|
end: [b.x, b.y],
|
|
}),
|
|
whole: ParametricRef::segment(*handle, index),
|
|
endpoints: [
|
|
ParametricRef::point(*handle, index as i32),
|
|
ParametricRef::point(*handle, (next % count) as i32),
|
|
],
|
|
});
|
|
}
|
|
}
|
|
Some(acadrust::EntityType::Polyline2D(polyline)) => {
|
|
let count = polyline.vertices.len();
|
|
for index in 0..count {
|
|
let next = index + 1;
|
|
if next >= count && !polyline.is_closed() {
|
|
break;
|
|
}
|
|
if polyline.vertices[index].bulge.abs() > 1e-9 {
|
|
continue;
|
|
}
|
|
let a = polyline.vertices[index].location;
|
|
let b = polyline.vertices[next % count].location;
|
|
sources.push(Source {
|
|
handle: *handle,
|
|
primitive: ParametricPrimitive::Line(Line {
|
|
start: [a.x, a.y],
|
|
end: [b.x, b.y],
|
|
}),
|
|
whole: ParametricRef::segment(*handle, index),
|
|
endpoints: [
|
|
ParametricRef::point(*handle, index as i32),
|
|
ParametricRef::point(*handle, (next % count) as i32),
|
|
],
|
|
});
|
|
}
|
|
}
|
|
_ => continue,
|
|
}
|
|
}
|
|
let primitives: Vec<_> = sources.iter().map(|source| source.primitive).collect();
|
|
let marker = |endpoint| match endpoint {
|
|
ConstraintEndpoint::Start => 0usize,
|
|
ConstraintEndpoint::End => 1usize,
|
|
};
|
|
let kind = |kind| match kind {
|
|
crate::app::settings::AutoConstraintKind::Coincident => InferenceKind::Coincident,
|
|
crate::app::settings::AutoConstraintKind::Collinear => InferenceKind::Collinear,
|
|
crate::app::settings::AutoConstraintKind::Parallel => InferenceKind::Parallel,
|
|
crate::app::settings::AutoConstraintKind::Perpendicular => {
|
|
InferenceKind::Perpendicular
|
|
}
|
|
crate::app::settings::AutoConstraintKind::Tangent => InferenceKind::Tangent,
|
|
crate::app::settings::AutoConstraintKind::Concentric => InferenceKind::Concentric,
|
|
crate::app::settings::AutoConstraintKind::Horizontal => InferenceKind::Horizontal,
|
|
crate::app::settings::AutoConstraintKind::Vertical => InferenceKind::Vertical,
|
|
crate::app::settings::AutoConstraintKind::Equal => InferenceKind::Equal,
|
|
};
|
|
let inference_settings = InferenceSettings {
|
|
priority: settings
|
|
.priority
|
|
.iter()
|
|
.copied()
|
|
.filter(|candidate| settings.enabled.contains(candidate))
|
|
.map(kind)
|
|
.collect(),
|
|
distance_tolerance: settings.distance_tolerance,
|
|
angle_tolerance_radians: settings.angle_tolerance_deg.to_radians(),
|
|
tangent_must_share_point: settings.tangent_must_share_point,
|
|
perpendicular_must_intersect: settings.perpendicular_must_intersect,
|
|
};
|
|
let mut mapped: Vec<_> =
|
|
infer_constraints_with_settings(&primitives, &inference_settings)
|
|
.into_iter()
|
|
.filter(|relation| {
|
|
!matches!(relation, InferredConstraint::Coincident { first, second, .. }
|
|
if sources[*first].handle == sources[*second].handle)
|
|
})
|
|
.map(|relation| match relation {
|
|
InferredConstraint::Coincident {
|
|
first,
|
|
first_endpoint,
|
|
second,
|
|
second_endpoint,
|
|
} => (
|
|
ConstraintKind::Coincident,
|
|
vec![
|
|
sources[first].endpoints[marker(first_endpoint)],
|
|
sources[second].endpoints[marker(second_endpoint)],
|
|
],
|
|
),
|
|
InferredConstraint::Collinear { first, second } => (
|
|
ConstraintKind::Colinear,
|
|
vec![
|
|
sources[first].whole,
|
|
sources[second].whole,
|
|
],
|
|
),
|
|
InferredConstraint::Concentric { first, second } => (
|
|
ConstraintKind::Concentric,
|
|
vec![
|
|
ParametricRef::center(sources[first].handle),
|
|
ParametricRef::center(sources[second].handle),
|
|
],
|
|
),
|
|
InferredConstraint::Parallel { first, second } => (
|
|
ConstraintKind::Parallel,
|
|
vec![
|
|
sources[first].whole,
|
|
sources[second].whole,
|
|
],
|
|
),
|
|
InferredConstraint::Perpendicular { first, second } => (
|
|
ConstraintKind::Perpendicular,
|
|
vec![
|
|
sources[first].whole,
|
|
sources[second].whole,
|
|
],
|
|
),
|
|
InferredConstraint::Horizontal { entity } => (
|
|
ConstraintKind::Horizontal,
|
|
vec![sources[entity].whole],
|
|
),
|
|
InferredConstraint::Vertical { entity } => (
|
|
ConstraintKind::Vertical,
|
|
vec![sources[entity].whole],
|
|
),
|
|
InferredConstraint::Tangent { first, second } => (
|
|
ConstraintKind::Tangent,
|
|
vec![
|
|
sources[first].whole,
|
|
sources[second].whole,
|
|
],
|
|
),
|
|
InferredConstraint::Equal { first, second } => (
|
|
ConstraintKind::Equal,
|
|
vec![sources[first].whole, sources[second].whole],
|
|
),
|
|
})
|
|
.collect();
|
|
if let Some(existing) = self.parametric_constraint_set(scope) {
|
|
mapped.retain(|(kind, refs)| {
|
|
!existing.constraints.iter().any(|constraint| {
|
|
constraint.kind == *kind
|
|
&& (constraint.refs == *refs
|
|
|| (constraint.refs.len() == 2
|
|
&& refs.len() == 2
|
|
&& constraint.refs[0] == refs[1]
|
|
&& constraint.refs[1] == refs[0]))
|
|
})
|
|
});
|
|
}
|
|
mapped.retain(|(kind, refs)| {
|
|
self.validate_parametric_constraint(*kind, refs, None)
|
|
.is_ok()
|
|
});
|
|
mapped
|
|
}
|
|
|
|
/// Infer only endpoint/vertex coincidences from the selected objects.
|
|
/// Unlike the broader automatic constraint pass, this includes spline,
|
|
/// ellipse, and polyline vertices while leaving centers and midpoints to
|
|
/// their dedicated constraint kinds.
|
|
pub fn inferred_coincident_constraints(
|
|
&self,
|
|
scope: ParametricScope,
|
|
handles: &[Handle],
|
|
) -> Vec<Vec<ParametricRef>> {
|
|
let owner = scope.owner_handle(&self.document);
|
|
let mut sources = Vec::new();
|
|
let mut seen_handles = std::collections::HashSet::new();
|
|
for handle in handles.iter().copied() {
|
|
if !seen_handles.insert(handle) {
|
|
continue;
|
|
}
|
|
let Some(entity) = self.document.get_entity(handle) else {
|
|
continue;
|
|
};
|
|
if entity.common().owner_handle != owner {
|
|
continue;
|
|
}
|
|
for (marker, point) in super::dimension_assoc::source_points(entity)
|
|
.into_iter()
|
|
.enumerate()
|
|
{
|
|
sources.push((ParametricRef::point(handle, marker as i32), point));
|
|
}
|
|
}
|
|
|
|
let existing = self.parametric_constraint_set(scope);
|
|
let mut inferred = Vec::new();
|
|
for first in 0..sources.len() {
|
|
for second in first + 1..sources.len() {
|
|
if sources[first].0.entity == sources[second].0.entity
|
|
|| (sources[first].1 - sources[second].1).length_squared()
|
|
> COINCIDENT_EPSILON_SQ
|
|
{
|
|
continue;
|
|
}
|
|
let refs = vec![sources[first].0, sources[second].0];
|
|
let already_exists = existing.is_some_and(|set| {
|
|
set.constraints.iter().any(|constraint| {
|
|
constraint.kind == ConstraintKind::Coincident
|
|
&& constraint.refs.len() == 2
|
|
&& (constraint.refs == refs
|
|
|| (constraint.refs[0] == refs[1]
|
|
&& constraint.refs[1] == refs[0]))
|
|
})
|
|
});
|
|
if !already_exists
|
|
&& self
|
|
.validate_parametric_constraint(ConstraintKind::Coincident, &refs, None)
|
|
.is_ok()
|
|
{
|
|
inferred.push(refs);
|
|
}
|
|
}
|
|
}
|
|
inferred
|
|
}
|
|
|
|
/// The constraint set for `scope`, if one has been created.
|
|
pub fn parametric_constraint_set(
|
|
&self,
|
|
scope: ParametricScope,
|
|
) -> Option<&ParametricConstraintSet> {
|
|
self.parametric_constraints
|
|
.iter()
|
|
.find(|s| s.scope == scope)
|
|
}
|
|
|
|
/// The constraint set for `scope`, creating an empty one on first use.
|
|
/// The `pub(crate)` `parametric_constraints` field itself stays private so
|
|
/// nothing outside this module can end up with two sets for the same
|
|
/// scope — this is the one way to reach a scope's set for both reading
|
|
/// and mutating.
|
|
pub fn parametric_constraint_set_mut(
|
|
&mut self,
|
|
scope: ParametricScope,
|
|
) -> &mut ParametricConstraintSet {
|
|
if let Some(index) = self
|
|
.parametric_constraints
|
|
.iter()
|
|
.position(|s| s.scope == scope)
|
|
{
|
|
&mut self.parametric_constraints[index]
|
|
} else {
|
|
self.parametric_constraints
|
|
.push(ParametricConstraintSet::new(scope));
|
|
self.parametric_constraints.last_mut().expect("just pushed")
|
|
}
|
|
}
|
|
|
|
/// Screen-projected parametric-constraint glyph placements for `scope`:
|
|
/// `(id, anchor, outward_screen_direction, label, is_conflicting,
|
|
/// hover_points)` for every enabled, visible constraint whose glyph
|
|
/// projects on-screen.
|
|
/// `vp_size` is the full canvas size (as `SelectionState::vp_size`
|
|
/// reports it), matching what `viewport_edit_frame`/
|
|
/// `active_model_tile_bounds` expect. Mirrors the projection
|
|
/// `crate::app::view` builds its own render list with, and is reused by
|
|
/// [`constraint_glyph_hit`](Self::constraint_glyph_hit) — both feed the
|
|
/// same `(anchor, outward, label)` triples into
|
|
/// `crate::ui::overlay::constraint_glyph_box`/`constraint_glyph_offsets`,
|
|
/// so hit-testing can never drift from what's actually drawn.
|
|
pub fn constraint_glyph_placements_screen(
|
|
&self,
|
|
scope: ParametricScope,
|
|
vp_size: (f32, f32),
|
|
show_values: bool,
|
|
display_mode: i16,
|
|
bar_mode: i16,
|
|
) -> Vec<(
|
|
ConstraintId,
|
|
iced::Point,
|
|
[f32; 2],
|
|
String,
|
|
bool,
|
|
Vec<iced::Point>,
|
|
)> {
|
|
let Some(set) = self.parametric_constraint_set(scope) else {
|
|
return Vec::new();
|
|
};
|
|
if set.constraints.is_empty() {
|
|
return Vec::new();
|
|
}
|
|
let edit_frame = self.viewport_edit_frame(vp_size);
|
|
let bounds = match &edit_frame {
|
|
Some((_, full)) => *full,
|
|
None => self.active_model_tile_bounds(vp_size.0, vp_size.1),
|
|
};
|
|
let (view_rot, eye) = if let Some((cam, _)) = &edit_frame {
|
|
(cam.view_proj_rte(bounds), cam.eye())
|
|
} else {
|
|
let cam = self.camera.borrow();
|
|
(cam.view_proj_rte(bounds), cam.eye())
|
|
};
|
|
set.constraints
|
|
.iter()
|
|
.filter(|c| c.enabled)
|
|
.filter(|c| {
|
|
let selected = c
|
|
.refs
|
|
.iter()
|
|
.any(|reference| self.selected.contains(&reference.entity)
|
|
|| self.preview_hidden.contains(&reference.entity));
|
|
self.should_display_parametric_constraint(
|
|
scope,
|
|
c.id,
|
|
c.kind,
|
|
selected,
|
|
display_mode,
|
|
bar_mode,
|
|
)
|
|
})
|
|
.flat_map(|c| {
|
|
let is_conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id);
|
|
let label = if show_values {
|
|
glyph_label(c)
|
|
} else {
|
|
c.kind.glyph_symbol().to_string()
|
|
};
|
|
let hover_points: Vec<iced::Point> = constraint_hover_points(&self.document, c)
|
|
.into_iter()
|
|
.filter_map(|hover_point| {
|
|
let projected = crate::scene::pick::grip::project_rte(
|
|
glam::DVec3::new(hover_point.x, hover_point.y, hover_point.z),
|
|
view_rot,
|
|
eye,
|
|
bounds,
|
|
)?;
|
|
let point = iced::Point::new(
|
|
bounds.x + projected.x,
|
|
bounds.y + projected.y,
|
|
);
|
|
(point.x.is_finite() && point.y.is_finite()).then_some(point)
|
|
})
|
|
.collect();
|
|
glyph_placements(&self.document, c)
|
|
.into_iter()
|
|
.filter_map(|(anchor, outward)| {
|
|
let screen = crate::scene::pick::grip::project_rte(
|
|
glam::DVec3::new(anchor.x, anchor.y, anchor.z),
|
|
view_rot,
|
|
eye,
|
|
bounds,
|
|
)?;
|
|
let outward_screen = crate::scene::pick::grip::project_rte(
|
|
glam::DVec3::new(
|
|
anchor.x + outward.x,
|
|
anchor.y + outward.y,
|
|
anchor.z + outward.z,
|
|
),
|
|
view_rot,
|
|
eye,
|
|
bounds,
|
|
)?;
|
|
let direction =
|
|
(outward_screen - screen).normalize_or(glam::Vec2::NEG_Y);
|
|
let point = iced::Point::new(bounds.x + screen.x, bounds.y + screen.y);
|
|
point.x.is_finite().then(|| (
|
|
c.id,
|
|
point,
|
|
direction.to_array(),
|
|
label.clone(),
|
|
is_conflicting,
|
|
hover_points.clone(),
|
|
))
|
|
})
|
|
.collect::<Vec<_>>()
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Hit-tests screen point `p` (same coordinate space as `p_full` in the
|
|
/// viewport click handler) against the glyph pills from
|
|
/// [`constraint_glyph_placements_screen`](Self::constraint_glyph_placements_screen),
|
|
/// via `crate::ui::overlay::constraint_glyph_hit_test`'s shared layout
|
|
/// math, so a click only registers where the pill is actually drawn.
|
|
pub fn constraint_glyph_hit(
|
|
&self,
|
|
scope: ParametricScope,
|
|
vp_size: (f32, f32),
|
|
show_values: bool,
|
|
display_mode: i16,
|
|
bar_mode: i16,
|
|
p: iced::Point,
|
|
) -> Option<ConstraintId> {
|
|
let placements = self.constraint_glyph_placements_screen(
|
|
scope,
|
|
vp_size,
|
|
show_values,
|
|
display_mode,
|
|
bar_mode,
|
|
);
|
|
let glyphs: Vec<(iced::Point, [f32; 2], String, bool)> = placements
|
|
.iter()
|
|
.map(|(_, point, direction, label, is_conflicting, _)| {
|
|
(*point, *direction, label.clone(), *is_conflicting)
|
|
})
|
|
.collect();
|
|
let index = crate::ui::overlay::constraint_glyph_hit_test(&glyphs, p)?;
|
|
Some(placements[index].0)
|
|
}
|
|
|
|
/// Handle remapping lives in each command that duplicates
|
|
/// entities — `Scene::copy_entities`' `handle_map` (COPY/ARRAY/MIRROR,
|
|
/// `src/scene/modify.rs`) and `OpenCADStudio::finalize_paste`'s own
|
|
/// (clipboard paste, `src/app/command_driver.rs`) — rather than in one
|
|
/// shared table, so each call site passes its own `handle_map` here
|
|
/// after adding the duplicated entities.
|
|
///
|
|
/// For every enabled constraint whose *every* referenced entity was
|
|
/// duplicated (a constraint straddling a duplicated and a
|
|
/// non-duplicated entity can't sensibly follow — only one side moved),
|
|
/// adds an equivalent constraint over the new handles to the same
|
|
/// scope, then triggers a solve for the newly duplicated geometry the
|
|
/// same way any other edit would. A no-op when `handle_map` is empty or
|
|
/// nothing constrained was duplicated.
|
|
pub fn duplicate_parametric_constraints_for(
|
|
&mut self,
|
|
handle_map: &rustc_hash::FxHashMap<Handle, Handle>,
|
|
) {
|
|
if handle_map.is_empty() {
|
|
return;
|
|
}
|
|
let mut to_add: Vec<(
|
|
usize,
|
|
ConstraintKind,
|
|
Vec<ParametricRef>,
|
|
Option<DrivingValue>,
|
|
Option<Vector3>,
|
|
)> = Vec::new();
|
|
for (scope_index, set) in self.parametric_constraints.iter().enumerate() {
|
|
for c in &set.constraints {
|
|
if !c.enabled || !c.refs.iter().all(|r| handle_map.contains_key(&r.entity)) {
|
|
continue;
|
|
}
|
|
let new_refs: Vec<ParametricRef> = c
|
|
.refs
|
|
.iter()
|
|
.map(|r| ParametricRef {
|
|
entity: handle_map[&r.entity],
|
|
marker: r.marker,
|
|
})
|
|
.collect();
|
|
to_add.push((
|
|
scope_index,
|
|
c.kind,
|
|
new_refs,
|
|
c.driving_param.clone(),
|
|
c.axis_direction,
|
|
));
|
|
}
|
|
}
|
|
if to_add.is_empty() {
|
|
return;
|
|
}
|
|
let mut touched: Vec<Handle> = Vec::new();
|
|
for (scope_index, kind, refs, driving_param, axis_direction) in to_add {
|
|
touched.extend(refs.iter().map(|r| r.entity));
|
|
self.parametric_constraints[scope_index].add(kind, refs, driving_param);
|
|
self.parametric_constraints[scope_index]
|
|
.constraints
|
|
.last_mut()
|
|
.expect("the copied constraint was just added")
|
|
.axis_direction = axis_direction;
|
|
}
|
|
touched.sort();
|
|
touched.dedup();
|
|
let changes: Vec<(Handle, super::ChangeKind)> = touched
|
|
.into_iter()
|
|
.map(|h| (h, super::ChangeKind::Modified))
|
|
.collect();
|
|
self.bump_entities(&changes);
|
|
}
|
|
|
|
/// Every persistent constraint, in any scope, currently driven by the
|
|
/// named parameter `name` — what the Named Parameters panel's "used by"
|
|
/// column shows. `entities` is the constraint's own referenced handles
|
|
/// (deduplicated; a two-point constraint on the same entity's own two
|
|
/// markers would otherwise list it twice), not resolved against the
|
|
/// live document — a caller wanting an entity's current type/position
|
|
/// still needs `Scene::document.get_entity`.
|
|
pub fn parameter_usage(&self, name: &str) -> Vec<ParameterUsage> {
|
|
let mut out = Vec::new();
|
|
for set in &self.parametric_constraints {
|
|
for c in &set.constraints {
|
|
let Some(DrivingValue::Named(n)) = &c.driving_param else {
|
|
continue;
|
|
};
|
|
if n != name {
|
|
continue;
|
|
}
|
|
let mut entities: Vec<Handle> = c.refs.iter().map(|r| r.entity).collect();
|
|
entities.sort();
|
|
entities.dedup();
|
|
out.push(ParameterUsage {
|
|
scope: set.scope,
|
|
constraint_id: c.id,
|
|
kind: c.kind,
|
|
entities,
|
|
});
|
|
}
|
|
}
|
|
out
|
|
}
|
|
}
|
|
|
|
/// One persistent constraint driven by a named parameter — [`Scene::parameter_usage`]'s
|
|
/// result type.
|
|
#[derive(Debug, Clone)]
|
|
pub struct ParameterUsage {
|
|
pub scope: ParametricScope,
|
|
pub constraint_id: ConstraintId,
|
|
pub kind: ConstraintKind,
|
|
pub entities: Vec<Handle>,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn h(v: u64) -> Handle {
|
|
Handle::new(v)
|
|
}
|
|
|
|
#[test]
|
|
fn glyph_placement_points_away_from_its_geometry() {
|
|
let mut document = acadrust::CadDocument::new();
|
|
let mut line = acadrust::entities::Line::from_points(
|
|
Vector3::new(0.0, 0.0, 0.0),
|
|
Vector3::new(10.0, 0.0, 0.0),
|
|
);
|
|
line.common.handle = h(1);
|
|
document
|
|
.add_entity(acadrust::EntityType::Line(line))
|
|
.unwrap();
|
|
let mut circle = acadrust::entities::Circle::from_center_radius(Vector3::ZERO, 5.0);
|
|
circle.common.handle = h(2);
|
|
document
|
|
.add_entity(acadrust::EntityType::Circle(circle))
|
|
.unwrap();
|
|
let mut polyline = acadrust::entities::LwPolyline::from_points(vec![
|
|
acadrust::types::Vector2::new(0.0, 0.0),
|
|
acadrust::types::Vector2::new(10.0, 0.0),
|
|
]);
|
|
polyline.common.handle = h(3);
|
|
document
|
|
.add_entity(acadrust::EntityType::LwPolyline(polyline))
|
|
.unwrap();
|
|
|
|
let constraint = |reference| ParametricConstraint {
|
|
id: 0,
|
|
kind: ConstraintKind::Fixed,
|
|
refs: vec![reference],
|
|
driving_param: None,
|
|
enabled: true,
|
|
native_origin: None,
|
|
rigid_points: Vec::new(),
|
|
distance_direction_type: 0,
|
|
distance_direction: None,
|
|
axis_direction: None,
|
|
angle_sector: angle_sector::PARALLEL_COUNTERCLOCKWISE,
|
|
};
|
|
let (anchor, direction) =
|
|
glyph_placement(&document, &constraint(ParametricRef::whole(h(1)))).unwrap();
|
|
assert_eq!(anchor, Vector3::new(5.0, 0.0, 0.0));
|
|
assert_eq!(direction, Vector3::new(0.0, 10.0, 0.0));
|
|
let (anchor, direction) =
|
|
glyph_placement(&document, &constraint(ParametricRef::point(h(1), 0))).unwrap();
|
|
assert_eq!(anchor, Vector3::ZERO);
|
|
assert_eq!(direction, Vector3::new(-5.0, 0.0, 0.0));
|
|
let (anchor, direction) =
|
|
glyph_placement(&document, &constraint(ParametricRef::center(h(2)))).unwrap();
|
|
assert_eq!(anchor, Vector3::new(5.0, 0.0, 0.0));
|
|
assert_eq!(direction, Vector3::new(5.0, 0.0, 0.0));
|
|
let (anchor, direction) =
|
|
glyph_placement(&document, &constraint(ParametricRef::segment(h(3), 0))).unwrap();
|
|
assert_eq!(anchor, Vector3::new(5.0, 0.0, 0.0));
|
|
assert_eq!(direction, Vector3::new(0.0, 10.0, 0.0));
|
|
|
|
let mut tangent_line = acadrust::entities::Line::from_points(
|
|
Vector3::new(-10.0, 5.0, 0.0),
|
|
Vector3::new(10.0, 5.0, 0.0),
|
|
);
|
|
tangent_line.common.handle = h(4);
|
|
document.add_entity(acadrust::EntityType::Line(tangent_line)).unwrap();
|
|
let relation = |id, kind, refs| ParametricConstraint {
|
|
id,
|
|
kind,
|
|
refs,
|
|
driving_param: None,
|
|
enabled: true,
|
|
native_origin: None,
|
|
rigid_points: Vec::new(),
|
|
distance_direction_type: 0,
|
|
distance_direction: None,
|
|
axis_direction: None,
|
|
angle_sector: angle_sector::PARALLEL_COUNTERCLOCKWISE,
|
|
};
|
|
let tangent = relation(
|
|
1,
|
|
ConstraintKind::Tangent,
|
|
vec![ParametricRef::whole(h(4)), ParametricRef::whole(h(2))],
|
|
);
|
|
assert_eq!(
|
|
glyph_placement(&document, &tangent).unwrap().0,
|
|
Vector3::new(0.0, 5.0, 0.0)
|
|
);
|
|
|
|
let mut rectangle = acadrust::entities::LwPolyline::from_points(vec![
|
|
acadrust::types::Vector2::new(0.0, 0.0),
|
|
acadrust::types::Vector2::new(4.0, 0.0),
|
|
acadrust::types::Vector2::new(4.0, 2.0),
|
|
acadrust::types::Vector2::new(0.0, 2.0),
|
|
]);
|
|
rectangle.common.handle = h(5);
|
|
rectangle.is_closed = true;
|
|
document.add_entity(acadrust::EntityType::LwPolyline(rectangle)).unwrap();
|
|
let parallel = glyph_placements(
|
|
&document,
|
|
&relation(
|
|
2,
|
|
ConstraintKind::Parallel,
|
|
vec![ParametricRef::segment(h(5), 0), ParametricRef::segment(h(5), 2)],
|
|
),
|
|
);
|
|
assert_eq!(parallel.len(), 2);
|
|
assert_eq!(parallel[0].0, Vector3::new(2.0, 0.0, 0.0));
|
|
assert_eq!(parallel[1].0, Vector3::new(2.0, 2.0, 0.0));
|
|
let perpendicular = relation(
|
|
3,
|
|
ConstraintKind::Perpendicular,
|
|
vec![ParametricRef::segment(h(5), 3), ParametricRef::segment(h(5), 2)],
|
|
);
|
|
assert_eq!(
|
|
glyph_placement(&document, &perpendicular).unwrap().0,
|
|
Vector3::new(0.0, 2.0, 0.0)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn polyline_constraint_hover_builds_only_the_referenced_segment() {
|
|
let mut scene = super::super::Scene::new();
|
|
let handle = scene.add_entity(acadrust::EntityType::LwPolyline(
|
|
acadrust::entities::LwPolyline::from_points(vec![
|
|
acadrust::types::Vector2::new(0.0, 0.0),
|
|
acadrust::types::Vector2::new(4.0, 0.0),
|
|
acadrust::types::Vector2::new(4.0, 2.0),
|
|
]),
|
|
));
|
|
|
|
scene.set_constraint_hover_highlights(&[ParametricRef::segment(handle, 1)]);
|
|
|
|
assert!(scene.constraint_hover_highlights.is_empty());
|
|
assert_eq!(scene.constraint_hover_wires.len(), 1);
|
|
let wire = &scene.constraint_hover_wires[0];
|
|
let points: Vec<_> = wire.points.iter().zip(&wire.points_low).map(|(high, low)| [
|
|
high[0] as f64 + low[0] as f64,
|
|
high[1] as f64 + low[1] as f64,
|
|
high[2] as f64 + low[2] as f64,
|
|
]).collect();
|
|
assert_eq!(points, vec![[4.0, 0.0, 0.0], [4.0, 2.0, 0.0]]);
|
|
}
|
|
|
|
#[test]
|
|
fn add_assigns_increasing_ids_and_get_finds_them() {
|
|
let mut set = ParametricConstraintSet::new(ParametricScope::ModelSpace);
|
|
let a = set.add(
|
|
ConstraintKind::Horizontal,
|
|
vec![ParametricRef::whole(h(1))],
|
|
None,
|
|
);
|
|
let b = set.add(
|
|
ConstraintKind::Distance,
|
|
vec![ParametricRef::whole(h(1))],
|
|
Some(DrivingValue::Literal(25.0)),
|
|
);
|
|
assert_ne!(a, b);
|
|
assert_eq!(set.get(a).unwrap().kind, ConstraintKind::Horizontal);
|
|
assert_eq!(
|
|
set.get(b).unwrap().driving_param,
|
|
Some(DrivingValue::Literal(25.0))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_drops_only_the_matching_id() {
|
|
let mut set = ParametricConstraintSet::new(ParametricScope::ModelSpace);
|
|
let a = set.add(
|
|
ConstraintKind::Horizontal,
|
|
vec![ParametricRef::whole(h(1))],
|
|
None,
|
|
);
|
|
let b = set.add(
|
|
ConstraintKind::Vertical,
|
|
vec![ParametricRef::whole(h(2))],
|
|
None,
|
|
);
|
|
assert!(set.remove(a));
|
|
assert!(
|
|
!set.remove(a),
|
|
"removing twice should report nothing removed the second time"
|
|
);
|
|
assert!(set.get(a).is_none());
|
|
assert!(set.get(b).is_some());
|
|
}
|
|
|
|
#[test]
|
|
fn constraints_touching_finds_entity_regardless_of_marker() {
|
|
let mut set = ParametricConstraintSet::new(ParametricScope::ModelSpace);
|
|
set.add(
|
|
ConstraintKind::Coincident,
|
|
vec![ParametricRef::point(h(1), 0), ParametricRef::point(h(2), 1)],
|
|
None,
|
|
);
|
|
set.add(
|
|
ConstraintKind::Horizontal,
|
|
vec![ParametricRef::whole(h(3))],
|
|
None,
|
|
);
|
|
|
|
let touching_1: Vec<_> = set.constraints_touching(h(1)).collect();
|
|
assert_eq!(touching_1.len(), 1);
|
|
let touching_3: Vec<_> = set.constraints_touching(h(3)).collect();
|
|
assert_eq!(touching_3.len(), 1);
|
|
assert_eq!(set.constraints_touching(h(99)).count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn constraints_touching_skips_disabled() {
|
|
let mut set = ParametricConstraintSet::new(ParametricScope::ModelSpace);
|
|
let id = set.add(
|
|
ConstraintKind::Horizontal,
|
|
vec![ParametricRef::whole(h(1))],
|
|
None,
|
|
);
|
|
set.constraints
|
|
.iter_mut()
|
|
.find(|c| c.id == id)
|
|
.unwrap()
|
|
.enabled = false;
|
|
assert_eq!(set.constraints_touching(h(1)).count(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_all_touching_drops_every_constraint_referencing_the_entity() {
|
|
let mut set = ParametricConstraintSet::new(ParametricScope::ModelSpace);
|
|
let coincident = set.add(
|
|
ConstraintKind::Coincident,
|
|
vec![ParametricRef::point(h(1), 0), ParametricRef::point(h(2), 1)],
|
|
None,
|
|
);
|
|
let horizontal_other = set.add(
|
|
ConstraintKind::Horizontal,
|
|
vec![ParametricRef::whole(h(3))],
|
|
None,
|
|
);
|
|
|
|
let removed = set.remove_all_touching(h(1));
|
|
assert_eq!(removed, vec![coincident]);
|
|
assert!(set.get(coincident).is_none());
|
|
assert!(
|
|
set.get(horizontal_other).is_some(),
|
|
"unrelated entity's constraint must survive"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn scope_owner_handle_resolves_block_directly() {
|
|
let block_handle = h(42);
|
|
let scope = ParametricScope::Block(block_handle);
|
|
let doc = acadrust::CadDocument::new();
|
|
assert_eq!(scope.owner_handle(&doc), block_handle);
|
|
}
|
|
|
|
#[test]
|
|
fn ref_center_constructor_matches_the_dash_three_convention() {
|
|
let r = ParametricRef::center(h(7));
|
|
assert_eq!(
|
|
r,
|
|
ParametricRef {
|
|
entity: h(7),
|
|
marker: Some(-3)
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn arc_grips_drive_center_start_and_end_but_not_midpoint() {
|
|
let handle = h(8);
|
|
let arc = acadrust::EntityType::Arc(acadrust::entities::Arc::from_coords(
|
|
0.0,
|
|
0.0,
|
|
0.0,
|
|
5.0,
|
|
0.0,
|
|
std::f64::consts::PI,
|
|
));
|
|
|
|
assert_eq!(
|
|
grip_solve_anchor_refs(&arc, handle, 0),
|
|
vec![ParametricRef::center(handle)]
|
|
);
|
|
assert_eq!(
|
|
grip_solve_anchor_refs(&arc, handle, 1),
|
|
vec![ParametricRef::point(handle, 0)]
|
|
);
|
|
assert_eq!(
|
|
grip_solve_anchor_refs(&arc, handle, 2),
|
|
vec![ParametricRef::point(handle, 1)]
|
|
);
|
|
assert_eq!(grip_solve_anchor_refs(&arc, handle, 3), Vec::new());
|
|
}
|
|
|
|
#[test]
|
|
fn parameter_usage_finds_every_constraint_driven_by_the_named_parameter() {
|
|
let mut scene = super::super::Scene::new();
|
|
scene
|
|
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
|
.add(
|
|
ConstraintKind::Distance,
|
|
vec![ParametricRef::point(h(1), 0), ParametricRef::point(h(1), 1)],
|
|
Some(DrivingValue::Named("gap".to_string())),
|
|
);
|
|
scene
|
|
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
|
.add(
|
|
ConstraintKind::Radius,
|
|
vec![ParametricRef::whole(h(2))],
|
|
Some(DrivingValue::Named("gap".to_string())),
|
|
);
|
|
// Unrelated: a literal-driven constraint and one driven by a
|
|
// different name must not show up.
|
|
scene
|
|
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
|
.add(
|
|
ConstraintKind::Radius,
|
|
vec![ParametricRef::whole(h(3))],
|
|
Some(DrivingValue::Literal(5.0)),
|
|
);
|
|
scene
|
|
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
|
.add(
|
|
ConstraintKind::Distance,
|
|
vec![ParametricRef::point(h(4), 0), ParametricRef::point(h(4), 1)],
|
|
Some(DrivingValue::Named("other".to_string())),
|
|
);
|
|
|
|
let usage = scene.parameter_usage("gap");
|
|
assert_eq!(
|
|
usage.len(),
|
|
2,
|
|
"exactly the two constraints driven by 'gap', got {usage:?}"
|
|
);
|
|
assert!(usage
|
|
.iter()
|
|
.any(|u| u.kind == ConstraintKind::Distance && u.entities == vec![h(1)]));
|
|
assert!(usage
|
|
.iter()
|
|
.any(|u| u.kind == ConstraintKind::Radius && u.entities == vec![h(2)]));
|
|
|
|
assert_eq!(scene.parameter_usage("nonexistent").len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn parameter_usage_searches_every_scope_not_just_model_space() {
|
|
let mut scene = super::super::Scene::new();
|
|
let block = h(99);
|
|
scene
|
|
.parametric_constraint_set_mut(ParametricScope::Block(block))
|
|
.add(
|
|
ConstraintKind::Radius,
|
|
vec![ParametricRef::whole(h(1))],
|
|
Some(DrivingValue::Named("r".to_string())),
|
|
);
|
|
let usage = scene.parameter_usage("r");
|
|
assert_eq!(usage.len(), 1);
|
|
assert_eq!(usage[0].scope, ParametricScope::Block(block));
|
|
}
|
|
|
|
#[test]
|
|
fn parameter_usage_deduplicates_an_entity_referenced_by_two_markers() {
|
|
let mut scene = super::super::Scene::new();
|
|
// A Distance constraint whose two points are both on the same
|
|
// entity (e.g. a line's own start and end) must list that entity
|
|
// once, not twice.
|
|
scene
|
|
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
|
.add(
|
|
ConstraintKind::Distance,
|
|
vec![ParametricRef::point(h(1), 0), ParametricRef::point(h(1), 1)],
|
|
Some(DrivingValue::Named("len".to_string())),
|
|
);
|
|
let usage = scene.parameter_usage("len");
|
|
assert_eq!(usage.len(), 1);
|
|
assert_eq!(usage[0].entities, vec![h(1)]);
|
|
}
|
|
|
|
#[test]
|
|
fn automatic_inference_maps_relations_and_skips_existing_constraints() {
|
|
let mut scene = super::super::Scene::new();
|
|
let first = scene.add_entity(acadrust::EntityType::Line(
|
|
acadrust::entities::Line::from_points(
|
|
Vector3::new(0.0, 0.0, 0.0),
|
|
Vector3::new(5.0, 0.0, 0.0),
|
|
),
|
|
));
|
|
let second = scene.add_entity(acadrust::EntityType::Line(
|
|
acadrust::entities::Line::from_points(
|
|
Vector3::new(5.0, 0.0, 0.0),
|
|
Vector3::new(10.0, 0.0, 0.0),
|
|
),
|
|
));
|
|
scene
|
|
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
|
.add(
|
|
ConstraintKind::Horizontal,
|
|
vec![ParametricRef::whole(first)],
|
|
None,
|
|
);
|
|
|
|
let inferred =
|
|
scene.inferred_parametric_constraints(
|
|
ParametricScope::ModelSpace,
|
|
&[first, second],
|
|
&crate::app::settings::AutoConstrainSettings::default(),
|
|
);
|
|
|
|
assert!(!inferred.iter().any(|(kind, refs)| {
|
|
*kind == ConstraintKind::Horizontal && *refs == [ParametricRef::whole(first)]
|
|
}));
|
|
assert!(inferred.iter().any(|(kind, refs)| {
|
|
*kind == ConstraintKind::Horizontal && *refs == [ParametricRef::whole(second)]
|
|
}));
|
|
assert!(inferred
|
|
.iter()
|
|
.any(|(kind, _)| *kind == ConstraintKind::Coincident));
|
|
assert!(!inferred
|
|
.iter()
|
|
.any(|(kind, _)| *kind == ConstraintKind::Colinear));
|
|
}
|
|
|
|
#[test]
|
|
fn visibility_toggles_geometric_and_dimensional_independently() {
|
|
let mut scene = super::super::Scene::new();
|
|
let line = scene.add_entity(acadrust::EntityType::Line(
|
|
acadrust::entities::Line::from_points(
|
|
Vector3::new(0.0, 0.0, 0.0),
|
|
Vector3::new(5.0, 0.0, 0.0),
|
|
),
|
|
));
|
|
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
|
|
let geometric = set.add(
|
|
ConstraintKind::Horizontal,
|
|
vec![ParametricRef::whole(line)],
|
|
None,
|
|
);
|
|
let dimensional = set.add(
|
|
ConstraintKind::Distance,
|
|
vec![ParametricRef::point(line, 0), ParametricRef::point(line, 1)],
|
|
Some(DrivingValue::Literal(5.0)),
|
|
);
|
|
|
|
assert_eq!(
|
|
scene.set_parametric_constraint_visibility(
|
|
ParametricScope::ModelSpace,
|
|
None,
|
|
false,
|
|
false,
|
|
),
|
|
1
|
|
);
|
|
assert!(!scene.is_parametric_constraint_visible(ParametricScope::ModelSpace, geometric));
|
|
assert!(scene.is_parametric_constraint_visible(ParametricScope::ModelSpace, dimensional));
|
|
}
|
|
|
|
#[test]
|
|
fn constraint_display_distinguishes_loaded_created_and_explicit_visibility() {
|
|
let mut scene = super::super::Scene::new();
|
|
let scope = ParametricScope::ModelSpace;
|
|
let id = scene.parametric_constraint_set_mut(scope).add(
|
|
ConstraintKind::Horizontal,
|
|
vec![ParametricRef::whole(h(1))],
|
|
None,
|
|
);
|
|
assert!(!scene.should_display_parametric_constraint(
|
|
scope, id, ConstraintKind::Horizontal, false, 3, 4095
|
|
));
|
|
assert!(scene.should_display_parametric_constraint(
|
|
scope, id, ConstraintKind::Horizontal, true, 2, 4095
|
|
));
|
|
assert!(!scene.should_display_parametric_constraint(
|
|
scope, id, ConstraintKind::Horizontal, true, 1, 4095
|
|
));
|
|
scene.note_parametric_constraint_applied(scope, id, 1);
|
|
assert!(scene.should_display_parametric_constraint(
|
|
scope, id, ConstraintKind::Horizontal, false, 0, 4095
|
|
));
|
|
scene.set_parametric_constraint_visibility(scope, None, false, false);
|
|
assert!(!scene.should_display_parametric_constraint(
|
|
scope, id, ConstraintKind::Horizontal, true, 3, 4095
|
|
));
|
|
scene.set_parametric_constraint_visibility(scope, None, false, true);
|
|
assert!(scene.should_display_parametric_constraint(
|
|
scope, id, ConstraintKind::Horizontal, false, 0, 4095
|
|
));
|
|
scene.note_parametric_constraint_applied(scope, id, 0);
|
|
assert!(!scene.should_display_parametric_constraint(
|
|
scope, id, ConstraintKind::Horizontal, false, 3, 4095
|
|
));
|
|
assert!(scene.should_display_parametric_constraint(
|
|
scope, id, ConstraintKind::Horizontal, true, 2, 4095
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn hidden_constraint_stays_hidden_when_related_entity_is_selected() {
|
|
let mut scene = super::super::Scene::new();
|
|
let scope = ParametricScope::ModelSpace;
|
|
let id = scene.parametric_constraint_set_mut(scope).add(
|
|
ConstraintKind::Horizontal,
|
|
vec![ParametricRef::whole(h(1))],
|
|
None,
|
|
);
|
|
scene.hidden_parametric_constraints.insert((scope, id));
|
|
|
|
assert!(!scene.should_display_parametric_constraint(
|
|
scope, id, ConstraintKind::Horizontal, true, 2, 4095
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn hover_geometry_keeps_a_bulged_polyline_segment_curved() {
|
|
let mut document = acadrust::CadDocument::new();
|
|
let mut polyline = acadrust::entities::LwPolyline::new();
|
|
polyline.common.handle = h(1);
|
|
polyline.vertices = vec![
|
|
acadrust::entities::LwVertex::with_bulge(
|
|
acadrust::types::Vector2::new(0.0, 0.0),
|
|
1.0,
|
|
),
|
|
acadrust::entities::LwVertex::from_coords(10.0, 0.0),
|
|
];
|
|
document
|
|
.add_entity(acadrust::EntityType::LwPolyline(polyline))
|
|
.unwrap();
|
|
|
|
assert!(matches!(
|
|
constraint_reference_curve_xy(&document, ParametricRef::segment(h(1), 0)),
|
|
Some(cadkernel::geom2d::Curve::Arc(_))
|
|
));
|
|
}
|
|
|
|
#[test]
|
|
fn curve_pick_uses_the_bulged_segment_instead_of_its_chord() {
|
|
let mut scene = super::super::Scene::new();
|
|
let mut polyline = acadrust::entities::LwPolyline::new();
|
|
polyline.vertices = vec![
|
|
acadrust::entities::LwVertex::with_bulge(
|
|
acadrust::types::Vector2::new(0.0, 0.0),
|
|
1.0,
|
|
),
|
|
acadrust::entities::LwVertex::from_coords(10.0, 0.0),
|
|
acadrust::entities::LwVertex::from_coords(0.0, -4.0),
|
|
];
|
|
let handle = scene.add_entity(acadrust::EntityType::LwPolyline(polyline));
|
|
|
|
assert_eq!(
|
|
parametric_curve_ref_for_pick(
|
|
&scene.document,
|
|
ParametricScope::ModelSpace,
|
|
handle,
|
|
Vector3::new(5.0, -5.0, 0.0),
|
|
),
|
|
Some(ParametricRef::segment(handle, 0))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn move_expansion_follows_only_coincident_relations() {
|
|
let mut scene = super::super::Scene::new();
|
|
let set = scene.parametric_constraint_set_mut(ParametricScope::ModelSpace);
|
|
set.add(
|
|
ConstraintKind::Coincident,
|
|
vec![ParametricRef::point(h(1), 0), ParametricRef::point(h(2), 0)],
|
|
None,
|
|
);
|
|
set.add(
|
|
ConstraintKind::PointOnCurve,
|
|
vec![ParametricRef::point(h(2), 0), ParametricRef::whole(h(3))],
|
|
None,
|
|
);
|
|
set.add(
|
|
ConstraintKind::Parallel,
|
|
vec![ParametricRef::whole(h(3)), ParametricRef::whole(h(4))],
|
|
None,
|
|
);
|
|
|
|
assert_eq!(
|
|
scene.parametric_connected_handles(ParametricScope::ModelSpace, &[h(1)], false),
|
|
vec![h(1), h(2), h(3)]
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn duplicating_an_axis_constraint_keeps_its_direction() {
|
|
let mut scene = super::super::Scene::new();
|
|
let line = |y| {
|
|
acadrust::EntityType::Line(acadrust::entities::Line::from_points(
|
|
Vector3::new(0.0, y, 0.0),
|
|
Vector3::new(4.0, y + 1.0, 0.0),
|
|
))
|
|
};
|
|
let source = scene.add_entity(line(0.0));
|
|
let copied = scene.add_entity(line(10.0));
|
|
let direction = Vector3::new(3.0, 4.0, 0.0);
|
|
scene
|
|
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
|
|
.add_axis_constraint(
|
|
ConstraintKind::Horizontal,
|
|
vec![ParametricRef::whole(source)],
|
|
direction,
|
|
);
|
|
let mut handle_map = rustc_hash::FxHashMap::default();
|
|
handle_map.insert(source, copied);
|
|
|
|
scene.duplicate_parametric_constraints_for(&handle_map);
|
|
|
|
let constraints = &scene
|
|
.parametric_constraint_set(ParametricScope::ModelSpace)
|
|
.unwrap()
|
|
.constraints;
|
|
assert_eq!(constraints.len(), 2);
|
|
assert_eq!(constraints[1].refs, vec![ParametricRef::whole(copied)]);
|
|
assert_eq!(constraints[1].axis_direction, Some(direction.normalize()));
|
|
}
|
|
}
|