forked from 1b-agent/opencadstudio-upstream
Merge pull request #1356 from ramox81/codex/fixed-constraint-parity
Match Fix geometric constraint behavior # Conflicts: # src/scene/parametric_constraints.rs # src/scene/parametric_solve.rs
This commit is contained in:
commit
7ef4e9f336
9 files changed
+572
-203
No files matched your search
@@ -3182,23 +3182,23 @@ impl OpenCADStudio {
|
||||
driving_param,
|
||||
label,
|
||||
} => {
|
||||
use crate::scene::parametric_constraints::ConstraintKind;
|
||||
// GCSMOOTH and FXCONSTRAINT re-prompt after a rejected pick.
|
||||
let keep_command = self.tabs[i].active_cmd.as_ref().is_some_and(|command| {
|
||||
(kind == ConstraintKind::Smooth && command.name() == "GCSMOOTH")
|
||||
|| (kind == ConstraintKind::Fixed && command.name() == "FXCONSTRAINT")
|
||||
});
|
||||
if let Err(message) = self.tabs[i].scene.validate_parametric_constraint(
|
||||
kind,
|
||||
&refs,
|
||||
driving_param.as_ref(),
|
||||
) {
|
||||
let keep_smooth_selection = kind
|
||||
== crate::scene::parametric_constraints::ConstraintKind::Smooth
|
||||
&& self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.is_some_and(|command| command.name() == "GCSMOOTH");
|
||||
if !keep_smooth_selection {
|
||||
if !keep_command {
|
||||
self.tabs[i].active_cmd = None;
|
||||
}
|
||||
self.tabs[i].snap_result = None;
|
||||
self.command_line.push_error(message);
|
||||
if keep_smooth_selection {
|
||||
if keep_command {
|
||||
if let Some(prompt) =
|
||||
self.tabs[i].active_cmd.as_ref().map(|command| command.prompt())
|
||||
{
|
||||
@@ -3208,6 +3208,22 @@ impl OpenCADStudio {
|
||||
return Task::none();
|
||||
}
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
if kind == ConstraintKind::Fixed
|
||||
&& self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.is_some_and(|set| {
|
||||
set.constraints.iter().any(|existing| {
|
||||
existing.enabled && existing.kind == kind && existing.refs == refs
|
||||
})
|
||||
})
|
||||
{
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.command_line
|
||||
.push_error("The constraint already exists on the selected objects.");
|
||||
return Task::none();
|
||||
}
|
||||
let constraints_before = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
@@ -3249,6 +3265,59 @@ impl OpenCADStudio {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::AddFixedConstraint(pick) => {
|
||||
use crate::modules::parametric::FixConstraintCommand;
|
||||
use crate::scene::parametric_constraints::{
|
||||
nearest_parametric_point, nearest_parametric_point_on_entity,
|
||||
parametric_curve_ref_for_pick, ConstraintKind,
|
||||
};
|
||||
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let document = &self.tabs[i].scene.document;
|
||||
let world =
|
||||
acadrust::types::Vector3::new(pick.point.x, pick.point.y, pick.point.z);
|
||||
let resolved = match (pick.whole_curve, pick.handle) {
|
||||
(true, Some(handle)) => {
|
||||
parametric_curve_ref_for_pick(document, scope, handle, world)
|
||||
.ok_or(FixConstraintCommand::INVALID_OBJECT)
|
||||
}
|
||||
(true, None) => Err(FixConstraintCommand::NO_OBJECT),
|
||||
(false, Some(handle)) => {
|
||||
nearest_parametric_point_on_entity(document, scope, handle, world)
|
||||
.ok_or(FixConstraintCommand::INVALID_OBJECT)
|
||||
}
|
||||
(false, None) => nearest_parametric_point(document, scope, world, None)
|
||||
.ok_or(FixConstraintCommand::NO_POINT),
|
||||
};
|
||||
// An off-plane or 3D curve is "not a valid object" to the
|
||||
// reference, not a solver limitation.
|
||||
let resolved = resolved.and_then(|reference| {
|
||||
let supported = !pick.whole_curve
|
||||
|| self.tabs[i]
|
||||
.scene
|
||||
.validate_parametric_constraint(
|
||||
ConstraintKind::Fixed,
|
||||
&[reference],
|
||||
None,
|
||||
)
|
||||
.is_ok();
|
||||
supported
|
||||
.then_some(reference)
|
||||
.ok_or(FixConstraintCommand::INVALID_OBJECT)
|
||||
});
|
||||
return match resolved {
|
||||
Ok(reference) => self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
||||
kind: ConstraintKind::Fixed,
|
||||
refs: vec![reference],
|
||||
driving_param: None,
|
||||
label: "Fixed constraint",
|
||||
}),
|
||||
// A miss re-prompts: `ReportError` keeps the command.
|
||||
Err(message) => {
|
||||
self.apply_cmd_result(CmdResult::ReportError(message.to_string()))
|
||||
}
|
||||
};
|
||||
}
|
||||
CmdResult::AddHorizontalConstraint {
|
||||
selection,
|
||||
direction,
|
||||
|
||||
@@ -1420,7 +1420,7 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
"VCONSTRAINT" | "FXCONSTRAINT" => {
|
||||
"VCONSTRAINT" => {
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
@@ -1433,20 +1433,49 @@ impl OpenCADStudio {
|
||||
} else {
|
||||
use crate::command::CmdResult;
|
||||
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
|
||||
let (kind, label) = if cmd == "VCONSTRAINT" {
|
||||
(ConstraintKind::Vertical, "Vertical constraint")
|
||||
} else {
|
||||
(ConstraintKind::Fixed, "Fixed constraint")
|
||||
};
|
||||
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
||||
kind,
|
||||
kind: ConstraintKind::Vertical,
|
||||
refs: vec![ParametricRef::whole(handles[0])],
|
||||
driving_param: None,
|
||||
label,
|
||||
label: "Vertical constraint",
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
"FXCONSTRAINT" | "GCFIX" => {
|
||||
use crate::command::CmdResult;
|
||||
use crate::modules::parametric::FixConstraintCommand;
|
||||
use crate::scene::parametric_constraints::ConstraintKind;
|
||||
|
||||
// One preselected whole curve applies at once (ribbon
|
||||
// "select first, then click"); anything else goes through
|
||||
// the reference's own point-or-object prompt.
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if let [handle] = handles.as_slice() {
|
||||
let reference = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(*handle)
|
||||
.and_then(|entity| {
|
||||
FixConstraintCommand::preselected_reference(entity, *handle)
|
||||
});
|
||||
if let Some(reference) = reference {
|
||||
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
||||
kind: ConstraintKind::Fixed,
|
||||
refs: vec![reference],
|
||||
driving_param: None,
|
||||
label: "Fixed constraint",
|
||||
}));
|
||||
}
|
||||
}
|
||||
if !handles.is_empty() {
|
||||
self.tabs[i].scene.deselect_all();
|
||||
}
|
||||
let command = FixConstraintCommand::new();
|
||||
self.command_line.push_info(&command.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||
}
|
||||
|
||||
"CCONSTRAINT" | "GCCOINCIDENT" => {
|
||||
use crate::modules::parametric::CoincidentConstraintCommand;
|
||||
let new_cmd = CoincidentConstraintCommand::new();
|
||||
|
||||
@@ -1655,6 +1655,11 @@ pub enum CmdResult {
|
||||
kind: crate::scene::parametric_constraints::ConstraintKind,
|
||||
label: &'static str,
|
||||
},
|
||||
/// Add a persistent `Fixed` constraint (`crate::modules::parametric::fixed`)
|
||||
/// at one picked constraint point (`whole_curve == false`, resolved by the
|
||||
/// host like `AddPointOnEntityConstraint`'s point) or on the whole curve /
|
||||
/// polyline segment under the pick (`whole_curve == true`).
|
||||
AddFixedConstraint(CoincidentPick),
|
||||
/// Add a persistent `EqualDistance` constraint
|
||||
/// (`crate::modules::parametric::equal_distance`): the distance
|
||||
/// between `points[0]`/`points[1]` equals the distance between
|
||||
@@ -2891,6 +2896,7 @@ mod constraint_registry_tests {
|
||||
"NRCONSTRAINT",
|
||||
"LCONSTRAINT",
|
||||
"FXCONSTRAINT",
|
||||
"GCFIX",
|
||||
"SYCONSTRAINT",
|
||||
"DCONSTRAINT",
|
||||
"ACONSTRAINT",
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
//! GCFIX / FXCONSTRAINT — locks a constraint point, or a whole curve /
|
||||
//! polyline segment, at its current position. Picks resolve on the host (a
|
||||
//! `CadCommand` has no document access): `AddFixedConstraint` carries the
|
||||
//! click, the driver maps it to a `ParametricRef` and re-prompts on a miss,
|
||||
//! the way the reference does.
|
||||
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, CoincidentPick};
|
||||
use crate::scene::parametric_constraints::ParametricRef;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
enum Step {
|
||||
PointOrObject,
|
||||
Object,
|
||||
}
|
||||
|
||||
pub struct FixConstraintCommand {
|
||||
step: Step,
|
||||
}
|
||||
|
||||
impl FixConstraintCommand {
|
||||
pub const INVALID_OBJECT: &'static str =
|
||||
"Invalid selection for Fixed. Select a line, polyline segment, circle, arc, ellipse or spline.";
|
||||
pub const NO_POINT: &'static str = "No valid constraint point found.";
|
||||
pub const NO_OBJECT: &'static str = "No object found.";
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
step: Step::PointOrObject,
|
||||
}
|
||||
}
|
||||
|
||||
/// The Object-mode reading of a single preselected entity: a whole
|
||||
/// curve, or a one-segment polyline's segment. Anything else prompts.
|
||||
pub fn preselected_reference(entity: &EntityType, handle: Handle) -> Option<ParametricRef> {
|
||||
match entity {
|
||||
EntityType::Line(_)
|
||||
| EntityType::Circle(_)
|
||||
| EntityType::Arc(_)
|
||||
| EntityType::Ellipse(_)
|
||||
| EntityType::Spline(_) => Some(ParametricRef::whole(handle)),
|
||||
EntityType::LwPolyline(polyline) => (polyline.vertices.len() == 2
|
||||
&& !polyline.is_closed)
|
||||
.then(|| ParametricRef::segment(handle, 0)),
|
||||
EntityType::Polyline2D(polyline) => (polyline.vertices.len() == 2
|
||||
&& !polyline.is_closed())
|
||||
.then(|| ParametricRef::segment(handle, 0)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn pick(&self, handle: Option<Handle>, point: DVec3) -> CmdResult {
|
||||
CmdResult::AddFixedConstraint(CoincidentPick {
|
||||
handle,
|
||||
point,
|
||||
whole_curve: matches!(self.step, Step::Object),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for FixConstraintCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"FXCONSTRAINT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.step {
|
||||
Step::PointOrObject => "FXCONSTRAINT Select point or [Object] <Object>:".to_string(),
|
||||
Step::Object => "FXCONSTRAINT Select object:".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
matches!(self.step, Step::PointOrObject)
|
||||
.then(|| vec![CmdOption::new("Object", "O")])
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
matches!(self.step, Step::PointOrObject)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let keyword = text.trim().trim_start_matches('_').to_ascii_uppercase();
|
||||
let to_object =
|
||||
matches!(self.step, Step::PointOrObject) && matches!(keyword.as_str(), "O" | "OBJECT");
|
||||
to_object.then(|| {
|
||||
self.step = Step::Object;
|
||||
CmdResult::NeedPoint
|
||||
})
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn entity_pick_accepts_points(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn entity_pick_highlights_hover(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return self.on_point(point);
|
||||
}
|
||||
self.pick(Some(handle), point)
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
match self.step {
|
||||
Step::PointOrObject => self.pick(None, point),
|
||||
// A click on nothing at "Select object:".
|
||||
Step::Object => CmdResult::ReportError(Self::NO_OBJECT.to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
match self.step {
|
||||
Step::PointOrObject => {
|
||||
self.step = Step::Object;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Object => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,8 @@ mod coincident;
|
||||
mod concentric;
|
||||
mod constraint_bar;
|
||||
mod equal_distance;
|
||||
#[path = "fixed.rs"]
|
||||
mod fixed_command;
|
||||
mod geom_constraint;
|
||||
#[path = "horizontal.rs"]
|
||||
mod horizontal_command;
|
||||
@@ -23,6 +25,7 @@ pub use coincident::{coincident_tool, CoincidentConstraintCommand};
|
||||
pub use concentric::ConcentricConstraintCommand;
|
||||
pub use constraint_bar::ConstraintBarOptionCommand;
|
||||
pub use equal_distance::{equal_distance_tool, EqualDistanceConstraintCommand};
|
||||
pub use fixed_command::FixConstraintCommand;
|
||||
pub use geom_constraint::GeomConstraintCommand;
|
||||
pub use horizontal_command::HorizontalConstraintCommand;
|
||||
pub use perpendicular_command::{PerpendicularConstraintCommand, PerpendicularPick};
|
||||
|
||||
@@ -136,7 +136,7 @@ pub mod fixed {
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "FXCONSTRAINT",
|
||||
label: "Fixed",
|
||||
label: "Fix",
|
||||
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/fixed.svg")),
|
||||
event: ModuleEvent::Command("FXCONSTRAINT".to_string()),
|
||||
}
|
||||
@@ -171,6 +171,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
||||
"NRCONSTRAINT",
|
||||
"LCONSTRAINT",
|
||||
"FXCONSTRAINT",
|
||||
"GCFIX",
|
||||
"SYCONSTRAINT",
|
||||
]
|
||||
});
|
||||
@@ -593,7 +593,29 @@ impl ParametricConstraintSet {
|
||||
///
|
||||
/// 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()),
|
||||
@@ -651,6 +673,12 @@ pub(crate) fn parametric_point_candidates(
|
||||
.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())),
|
||||
@@ -869,6 +897,23 @@ impl ConstraintKind {
|
||||
}
|
||||
}
|
||||
|
||||
/// Fixed draws a padlock (`src/ui/overlay.rs`), the way the reference bar
|
||||
/// does: the plain symbol for a held curve or segment, this variant when one
|
||||
/// addressable point is held (white lock with the point marker).
|
||||
pub(crate) const FIXED_POINT_GLYPH: &str = "F·";
|
||||
|
||||
pub(crate) fn fixed_glyph_label(constraint: &ParametricConstraint) -> &'static str {
|
||||
let point = constraint
|
||||
.refs
|
||||
.first()
|
||||
.is_some_and(|reference| reference.marker.is_some() && reference.segment_index().is_none());
|
||||
if point {
|
||||
FIXED_POINT_GLYPH
|
||||
} else {
|
||||
ConstraintKind::Fixed.glyph_symbol()
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
@@ -1672,7 +1717,9 @@ impl super::Scene {
|
||||
})
|
||||
.flat_map(|c| {
|
||||
let is_conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id);
|
||||
let base_label = if show_values {
|
||||
let base_label = if c.kind == ConstraintKind::Fixed {
|
||||
fixed_glyph_label(c).to_string()
|
||||
} else if show_values {
|
||||
glyph_label(c)
|
||||
} else {
|
||||
c.kind.glyph_symbol().to_string()
|
||||
|
||||
+188
-184
@@ -28,6 +28,7 @@ use cadkernel_constraints::solvers::dogleg::solve_dl;
|
||||
use cadkernel_constraints::solvers::lm::solve_lm;
|
||||
use cadkernel_constraints::solvers::SolveStatus;
|
||||
use cadkernel_constraints::system::System;
|
||||
use cadkernel_constraints::util::ParamId;
|
||||
|
||||
use super::named_parameters::ParameterTable;
|
||||
use super::parametric_constraints::{
|
||||
@@ -184,6 +185,16 @@ fn register_entity(
|
||||
handle: Handle,
|
||||
) -> Option<EntityGeom> {
|
||||
let entity = document.get_entity(handle)?;
|
||||
register_entity_geom(sys, handle, entity)
|
||||
}
|
||||
|
||||
/// `register_entity` for an entity value that isn't the document's — the
|
||||
/// retained pre-edit original a persistent `Fixed` pins to.
|
||||
fn register_entity_geom(
|
||||
sys: &mut System,
|
||||
handle: Handle,
|
||||
entity: &EntityType,
|
||||
) -> Option<EntityGeom> {
|
||||
match entity {
|
||||
EntityType::Point(point) => Some(EntityGeom::Point(GPoint::new(
|
||||
sys.add_param(point.location.x, false),
|
||||
@@ -1027,6 +1038,74 @@ fn retained_tangent_side(
|
||||
Some(dx * (center[1] - y1) - dy * (center[0] - x1) >= 0.0)
|
||||
}
|
||||
|
||||
/// The kernel parameters a `Fixed` constraint on `r` holds in place: a
|
||||
/// polyline segment's endpoints (plus center/radius for an arc segment), one
|
||||
/// addressable point, or every intrinsic parameter of a whole entity. Arc
|
||||
/// pins its five intrinsic params only — `start`/`end` are already tied to
|
||||
/// those by the arc-rules `CurveValue` constraints `solve_scope` adds;
|
||||
/// Ellipse likewise pins center + radmin, `focus1` being tied to `center`
|
||||
/// by the ellipse-rules `Difference` constraints.
|
||||
fn fixed_pin_params(
|
||||
document: &acadrust::CadDocument,
|
||||
sys: &mut System,
|
||||
cache: &mut HashMap<Handle, EntityGeom>,
|
||||
r: ParametricRef,
|
||||
) -> Vec<ParamId> {
|
||||
if let Some(index) = r.segment_index() {
|
||||
let Some(geometry) = resolve_ref(document, sys, cache, r) else {
|
||||
return Vec::new();
|
||||
};
|
||||
if let Some(line) = geometry.line_segment(index) {
|
||||
return vec![line.p1.x, line.p1.y, line.p2.x, line.p2.y];
|
||||
}
|
||||
return geometry
|
||||
.arc_segment(index)
|
||||
.map(|segment| {
|
||||
let arc = segment.arc;
|
||||
vec![
|
||||
arc.start.x,
|
||||
arc.start.y,
|
||||
arc.end.x,
|
||||
arc.end.y,
|
||||
arc.circle.center.x,
|
||||
arc.circle.center.y,
|
||||
arc.circle.rad,
|
||||
]
|
||||
})
|
||||
.unwrap_or_default();
|
||||
}
|
||||
if r.marker.is_some() {
|
||||
return resolve_constraint_point(document, sys, cache, r)
|
||||
.map(|point| vec![point.x, point.y])
|
||||
.unwrap_or_default();
|
||||
}
|
||||
match resolve_ref(document, sys, cache, r) {
|
||||
Some(EntityGeom::Point(point)) => vec![point.x, point.y],
|
||||
Some(EntityGeom::Polyline { points, .. }) => {
|
||||
points.iter().flat_map(|point| [point.x, point.y]).collect()
|
||||
}
|
||||
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],
|
||||
Some(EntityGeom::Circle(circle)) => vec![circle.center.x, circle.center.y, circle.rad],
|
||||
Some(EntityGeom::Arc(arc)) => vec![
|
||||
arc.circle.center.x,
|
||||
arc.circle.center.y,
|
||||
arc.circle.rad,
|
||||
arc.start_angle,
|
||||
arc.end_angle,
|
||||
],
|
||||
Some(EntityGeom::Ellipse(ellipse)) => {
|
||||
vec![ellipse.center.x, ellipse.center.y, ellipse.radmin]
|
||||
}
|
||||
Some(EntityGeom::Spline { curve, .. }) => {
|
||||
curve.poles.iter().flat_map(|point| [point.x, point.y]).collect()
|
||||
}
|
||||
None => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the kernel equations for one constraint. Unsupported references or
|
||||
/// invalid dimensional targets produce no equations.
|
||||
fn build_constraint(
|
||||
@@ -1037,6 +1116,7 @@ fn build_constraint(
|
||||
c: &ParametricConstraint,
|
||||
tangent_side: Option<bool>,
|
||||
tangent_point: Option<ParametricRef>,
|
||||
originals: &HashMap<Handle, std::sync::Arc<EntityType>>,
|
||||
) -> Vec<Rc<dyn Constraint>> {
|
||||
if !c.enabled || !refs_share_supported_plane(document, &c.refs) {
|
||||
return Vec::new();
|
||||
@@ -1448,175 +1528,36 @@ fn build_constraint(
|
||||
let Some(r) = c.refs.first() else {
|
||||
return Vec::new();
|
||||
};
|
||||
if r.segment_index().is_some() {
|
||||
let Some(line) = whole_line(sys, cache, *r) else {
|
||||
return Vec::new();
|
||||
};
|
||||
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),
|
||||
)
|
||||
};
|
||||
return vec![
|
||||
Rc::new(Equal::new(line.p1.x, sys.add_param(x1, true), 1.0)),
|
||||
Rc::new(Equal::new(line.p1.y, sys.add_param(y1, true), 1.0)),
|
||||
Rc::new(Equal::new(line.p2.x, sys.add_param(x2, true), 1.0)),
|
||||
Rc::new(Equal::new(line.p2.y, sys.add_param(y2, true), 1.0)),
|
||||
];
|
||||
}
|
||||
if r.marker.is_some() {
|
||||
let Some(point) = point_ref(sys, cache, *r) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let (x, y) = {
|
||||
let store = sys.store();
|
||||
(store.get(point.x), store.get(point.y))
|
||||
};
|
||||
return vec![
|
||||
Rc::new(Equal::new(point.x, sys.add_param(x, true), 1.0)),
|
||||
Rc::new(Equal::new(point.y, sys.add_param(y, true), 1.0)),
|
||||
];
|
||||
}
|
||||
let Some(geom) = resolve_ref(document, sys, cache, *r) else {
|
||||
return Vec::new();
|
||||
};
|
||||
match geom {
|
||||
EntityGeom::Point(point) => {
|
||||
let (x, y) = {
|
||||
let pinned = fixed_pin_params(document, sys, cache, *r);
|
||||
// A persistent Fixed holds the *pre-edit* geometry: after MOVE /
|
||||
// STRETCH / a grip drag the entity in `document` is already
|
||||
// displaced, so the target comes from the caller's retained
|
||||
// original when there is one — the entity's own live values
|
||||
// otherwise (a freshly applied Fixed pins where it stands).
|
||||
let mut targets: Option<Vec<f64>> = None;
|
||||
if let Some(original) = originals.get(&r.entity) {
|
||||
let mut original_cache = HashMap::new();
|
||||
if let Some(geometry) = register_entity_geom(sys, r.entity, original.as_ref()) {
|
||||
original_cache.insert(r.entity, geometry);
|
||||
let params = fixed_pin_params(document, sys, &mut original_cache, *r);
|
||||
if params.len() == pinned.len() {
|
||||
let store = sys.store();
|
||||
(store.get(point.x), store.get(point.y))
|
||||
};
|
||||
vec![
|
||||
Rc::new(Equal::new(point.x, sys.add_param(x, true), 1.0)),
|
||||
Rc::new(Equal::new(point.y, sys.add_param(y, true), 1.0)),
|
||||
]
|
||||
}
|
||||
EntityGeom::Polyline { points, .. } => {
|
||||
let values: Vec<_> = points
|
||||
.iter()
|
||||
.map(|point| {
|
||||
let store = sys.store();
|
||||
(point, store.get(point.x), store.get(point.y))
|
||||
})
|
||||
.collect();
|
||||
values
|
||||
.into_iter()
|
||||
.flat_map(|(point, x, y)| {
|
||||
[
|
||||
Rc::new(Equal::new(point.x, sys.add_param(x, true), 1.0))
|
||||
as Rc<dyn Constraint>,
|
||||
Rc::new(Equal::new(point.y, sys.add_param(y, true), 1.0))
|
||||
as Rc<dyn Constraint>,
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
EntityGeom::TextLine(l)
|
||||
| EntityGeom::Line(l)
|
||||
| EntityGeom::Ray(l)
|
||||
| EntityGeom::XLine(l) => {
|
||||
let (x1, y1, x2, y2) = {
|
||||
let store = sys.store();
|
||||
(
|
||||
store.get(l.p1.x),
|
||||
store.get(l.p1.y),
|
||||
store.get(l.p2.x),
|
||||
store.get(l.p2.y),
|
||||
)
|
||||
};
|
||||
vec![
|
||||
Rc::new(Equal::new(l.p1.x, sys.add_param(x1, true), 1.0)),
|
||||
Rc::new(Equal::new(l.p1.y, sys.add_param(y1, true), 1.0)),
|
||||
Rc::new(Equal::new(l.p2.x, sys.add_param(x2, true), 1.0)),
|
||||
Rc::new(Equal::new(l.p2.y, sys.add_param(y2, true), 1.0)),
|
||||
]
|
||||
}
|
||||
EntityGeom::Circle(circ) => {
|
||||
let (cx, cy, r) = {
|
||||
let store = sys.store();
|
||||
(
|
||||
store.get(circ.center.x),
|
||||
store.get(circ.center.y),
|
||||
store.get(circ.rad),
|
||||
)
|
||||
};
|
||||
vec![
|
||||
Rc::new(Equal::new(circ.center.x, sys.add_param(cx, true), 1.0)),
|
||||
Rc::new(Equal::new(circ.center.y, sys.add_param(cy, true), 1.0)),
|
||||
Rc::new(Equal::new(circ.rad, sys.add_param(r, true), 1.0)),
|
||||
]
|
||||
}
|
||||
// Pinning the 5 intrinsic params (center, radius, both
|
||||
// angles) is enough — `start`/`end` are already tied to
|
||||
// those via the arc-rules `CurveValue` constraints
|
||||
// `solve_scope` adds for every arc, so pinning them too
|
||||
// would just be redundant.
|
||||
EntityGeom::Arc(a) => {
|
||||
let (cx, cy, r, sa, ea) = {
|
||||
let store = sys.store();
|
||||
(
|
||||
store.get(a.circle.center.x),
|
||||
store.get(a.circle.center.y),
|
||||
store.get(a.circle.rad),
|
||||
store.get(a.start_angle),
|
||||
store.get(a.end_angle),
|
||||
)
|
||||
};
|
||||
vec![
|
||||
Rc::new(Equal::new(a.circle.center.x, sys.add_param(cx, true), 1.0)),
|
||||
Rc::new(Equal::new(a.circle.center.y, sys.add_param(cy, true), 1.0)),
|
||||
Rc::new(Equal::new(a.circle.rad, sys.add_param(r, true), 1.0)),
|
||||
Rc::new(Equal::new(a.start_angle, sys.add_param(sa, true), 1.0)),
|
||||
Rc::new(Equal::new(a.end_angle, sys.add_param(ea, true), 1.0)),
|
||||
]
|
||||
}
|
||||
// Pinning center + radmin is enough — `focus1` is already
|
||||
// tied to `center` by the ellipse-rules `Difference`
|
||||
// constraints `solve_scope` adds for every registered
|
||||
// ellipse (see this module's doc comment), so with `center`
|
||||
// pinned here too, pinning `focus1` again would just be
|
||||
// redundant (same reasoning as Arc's `Fixed` arm above).
|
||||
EntityGeom::Ellipse(el) => {
|
||||
let (cx, cy, b) = {
|
||||
let store = sys.store();
|
||||
(
|
||||
store.get(el.center.x),
|
||||
store.get(el.center.y),
|
||||
store.get(el.radmin),
|
||||
)
|
||||
};
|
||||
vec![
|
||||
Rc::new(Equal::new(el.center.x, sys.add_param(cx, true), 1.0)),
|
||||
Rc::new(Equal::new(el.center.y, sys.add_param(cy, true), 1.0)),
|
||||
Rc::new(Equal::new(el.radmin, sys.add_param(b, true), 1.0)),
|
||||
]
|
||||
}
|
||||
EntityGeom::Spline { curve, .. } => {
|
||||
let values: Vec<_> = curve
|
||||
.poles
|
||||
.iter()
|
||||
.map(|point| {
|
||||
let store = sys.store();
|
||||
(point, store.get(point.x), store.get(point.y))
|
||||
})
|
||||
.collect();
|
||||
values
|
||||
.into_iter()
|
||||
.flat_map(|(point, x, y)| {
|
||||
[
|
||||
Rc::new(Equal::new(point.x, sys.add_param(x, true), 1.0))
|
||||
as Rc<dyn Constraint>,
|
||||
Rc::new(Equal::new(point.y, sys.add_param(y, true), 1.0))
|
||||
as Rc<dyn Constraint>,
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
targets = Some(params.iter().map(|param| store.get(*param)).collect());
|
||||
}
|
||||
}
|
||||
}
|
||||
let targets = targets.unwrap_or_else(|| {
|
||||
let store = sys.store();
|
||||
pinned.iter().map(|param| store.get(*param)).collect()
|
||||
});
|
||||
pinned
|
||||
.into_iter()
|
||||
.zip(targets)
|
||||
.map(|(param, value)| {
|
||||
Rc::new(Equal::new(param, sys.add_param(value, true), 1.0))
|
||||
as Rc<dyn Constraint>
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
ConstraintKind::Distance => {
|
||||
let [a, b] = c.refs.as_slice() else {
|
||||
@@ -2343,6 +2284,7 @@ fn solve_scope(
|
||||
retain_lengths: bool,
|
||||
retained_before: &HashMap<Handle, std::sync::Arc<EntityType>>,
|
||||
initial_fixed_refs: &[ParametricRef],
|
||||
originals: &HashMap<Handle, std::sync::Arc<EntityType>>,
|
||||
) -> Option<SolveResult> {
|
||||
let params = if set.local_parameters.is_empty() {
|
||||
drawing_params
|
||||
@@ -2352,6 +2294,53 @@ fn solve_scope(
|
||||
let mut sys = System::new();
|
||||
let mut cache: HashMap<Handle, EntityGeom> = HashMap::new();
|
||||
let constraints = constraints_in_drawing_order(document, set);
|
||||
// A persistent Fixed outranks any edit: a point it holds is never a
|
||||
// driven grip / STRETCH anchor, and a moved entity it holds gets no
|
||||
// temporary whole-entity pin — the Fixed one, targeting the retained
|
||||
// original, pulls it back instead (a MOVE leaves a fixed object, or the
|
||||
// fixed points of a partly fixed one, where they were).
|
||||
let fixed_refs: Vec<ParametricRef> = constraints
|
||||
.iter()
|
||||
.filter(|c| c.enabled && c.kind == ConstraintKind::Fixed)
|
||||
.filter_map(|c| c.refs.first().copied())
|
||||
.collect();
|
||||
let held_by_fixed = |r: &ParametricRef| {
|
||||
fixed_refs.iter().any(|f| {
|
||||
f.entity == r.entity
|
||||
&& (f.marker.is_none()
|
||||
|| f.marker == r.marker
|
||||
|| f.segment_index().is_some_and(|index| {
|
||||
matches!(r.marker, Some(marker)
|
||||
if marker == index as i32 || marker == index as i32 + 1)
|
||||
}))
|
||||
})
|
||||
};
|
||||
let mut driven_refs: Vec<ParametricRef> =
|
||||
driven_refs.iter().copied().filter(|r| !held_by_fixed(r)).collect();
|
||||
// A moved entity a Fixed holds only in part keeps its other primary
|
||||
// points where the edit put them (driven), so the fixed ones pull just
|
||||
// their own coordinates back — a MOVE on a line with one fixed end
|
||||
// stretches the line instead of sliding it.
|
||||
for reference in initial_fixed_refs {
|
||||
if reference.marker.is_some() || !fixed_refs.iter().any(|f| f.entity == reference.entity)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let Some(entity) = document.get_entity(reference.entity) else {
|
||||
continue;
|
||||
};
|
||||
for (marker, _) in super::parametric_constraints::parametric_point_candidates(entity) {
|
||||
let point = ParametricRef::point(reference.entity, marker);
|
||||
if (marker >= 0 || marker == -3)
|
||||
&& !held_by_fixed(&point)
|
||||
&& !driven_refs.contains(&point)
|
||||
{
|
||||
driven_refs.push(point);
|
||||
}
|
||||
}
|
||||
}
|
||||
let driven_refs = driven_refs.as_slice();
|
||||
let fixed_touches = |handle: Handle| fixed_refs.iter().any(|f| f.entity == handle);
|
||||
let line_axes = if retain_lengths { HashMap::new() } else { constrained_line_axes(&constraints) };
|
||||
// Map partition-local kernel rows back to persistent constraint IDs by Rc identity.
|
||||
let mut owner: Vec<(Rc<dyn Constraint>, ConstraintId)> = Vec::new();
|
||||
@@ -2393,6 +2382,7 @@ fn solve_scope(
|
||||
c,
|
||||
tangent_side,
|
||||
tangent_point,
|
||||
originals,
|
||||
) {
|
||||
owner.push((constraint.clone(), c.id));
|
||||
sys.add_constraint(constraint);
|
||||
@@ -2422,6 +2412,9 @@ fn solve_scope(
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
for reference in &temporary_fixed_refs {
|
||||
if fixed_touches(reference.entity) {
|
||||
continue;
|
||||
}
|
||||
let temporary = ParametricConstraint {
|
||||
id: ConstraintId::MAX,
|
||||
kind: ConstraintKind::Fixed,
|
||||
@@ -2443,6 +2436,7 @@ fn solve_scope(
|
||||
&temporary,
|
||||
None,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
) {
|
||||
sys.add_constraint(constraint);
|
||||
}
|
||||
@@ -2596,9 +2590,13 @@ fn solve_scope(
|
||||
// endpoints. Other edits can retain line lengths as well.
|
||||
if retain_size {
|
||||
for (handle, geom) in &cache {
|
||||
if initial_fixed_refs
|
||||
.iter()
|
||||
.any(|reference| reference.entity == *handle && reference.marker.is_none())
|
||||
// A Fixed entity's size is whatever its pins say: whole → all
|
||||
// pinned anyway, partial → the reference lets it resize (MOVE
|
||||
// on a one-end-fixed line shortens it).
|
||||
if fixed_touches(*handle)
|
||||
|| initial_fixed_refs
|
||||
.iter()
|
||||
.any(|reference| reference.entity == *handle && reference.marker.is_none())
|
||||
|| symmetric_constraints.iter().any(|constraint| {
|
||||
constraint.refs.get(2).is_some_and(|axis| axis.entity == *handle)
|
||||
})
|
||||
@@ -3480,6 +3478,7 @@ impl Scene {
|
||||
&candidate,
|
||||
None,
|
||||
None,
|
||||
&HashMap::new(),
|
||||
)
|
||||
.is_empty()
|
||||
{
|
||||
@@ -3577,22 +3576,25 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
|
||||
// Pre-edit images of everything this transaction touched: what a
|
||||
// persistent Fixed pins to, and (under CONSTRAINTSOLVEMODE) what
|
||||
// sizes are retained from.
|
||||
let mut originals: HashMap<Handle, std::sync::Arc<EntityType>> = self
|
||||
.undo_recording
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flat_map(|recording| recording.before.iter())
|
||||
.filter_map(|(handle, entity)| {
|
||||
entity
|
||||
.as_ref()
|
||||
.map(|entity| (*handle, std::sync::Arc::clone(entity)))
|
||||
})
|
||||
.collect();
|
||||
for (handle, entity) in retained_originals {
|
||||
originals.insert(*handle, std::sync::Arc::new(entity.clone()));
|
||||
}
|
||||
let retained_before: HashMap<Handle, std::sync::Arc<EntityType>> = if retain_size {
|
||||
let mut retained: HashMap<Handle, std::sync::Arc<EntityType>> = self
|
||||
.undo_recording
|
||||
.as_ref()
|
||||
.into_iter()
|
||||
.flat_map(|recording| recording.before.iter())
|
||||
.filter_map(|(handle, entity)| {
|
||||
entity
|
||||
.as_ref()
|
||||
.map(|entity| (*handle, std::sync::Arc::clone(entity)))
|
||||
})
|
||||
.collect();
|
||||
for (handle, entity) in retained_originals {
|
||||
retained.insert(*handle, std::sync::Arc::new(entity.clone()));
|
||||
}
|
||||
retained
|
||||
originals.clone()
|
||||
} else {
|
||||
HashMap::new()
|
||||
};
|
||||
@@ -3616,6 +3618,7 @@ impl Scene {
|
||||
true,
|
||||
&retained_before,
|
||||
initial_fixed_refs,
|
||||
&originals,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
@@ -3677,6 +3680,7 @@ impl Scene {
|
||||
false,
|
||||
&retained_before,
|
||||
&[],
|
||||
&retained_before,
|
||||
)
|
||||
else {
|
||||
continue;
|
||||
|
||||
+72
-1
@@ -34,11 +34,16 @@ fn is_compact_coincident_glyph(label: &str) -> bool {
|
||||
matches!(label, "≡" | "∈")
|
||||
}
|
||||
|
||||
/// Fixed's two padlock labels (`parametric_constraints::fixed_glyph_label`).
|
||||
fn is_fixed_glyph(label: &str) -> bool {
|
||||
label == "F" || label == crate::scene::parametric_constraints::FIXED_POINT_GLYPH
|
||||
}
|
||||
|
||||
fn constraint_glyph_size(label: &str) -> Size {
|
||||
if is_compact_coincident_glyph(label) {
|
||||
return Size::new(COINCIDENT_GLYPH_SIZE, COINCIDENT_GLYPH_SIZE);
|
||||
}
|
||||
if label == "G²" {
|
||||
if label == "G²" || is_fixed_glyph(label) {
|
||||
let side = CONSTRAINT_GLYPH_SIZE + CONSTRAINT_GLYPH_PAD_Y * 2.0;
|
||||
return Size::new(side, side);
|
||||
}
|
||||
@@ -94,6 +99,64 @@ fn draw_concentric_constraint_glyph(
|
||||
frame.stroke(&canvas::Path::circle(center, 2.15), stroke);
|
||||
}
|
||||
|
||||
/// The reference bar's padlock: white with a small orange point marker
|
||||
/// when one point is held, red when a whole curve or segment is.
|
||||
fn draw_fixed_constraint_glyph(
|
||||
frame: &mut canvas::Frame,
|
||||
center: Point,
|
||||
text: Color,
|
||||
badge: Color,
|
||||
point_marker: bool,
|
||||
) {
|
||||
let lock = if point_marker {
|
||||
text
|
||||
} else {
|
||||
Color::from_rgb8(214, 76, 76)
|
||||
};
|
||||
let shackle = canvas::Path::new(|builder| {
|
||||
let shackle_center = Point::new(center.x, center.y - 1.6);
|
||||
for step in 0..=12 {
|
||||
let angle = std::f32::consts::PI * (1.0 - step as f32 / 12.0);
|
||||
let point = Point::new(
|
||||
shackle_center.x + angle.cos() * 3.1,
|
||||
shackle_center.y - angle.sin() * 3.1,
|
||||
);
|
||||
if step == 0 {
|
||||
builder.move_to(Point::new(point.x, center.y - 0.6));
|
||||
builder.line_to(point);
|
||||
} else {
|
||||
builder.line_to(point);
|
||||
}
|
||||
}
|
||||
builder.line_to(Point::new(center.x + 3.1, center.y - 0.6));
|
||||
});
|
||||
frame.stroke(
|
||||
&shackle,
|
||||
canvas::Stroke::default().with_color(lock).with_width(1.5),
|
||||
);
|
||||
let body = canvas::Path::rounded_rectangle(
|
||||
Point::new(center.x - 5.0, center.y - 0.8),
|
||||
Size::new(10.0, 7.6),
|
||||
1.4.into(),
|
||||
);
|
||||
frame.fill(&body, lock);
|
||||
frame.stroke(
|
||||
&canvas::Path::line(
|
||||
Point::new(center.x, center.y + 1.4),
|
||||
Point::new(center.x, center.y + 4.6),
|
||||
),
|
||||
canvas::Stroke::default().with_color(badge).with_width(1.3),
|
||||
);
|
||||
if point_marker {
|
||||
frame.stroke(
|
||||
&canvas::Path::rectangle(Point::new(center.x + 4.0, center.y + 3.6), Size::new(4.6, 4.6)),
|
||||
canvas::Stroke::default()
|
||||
.with_color(Color::from_rgb8(224, 130, 62))
|
||||
.with_width(1.2),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn constraint_glyph_box(
|
||||
anchor: Point,
|
||||
outward: [f32; 2],
|
||||
@@ -2020,6 +2083,14 @@ impl canvas::Program<Message> for SelectionCanvas {
|
||||
draw_smooth_constraint_glyph(&mut frame, glyph_center, fg);
|
||||
} else if label == "◎" {
|
||||
draw_concentric_constraint_glyph(&mut frame, glyph_center, fg);
|
||||
} else if is_fixed_glyph(label) {
|
||||
draw_fixed_constraint_glyph(
|
||||
&mut frame,
|
||||
glyph_center,
|
||||
fg,
|
||||
bg,
|
||||
label == crate::scene::parametric_constraints::FIXED_POINT_GLYPH,
|
||||
);
|
||||
} else {
|
||||
frame.fill_text(canvas::Text {
|
||||
content: label.clone(),
|
||||
|
||||
Reference in new issue
Block a user