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,
|
driving_param,
|
||||||
label,
|
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(
|
if let Err(message) = self.tabs[i].scene.validate_parametric_constraint(
|
||||||
kind,
|
kind,
|
||||||
&refs,
|
&refs,
|
||||||
driving_param.as_ref(),
|
driving_param.as_ref(),
|
||||||
) {
|
) {
|
||||||
let keep_smooth_selection = kind
|
if !keep_command {
|
||||||
== crate::scene::parametric_constraints::ConstraintKind::Smooth
|
|
||||||
&& self.tabs[i]
|
|
||||||
.active_cmd
|
|
||||||
.as_ref()
|
|
||||||
.is_some_and(|command| command.name() == "GCSMOOTH");
|
|
||||||
if !keep_smooth_selection {
|
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
}
|
}
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
self.command_line.push_error(message);
|
self.command_line.push_error(message);
|
||||||
if keep_smooth_selection {
|
if keep_command {
|
||||||
if let Some(prompt) =
|
if let Some(prompt) =
|
||||||
self.tabs[i].active_cmd.as_ref().map(|command| command.prompt())
|
self.tabs[i].active_cmd.as_ref().map(|command| command.prompt())
|
||||||
{
|
{
|
||||||
@@ -3208,6 +3208,22 @@ impl OpenCADStudio {
|
|||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
let scope = self.tabs[i].current_parametric_scope();
|
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]
|
let constraints_before = self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
.parametric_constraint_set(scope)
|
.parametric_constraint_set(scope)
|
||||||
@@ -3249,6 +3265,59 @@ impl OpenCADStudio {
|
|||||||
self.commit_undo_delta(i, pd);
|
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 {
|
CmdResult::AddHorizontalConstraint {
|
||||||
selection,
|
selection,
|
||||||
direction,
|
direction,
|
||||||
|
|||||||
@@ -1420,7 +1420,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
"VCONSTRAINT" | "FXCONSTRAINT" => {
|
"VCONSTRAINT" => {
|
||||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||||
if handles.is_empty() {
|
if handles.is_empty() {
|
||||||
use crate::modules::draw::select::SelectObjectsCommand;
|
use crate::modules::draw::select::SelectObjectsCommand;
|
||||||
@@ -1433,20 +1433,49 @@ impl OpenCADStudio {
|
|||||||
} else {
|
} else {
|
||||||
use crate::command::CmdResult;
|
use crate::command::CmdResult;
|
||||||
use crate::scene::parametric_constraints::{ConstraintKind, ParametricRef};
|
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 {
|
return Some(self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
||||||
kind,
|
kind: ConstraintKind::Vertical,
|
||||||
refs: vec![ParametricRef::whole(handles[0])],
|
refs: vec![ParametricRef::whole(handles[0])],
|
||||||
driving_param: None,
|
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" => {
|
"CCONSTRAINT" | "GCCOINCIDENT" => {
|
||||||
use crate::modules::parametric::CoincidentConstraintCommand;
|
use crate::modules::parametric::CoincidentConstraintCommand;
|
||||||
let new_cmd = CoincidentConstraintCommand::new();
|
let new_cmd = CoincidentConstraintCommand::new();
|
||||||
|
|||||||
@@ -1655,6 +1655,11 @@ pub enum CmdResult {
|
|||||||
kind: crate::scene::parametric_constraints::ConstraintKind,
|
kind: crate::scene::parametric_constraints::ConstraintKind,
|
||||||
label: &'static str,
|
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
|
/// Add a persistent `EqualDistance` constraint
|
||||||
/// (`crate::modules::parametric::equal_distance`): the distance
|
/// (`crate::modules::parametric::equal_distance`): the distance
|
||||||
/// between `points[0]`/`points[1]` equals the distance between
|
/// between `points[0]`/`points[1]` equals the distance between
|
||||||
@@ -2891,6 +2896,7 @@ mod constraint_registry_tests {
|
|||||||
"NRCONSTRAINT",
|
"NRCONSTRAINT",
|
||||||
"LCONSTRAINT",
|
"LCONSTRAINT",
|
||||||
"FXCONSTRAINT",
|
"FXCONSTRAINT",
|
||||||
|
"GCFIX",
|
||||||
"SYCONSTRAINT",
|
"SYCONSTRAINT",
|
||||||
"DCONSTRAINT",
|
"DCONSTRAINT",
|
||||||
"ACONSTRAINT",
|
"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 concentric;
|
||||||
mod constraint_bar;
|
mod constraint_bar;
|
||||||
mod equal_distance;
|
mod equal_distance;
|
||||||
|
#[path = "fixed.rs"]
|
||||||
|
mod fixed_command;
|
||||||
mod geom_constraint;
|
mod geom_constraint;
|
||||||
#[path = "horizontal.rs"]
|
#[path = "horizontal.rs"]
|
||||||
mod horizontal_command;
|
mod horizontal_command;
|
||||||
@@ -23,6 +25,7 @@ pub use coincident::{coincident_tool, CoincidentConstraintCommand};
|
|||||||
pub use concentric::ConcentricConstraintCommand;
|
pub use concentric::ConcentricConstraintCommand;
|
||||||
pub use constraint_bar::ConstraintBarOptionCommand;
|
pub use constraint_bar::ConstraintBarOptionCommand;
|
||||||
pub use equal_distance::{equal_distance_tool, EqualDistanceConstraintCommand};
|
pub use equal_distance::{equal_distance_tool, EqualDistanceConstraintCommand};
|
||||||
|
pub use fixed_command::FixConstraintCommand;
|
||||||
pub use geom_constraint::GeomConstraintCommand;
|
pub use geom_constraint::GeomConstraintCommand;
|
||||||
pub use horizontal_command::HorizontalConstraintCommand;
|
pub use horizontal_command::HorizontalConstraintCommand;
|
||||||
pub use perpendicular_command::{PerpendicularConstraintCommand, PerpendicularPick};
|
pub use perpendicular_command::{PerpendicularConstraintCommand, PerpendicularPick};
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ pub mod fixed {
|
|||||||
pub fn tool() -> ToolDef {
|
pub fn tool() -> ToolDef {
|
||||||
ToolDef {
|
ToolDef {
|
||||||
id: "FXCONSTRAINT",
|
id: "FXCONSTRAINT",
|
||||||
label: "Fixed",
|
label: "Fix",
|
||||||
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/fixed.svg")),
|
icon: IconKind::Svg(include_bytes!("../../../assets/icons/constrain/fixed.svg")),
|
||||||
event: ModuleEvent::Command("FXCONSTRAINT".to_string()),
|
event: ModuleEvent::Command("FXCONSTRAINT".to_string()),
|
||||||
}
|
}
|
||||||
@@ -171,6 +171,7 @@ inventory::submit!(crate::command::CommandRegistration {
|
|||||||
"NRCONSTRAINT",
|
"NRCONSTRAINT",
|
||||||
"LCONSTRAINT",
|
"LCONSTRAINT",
|
||||||
"FXCONSTRAINT",
|
"FXCONSTRAINT",
|
||||||
|
"GCFIX",
|
||||||
"SYCONSTRAINT",
|
"SYCONSTRAINT",
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
@@ -593,7 +593,29 @@ impl ParametricConstraintSet {
|
|||||||
///
|
///
|
||||||
/// Solver-side registration reads raw entity fields directly. This helper is
|
/// Solver-side registration reads raw entity fields directly. This helper is
|
||||||
/// for UI-side consumers that need the current world-space position.
|
/// 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> {
|
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 {
|
if marker == -3 {
|
||||||
return match entity {
|
return match entity {
|
||||||
acadrust::EntityType::Circle(circle) => Some(circle.center_wcs()),
|
acadrust::EntityType::Circle(circle) => Some(circle.center_wcs()),
|
||||||
@@ -651,6 +673,12 @@ pub(crate) fn parametric_point_candidates(
|
|||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(marker, point)| (marker as i32, point))
|
.map(|(marker, point)| (marker as i32, point))
|
||||||
.collect();
|
.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 {
|
match entity {
|
||||||
acadrust::EntityType::Circle(circle) => points.push((-3, circle.center_wcs())),
|
acadrust::EntityType::Circle(circle) => points.push((-3, circle.center_wcs())),
|
||||||
acadrust::EntityType::Arc(arc) => points.push((-3, arc.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
|
/// The full glyph text for one constraint: its symbol, plus the driving
|
||||||
/// value for a dimensional kind (Distance/Angle/Radius).
|
/// value for a dimensional kind (Distance/Angle/Radius).
|
||||||
pub(crate) fn glyph_label(constraint: &ParametricConstraint) -> String {
|
pub(crate) fn glyph_label(constraint: &ParametricConstraint) -> String {
|
||||||
@@ -1672,7 +1717,9 @@ impl super::Scene {
|
|||||||
})
|
})
|
||||||
.flat_map(|c| {
|
.flat_map(|c| {
|
||||||
let is_conflicting = set.conflicts.iter().any(|(id, _)| *id == c.id);
|
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)
|
glyph_label(c)
|
||||||
} else {
|
} else {
|
||||||
c.kind.glyph_symbol().to_string()
|
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::lm::solve_lm;
|
||||||
use cadkernel_constraints::solvers::SolveStatus;
|
use cadkernel_constraints::solvers::SolveStatus;
|
||||||
use cadkernel_constraints::system::System;
|
use cadkernel_constraints::system::System;
|
||||||
|
use cadkernel_constraints::util::ParamId;
|
||||||
|
|
||||||
use super::named_parameters::ParameterTable;
|
use super::named_parameters::ParameterTable;
|
||||||
use super::parametric_constraints::{
|
use super::parametric_constraints::{
|
||||||
@@ -184,6 +185,16 @@ fn register_entity(
|
|||||||
handle: Handle,
|
handle: Handle,
|
||||||
) -> Option<EntityGeom> {
|
) -> Option<EntityGeom> {
|
||||||
let entity = document.get_entity(handle)?;
|
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 {
|
match entity {
|
||||||
EntityType::Point(point) => Some(EntityGeom::Point(GPoint::new(
|
EntityType::Point(point) => Some(EntityGeom::Point(GPoint::new(
|
||||||
sys.add_param(point.location.x, false),
|
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)
|
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
|
/// Builds the kernel equations for one constraint. Unsupported references or
|
||||||
/// invalid dimensional targets produce no equations.
|
/// invalid dimensional targets produce no equations.
|
||||||
fn build_constraint(
|
fn build_constraint(
|
||||||
@@ -1037,6 +1116,7 @@ fn build_constraint(
|
|||||||
c: &ParametricConstraint,
|
c: &ParametricConstraint,
|
||||||
tangent_side: Option<bool>,
|
tangent_side: Option<bool>,
|
||||||
tangent_point: Option<ParametricRef>,
|
tangent_point: Option<ParametricRef>,
|
||||||
|
originals: &HashMap<Handle, std::sync::Arc<EntityType>>,
|
||||||
) -> Vec<Rc<dyn Constraint>> {
|
) -> Vec<Rc<dyn Constraint>> {
|
||||||
if !c.enabled || !refs_share_supported_plane(document, &c.refs) {
|
if !c.enabled || !refs_share_supported_plane(document, &c.refs) {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
@@ -1448,175 +1528,36 @@ fn build_constraint(
|
|||||||
let Some(r) = c.refs.first() else {
|
let Some(r) = c.refs.first() else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
};
|
};
|
||||||
if r.segment_index().is_some() {
|
let pinned = fixed_pin_params(document, sys, cache, *r);
|
||||||
let Some(line) = whole_line(sys, cache, *r) else {
|
// A persistent Fixed holds the *pre-edit* geometry: after MOVE /
|
||||||
return Vec::new();
|
// STRETCH / a grip drag the entity in `document` is already
|
||||||
};
|
// displaced, so the target comes from the caller's retained
|
||||||
let (x1, y1, x2, y2) = {
|
// original when there is one — the entity's own live values
|
||||||
let store = sys.store();
|
// otherwise (a freshly applied Fixed pins where it stands).
|
||||||
(
|
let mut targets: Option<Vec<f64>> = None;
|
||||||
store.get(line.p1.x),
|
if let Some(original) = originals.get(&r.entity) {
|
||||||
store.get(line.p1.y),
|
let mut original_cache = HashMap::new();
|
||||||
store.get(line.p2.x),
|
if let Some(geometry) = register_entity_geom(sys, r.entity, original.as_ref()) {
|
||||||
store.get(line.p2.y),
|
original_cache.insert(r.entity, geometry);
|
||||||
)
|
let params = fixed_pin_params(document, sys, &mut original_cache, *r);
|
||||||
};
|
if params.len() == pinned.len() {
|
||||||
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 store = sys.store();
|
let store = sys.store();
|
||||||
(store.get(point.x), store.get(point.y))
|
targets = Some(params.iter().map(|param| store.get(*param)).collect());
|
||||||
};
|
}
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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 => {
|
ConstraintKind::Distance => {
|
||||||
let [a, b] = c.refs.as_slice() else {
|
let [a, b] = c.refs.as_slice() else {
|
||||||
@@ -2343,6 +2284,7 @@ fn solve_scope(
|
|||||||
retain_lengths: bool,
|
retain_lengths: bool,
|
||||||
retained_before: &HashMap<Handle, std::sync::Arc<EntityType>>,
|
retained_before: &HashMap<Handle, std::sync::Arc<EntityType>>,
|
||||||
initial_fixed_refs: &[ParametricRef],
|
initial_fixed_refs: &[ParametricRef],
|
||||||
|
originals: &HashMap<Handle, std::sync::Arc<EntityType>>,
|
||||||
) -> Option<SolveResult> {
|
) -> Option<SolveResult> {
|
||||||
let params = if set.local_parameters.is_empty() {
|
let params = if set.local_parameters.is_empty() {
|
||||||
drawing_params
|
drawing_params
|
||||||
@@ -2352,6 +2294,53 @@ fn solve_scope(
|
|||||||
let mut sys = System::new();
|
let mut sys = System::new();
|
||||||
let mut cache: HashMap<Handle, EntityGeom> = HashMap::new();
|
let mut cache: HashMap<Handle, EntityGeom> = HashMap::new();
|
||||||
let constraints = constraints_in_drawing_order(document, set);
|
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) };
|
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.
|
// Map partition-local kernel rows back to persistent constraint IDs by Rc identity.
|
||||||
let mut owner: Vec<(Rc<dyn Constraint>, ConstraintId)> = Vec::new();
|
let mut owner: Vec<(Rc<dyn Constraint>, ConstraintId)> = Vec::new();
|
||||||
@@ -2393,6 +2382,7 @@ fn solve_scope(
|
|||||||
c,
|
c,
|
||||||
tangent_side,
|
tangent_side,
|
||||||
tangent_point,
|
tangent_point,
|
||||||
|
originals,
|
||||||
) {
|
) {
|
||||||
owner.push((constraint.clone(), c.id));
|
owner.push((constraint.clone(), c.id));
|
||||||
sys.add_constraint(constraint);
|
sys.add_constraint(constraint);
|
||||||
@@ -2422,6 +2412,9 @@ fn solve_scope(
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
for reference in &temporary_fixed_refs {
|
for reference in &temporary_fixed_refs {
|
||||||
|
if fixed_touches(reference.entity) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let temporary = ParametricConstraint {
|
let temporary = ParametricConstraint {
|
||||||
id: ConstraintId::MAX,
|
id: ConstraintId::MAX,
|
||||||
kind: ConstraintKind::Fixed,
|
kind: ConstraintKind::Fixed,
|
||||||
@@ -2443,6 +2436,7 @@ fn solve_scope(
|
|||||||
&temporary,
|
&temporary,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
&HashMap::new(),
|
||||||
) {
|
) {
|
||||||
sys.add_constraint(constraint);
|
sys.add_constraint(constraint);
|
||||||
}
|
}
|
||||||
@@ -2596,9 +2590,13 @@ fn solve_scope(
|
|||||||
// endpoints. Other edits can retain line lengths as well.
|
// endpoints. Other edits can retain line lengths as well.
|
||||||
if retain_size {
|
if retain_size {
|
||||||
for (handle, geom) in &cache {
|
for (handle, geom) in &cache {
|
||||||
if initial_fixed_refs
|
// A Fixed entity's size is whatever its pins say: whole → all
|
||||||
.iter()
|
// pinned anyway, partial → the reference lets it resize (MOVE
|
||||||
.any(|reference| reference.entity == *handle && reference.marker.is_none())
|
// 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| {
|
|| symmetric_constraints.iter().any(|constraint| {
|
||||||
constraint.refs.get(2).is_some_and(|axis| axis.entity == *handle)
|
constraint.refs.get(2).is_some_and(|axis| axis.entity == *handle)
|
||||||
})
|
})
|
||||||
@@ -3480,6 +3478,7 @@ impl Scene {
|
|||||||
&candidate,
|
&candidate,
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
|
&HashMap::new(),
|
||||||
)
|
)
|
||||||
.is_empty()
|
.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 retained_before: HashMap<Handle, std::sync::Arc<EntityType>> = if retain_size {
|
||||||
let mut retained: HashMap<Handle, std::sync::Arc<EntityType>> = self
|
originals.clone()
|
||||||
.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
|
|
||||||
} else {
|
} else {
|
||||||
HashMap::new()
|
HashMap::new()
|
||||||
};
|
};
|
||||||
@@ -3616,6 +3618,7 @@ impl Scene {
|
|||||||
true,
|
true,
|
||||||
&retained_before,
|
&retained_before,
|
||||||
initial_fixed_refs,
|
initial_fixed_refs,
|
||||||
|
&originals,
|
||||||
) else {
|
) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -3677,6 +3680,7 @@ impl Scene {
|
|||||||
false,
|
false,
|
||||||
&retained_before,
|
&retained_before,
|
||||||
&[],
|
&[],
|
||||||
|
&retained_before,
|
||||||
)
|
)
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
|
|||||||
+72
-1
@@ -34,11 +34,16 @@ fn is_compact_coincident_glyph(label: &str) -> bool {
|
|||||||
matches!(label, "≡" | "∈")
|
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 {
|
fn constraint_glyph_size(label: &str) -> Size {
|
||||||
if is_compact_coincident_glyph(label) {
|
if is_compact_coincident_glyph(label) {
|
||||||
return Size::new(COINCIDENT_GLYPH_SIZE, COINCIDENT_GLYPH_SIZE);
|
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;
|
let side = CONSTRAINT_GLYPH_SIZE + CONSTRAINT_GLYPH_PAD_Y * 2.0;
|
||||||
return Size::new(side, side);
|
return Size::new(side, side);
|
||||||
}
|
}
|
||||||
@@ -94,6 +99,64 @@ fn draw_concentric_constraint_glyph(
|
|||||||
frame.stroke(&canvas::Path::circle(center, 2.15), stroke);
|
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(
|
fn constraint_glyph_box(
|
||||||
anchor: Point,
|
anchor: Point,
|
||||||
outward: [f32; 2],
|
outward: [f32; 2],
|
||||||
@@ -2020,6 +2083,14 @@ impl canvas::Program<Message> for SelectionCanvas {
|
|||||||
draw_smooth_constraint_glyph(&mut frame, glyph_center, fg);
|
draw_smooth_constraint_glyph(&mut frame, glyph_center, fg);
|
||||||
} else if label == "◎" {
|
} else if label == "◎" {
|
||||||
draw_concentric_constraint_glyph(&mut frame, glyph_center, fg);
|
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 {
|
} else {
|
||||||
frame.fill_text(canvas::Text {
|
frame.fill_text(canvas::Text {
|
||||||
content: label.clone(),
|
content: label.clone(),
|
||||||
|
|||||||
Reference in new issue
Block a user