forked from 1b-agent/opencadstudio-upstream
Complete Coincident command behavior
This commit is contained in:
1 parent
dfe8b77cca
commit
ba2b371269
13 files changed
+1183
-207
No files matched your search
+183
-37
@@ -2082,10 +2082,17 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = None;
|
||||
return Task::none();
|
||||
}
|
||||
if matches!(&transform, crate::command::EntityTransform::Translate(_)) {
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
handles = self.tabs[i]
|
||||
.scene
|
||||
.parametric_connected_handles(scope, &handles);
|
||||
handles.retain(|handle| !self.tabs[i].scene.is_layer_locked(*handle));
|
||||
}
|
||||
let label = self.history_label_from_active_cmd(i, "MOVE");
|
||||
// A move/rotate/scale/mirror mutates only the selected entities
|
||||
// (and their baked dimension sub-entities) through
|
||||
// transform_entities — always delta-safe.
|
||||
// A translation carries its complete connected constraint
|
||||
// component so each member keeps its own dimensions. Other
|
||||
// transforms retain their selected-object behavior.
|
||||
let pending = self.begin_undo(i, label, handles.len(), true);
|
||||
self.tabs[i].scene.transform_entities(&handles, &transform);
|
||||
self.tabs[i].dirty = true;
|
||||
@@ -2955,44 +2962,165 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
CmdResult::AddCoincidentConstraint {
|
||||
point_a,
|
||||
point_b,
|
||||
first,
|
||||
second,
|
||||
multiple,
|
||||
label,
|
||||
} => {
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let to_world = |p: glam::DVec3| acadrust::types::Vector3::new(p.x, p.y, p.z);
|
||||
let resolved_a = crate::scene::parametric_constraints::nearest_parametric_point(
|
||||
&self.tabs[i].scene.document,
|
||||
scope,
|
||||
to_world(point_a),
|
||||
None,
|
||||
);
|
||||
let resolved_b = resolved_a.and_then(|ref_a| {
|
||||
crate::scene::parametric_constraints::nearest_parametric_point(
|
||||
&self.tabs[i].scene.document,
|
||||
scope,
|
||||
to_world(point_b),
|
||||
None,
|
||||
)
|
||||
.filter(|ref_b| *ref_b != ref_a)
|
||||
});
|
||||
match (resolved_a, resolved_b) {
|
||||
(Some(ref_a), Some(ref_b)) => {
|
||||
return self.apply_cmd_result(CmdResult::AddParametricConstraint {
|
||||
kind: crate::scene::parametric_constraints::ConstraintKind::Coincident,
|
||||
refs: vec![ref_a, ref_b],
|
||||
driving_param: None,
|
||||
label,
|
||||
});
|
||||
let resolve = |pick: crate::command::CoincidentPick,
|
||||
exclude: Option<Handle>| {
|
||||
if pick.whole_curve {
|
||||
pick.handle.and_then(|handle| {
|
||||
crate::scene::parametric_constraints::parametric_curve_ref_for_pick(
|
||||
&self.tabs[i].scene.document,
|
||||
scope,
|
||||
handle,
|
||||
to_world(pick.point),
|
||||
)
|
||||
})
|
||||
} else if let Some(handle) = pick.handle {
|
||||
crate::scene::parametric_constraints::nearest_parametric_point_on_entity(
|
||||
&self.tabs[i].scene.document,
|
||||
scope,
|
||||
handle,
|
||||
to_world(pick.point),
|
||||
)
|
||||
} else {
|
||||
crate::scene::parametric_constraints::nearest_parametric_point(
|
||||
&self.tabs[i].scene.document,
|
||||
scope,
|
||||
to_world(pick.point),
|
||||
exclude,
|
||||
)
|
||||
}
|
||||
};
|
||||
let resolved_first = resolve(first, None);
|
||||
let resolved_second = resolved_first
|
||||
.and_then(|reference| resolve(second, Some(reference.entity)));
|
||||
let (Some(first_ref), Some(second_ref)) = (resolved_first, resolved_second) else {
|
||||
self.command_line.push_error(
|
||||
"Coincident: select a supported endpoint, center, midpoint, vertex, or curve.",
|
||||
);
|
||||
return Task::none();
|
||||
};
|
||||
let (kind, refs) = match (first.whole_curve, second.whole_curve) {
|
||||
(false, false) if first_ref != second_ref => (
|
||||
crate::scene::parametric_constraints::ConstraintKind::Coincident,
|
||||
vec![first_ref, second_ref],
|
||||
),
|
||||
(true, false) => (
|
||||
crate::scene::parametric_constraints::ConstraintKind::PointOnCurve,
|
||||
vec![second_ref, first_ref],
|
||||
),
|
||||
(false, true) => (
|
||||
crate::scene::parametric_constraints::ConstraintKind::PointOnCurve,
|
||||
vec![first_ref, second_ref],
|
||||
),
|
||||
_ => {
|
||||
self.command_line.push_error(
|
||||
"Coincident: pick didn't land on a point (endpoint, center, or vertex) — enable an Endpoint/Center object snap and try again.",
|
||||
);
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.command_line
|
||||
.push_error("Coincident: select one point and one curve, or two points.");
|
||||
return Task::none();
|
||||
}
|
||||
};
|
||||
if let Err(error) = self.tabs[i]
|
||||
.scene
|
||||
.validate_parametric_constraint(kind, &refs, None)
|
||||
{
|
||||
self.command_line.push_error(error);
|
||||
return Task::none();
|
||||
}
|
||||
let touched: Vec<_> = refs.iter().map(|reference| reference.entity).collect();
|
||||
let constraints_before = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
crate::scene::parametric_constraints::ParametricConstraintSet::new(scope)
|
||||
});
|
||||
let pending = self.begin_undo(i, label, touched.len(), true);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_parametric_constraints_before(scope, constraints_before);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set_mut(scope)
|
||||
.add(kind, refs, None);
|
||||
let changes: Vec<_> = touched
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities_with_parametric_driven(&changes, &[first_ref]);
|
||||
self.tabs[i].dirty = true;
|
||||
self.tabs[i].snap_result = None;
|
||||
if !multiple {
|
||||
self.tabs[i].active_cmd = None;
|
||||
}
|
||||
self.command_line.push_output("Coincident constraint applied.");
|
||||
if multiple {
|
||||
if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|cmd| cmd.prompt()) {
|
||||
self.command_line.push_info(&prompt);
|
||||
}
|
||||
self.command_line.set_step_options(
|
||||
self.tabs[i]
|
||||
.active_cmd
|
||||
.as_ref()
|
||||
.map(|cmd| cmd.options())
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
}
|
||||
self.refresh_properties();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
CmdResult::AddAutoCoincidentConstraints { handles } => {
|
||||
use crate::scene::parametric_constraints::ConstraintKind;
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let inferred = self.tabs[i]
|
||||
.scene
|
||||
.inferred_coincident_constraints(scope, &handles);
|
||||
let count = inferred.len();
|
||||
if count > 0 {
|
||||
let constraints_before = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| {
|
||||
crate::scene::parametric_constraints::ParametricConstraintSet::new(
|
||||
scope,
|
||||
)
|
||||
});
|
||||
let pending = self.begin_undo(i, "Coincident auto constrain", handles.len(), true);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.record_undo_parametric_constraints_before(scope, constraints_before);
|
||||
for refs in inferred {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set_mut(scope)
|
||||
.add(ConstraintKind::Coincident, refs, None);
|
||||
}
|
||||
let changes: Vec<_> = handles
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified))
|
||||
.collect();
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
self.tabs[i].dirty = true;
|
||||
self.refresh_properties();
|
||||
if let Some(pd) = pending {
|
||||
self.commit_undo_delta(i, pd);
|
||||
}
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.command_line
|
||||
.push_output(format!("{} coincident constraint(s) applied.", count).as_str());
|
||||
}
|
||||
CmdResult::AddPointOnEntityConstraint {
|
||||
point,
|
||||
@@ -8276,8 +8404,17 @@ mod parametric_constraint_undo_tests {
|
||||
let b = add_line(&mut app, 20.0, 0.0, 20.0, 5.0); // deliberately far from `a`, so the two picks are unambiguous
|
||||
|
||||
let _ = app.apply_cmd_result(CmdResult::AddCoincidentConstraint {
|
||||
point_a: glam::DVec3::new(5.0, 0.0, 0.0), // a's end
|
||||
point_b: glam::DVec3::new(20.0, 0.0, 0.0), // b's start
|
||||
first: crate::command::CoincidentPick {
|
||||
handle: Some(a),
|
||||
point: glam::DVec3::new(5.0, 0.0, 0.0),
|
||||
whole_curve: false,
|
||||
},
|
||||
second: crate::command::CoincidentPick {
|
||||
handle: Some(b),
|
||||
point: glam::DVec3::new(20.0, 0.0, 0.0),
|
||||
whole_curve: false,
|
||||
},
|
||||
multiple: false,
|
||||
label: "Coincident constraint",
|
||||
});
|
||||
|
||||
@@ -8305,8 +8442,17 @@ mod parametric_constraint_undo_tests {
|
||||
let _a = add_line(&mut app, 0.0, 0.0, 5.0, 0.0);
|
||||
|
||||
let _ = app.apply_cmd_result(CmdResult::AddCoincidentConstraint {
|
||||
point_a: glam::DVec3::new(5.0, 0.0, 0.0), // a's end — a real point
|
||||
point_b: glam::DVec3::new(500.0, 500.0, 0.0), // nowhere near anything
|
||||
first: crate::command::CoincidentPick {
|
||||
handle: Some(_a),
|
||||
point: glam::DVec3::new(5.0, 0.0, 0.0),
|
||||
whole_curve: false,
|
||||
},
|
||||
second: crate::command::CoincidentPick {
|
||||
handle: None,
|
||||
point: glam::DVec3::new(500.0, 500.0, 0.0),
|
||||
whole_curve: false,
|
||||
},
|
||||
multiple: false,
|
||||
label: "Coincident constraint",
|
||||
});
|
||||
|
||||
|
||||
@@ -1300,13 +1300,20 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
"CCONSTRAINT" => {
|
||||
"CCONSTRAINT" | "GCCOINCIDENT" => {
|
||||
use crate::modules::parametric::CoincidentConstraintCommand;
|
||||
let new_cmd = CoincidentConstraintCommand::new();
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
"GEOMCONSTRAINT" => {
|
||||
use crate::modules::parametric::GeomConstraintCommand;
|
||||
let new_cmd = GeomConstraintCommand::new();
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
"EDCONSTRAINT" => {
|
||||
use crate::modules::parametric::EqualDistanceConstraintCommand;
|
||||
let new_cmd = EqualDistanceConstraintCommand::new();
|
||||
|
||||
+11
-71
@@ -882,12 +882,21 @@ impl OpenCADStudio {
|
||||
}
|
||||
|
||||
if !compact_solid {
|
||||
sections.extend(crate::entities::object_data::sections(
|
||||
let mut object_sections = crate::entities::object_data::sections(
|
||||
&self.tabs[i].scene.document,
|
||||
&self.tabs[i].scene.object_data_cache,
|
||||
handle,
|
||||
entity,
|
||||
));
|
||||
);
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let constrained = self.tabs[i]
|
||||
.scene
|
||||
.parametric_constraint_set(scope)
|
||||
.is_some_and(|set| set.constraints_touching(handle).next().is_some());
|
||||
if constrained {
|
||||
object_sections.retain(|section| section.title != "Associative Data");
|
||||
}
|
||||
sections.extend(object_sections);
|
||||
}
|
||||
|
||||
{
|
||||
@@ -2225,75 +2234,6 @@ impl OpenCADStudio {
|
||||
if compact_solid {
|
||||
retain_compact_solid_sections(&mut sections);
|
||||
}
|
||||
// Parametric-constraint list for this entity —
|
||||
// the Properties-panel analogue of the viewport's
|
||||
// constraint glyph pills: one row per constraint
|
||||
// touching `handle`, clickable to select every entity
|
||||
// it relates (PropConstraintLinkClick). Omitted
|
||||
// entirely when the entity has none — an unconstrained
|
||||
// entity (the common case) shouldn't gain empty
|
||||
// bookkeeping.
|
||||
{
|
||||
let scope = self.tabs[i].current_parametric_scope();
|
||||
let scene = &self.tabs[i].scene;
|
||||
if let Some(set) = scene.parametric_constraint_set(scope) {
|
||||
let parameters = if set.local_parameters.is_empty() {
|
||||
scene.named_parameters()
|
||||
} else {
|
||||
&set.local_parameters
|
||||
};
|
||||
let props: Vec<crate::scene::model::object::Property> = set
|
||||
.constraints_touching(handle)
|
||||
.map(|c| {
|
||||
let conflicting =
|
||||
set.conflicts.iter().any(|(id, _)| *id == c.id);
|
||||
let value_suffix = match &c.driving_param {
|
||||
Some(
|
||||
crate::scene::named_parameters::DrivingValue::Literal(
|
||||
v,
|
||||
),
|
||||
) => {
|
||||
format!(" = {v:.2}")
|
||||
}
|
||||
Some(
|
||||
crate::scene::named_parameters::DrivingValue::Named(
|
||||
name,
|
||||
),
|
||||
) => match parameters.resolve(name) {
|
||||
Ok(v) => format!(" = {name} ({v:.2})"),
|
||||
Err(_) => format!(" = {name} (?)"),
|
||||
},
|
||||
None => String::new(),
|
||||
};
|
||||
let label = format!(
|
||||
"{} {:?}{}",
|
||||
c.kind.glyph_symbol(),
|
||||
c.kind,
|
||||
value_suffix
|
||||
);
|
||||
let mut handles: Vec<acadrust::Handle> =
|
||||
c.refs.iter().map(|r| r.entity).collect();
|
||||
handles.sort_by_key(|h| h.value());
|
||||
handles.dedup();
|
||||
crate::scene::model::object::Property {
|
||||
label,
|
||||
field: "parametric_constraint",
|
||||
value: crate::scene::model::object::PropValue::EntityLink {
|
||||
id: c.id,
|
||||
handles,
|
||||
conflicting,
|
||||
},
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if !props.is_empty() {
|
||||
sections.push(crate::scene::model::object::PropSection {
|
||||
title: t!("Constraints").into_owned(),
|
||||
props,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
let title = match entity {
|
||||
acadrust::EntityType::Insert(ins) => {
|
||||
let is_xref = self.tabs[i]
|
||||
|
||||
+25
-5
@@ -1237,6 +1237,17 @@ pub enum ExtrudeExtent {
|
||||
Path(Handle),
|
||||
}
|
||||
|
||||
/// One ordered selection used by the Coincident command. A click can name
|
||||
/// either a constraint point on an entity or the entity's whole curve. Typed
|
||||
/// coordinates have no entity handle and are resolved by the host after the
|
||||
/// command returns.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct CoincidentPick {
|
||||
pub handle: Option<Handle>,
|
||||
pub point: DVec3,
|
||||
pub whole_curve: bool,
|
||||
}
|
||||
|
||||
/// Construction options shared by SWEEP creation and its live preview.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct SweepOptions {
|
||||
@@ -1466,14 +1477,21 @@ pub enum CmdResult {
|
||||
/// Undo-history label, e.g. `"Horizontal constraint"`.
|
||||
label: &'static str,
|
||||
},
|
||||
/// Adds a Coincident constraint between the sub-entity points nearest
|
||||
/// the two picked positions. The host resolves the raw points because
|
||||
/// `CadCommand` has no document access.
|
||||
/// Adds an ordered Coincident relation. Point/point selections create a
|
||||
/// Coincident constraint; point/curve selections create the corresponding
|
||||
/// point-on-curve relation while retaining Coincident command semantics.
|
||||
AddCoincidentConstraint {
|
||||
point_a: DVec3,
|
||||
point_b: DVec3,
|
||||
first: CoincidentPick,
|
||||
second: CoincidentPick,
|
||||
/// Keep the first curve active and accept another point.
|
||||
multiple: bool,
|
||||
label: &'static str,
|
||||
},
|
||||
/// Apply only Coincident relations that already exist geometrically in
|
||||
/// the selected set.
|
||||
AddAutoCoincidentConstraints {
|
||||
handles: Vec<Handle>,
|
||||
},
|
||||
/// Add a persistent `CenterPoint`/`Midpoint`/`PointOnCurve` constraint
|
||||
/// (`crate::modules::parametric::point_on_entity`) between one
|
||||
/// picked point and `target`, a whole entity selected before the tool
|
||||
@@ -2681,6 +2699,8 @@ mod constraint_registry_tests {
|
||||
let names = all_registered_command_names();
|
||||
for id in [
|
||||
"CCONSTRAINT",
|
||||
"GCCOINCIDENT",
|
||||
"GEOMCONSTRAINT",
|
||||
"EDCONSTRAINT",
|
||||
"CPCONSTRAINT",
|
||||
"MPCONSTRAINT",
|
||||
|
||||
@@ -214,6 +214,9 @@ fn properties(line: &Line) -> Vec<PropSection> {
|
||||
ro(t!("Delta Z").as_ref(), "delta_z", format!("{dz:.4}")),
|
||||
ro(t!("Length").as_ref(), "length", format!("{:.4}", line.length())),
|
||||
ro(t!("Angle").as_ref(), "angle", format!("{angle:.2}")),
|
||||
edit(t!("Normal X").as_ref(), "normal_x", line.normal.x),
|
||||
edit(t!("Normal Y").as_ref(), "normal_y", line.normal.y),
|
||||
edit(t!("Normal Z").as_ref(), "normal_z", line.normal.z),
|
||||
],
|
||||
}]
|
||||
}
|
||||
@@ -286,6 +289,19 @@ fn apply_geom_prop(line: &mut Line, field: &str, value: &str) {
|
||||
"end_x" => line.end.x = v,
|
||||
"end_y" => line.end.y = v,
|
||||
"end_z" => line.end.z = v,
|
||||
"normal_x" | "normal_y" | "normal_z" => {
|
||||
let mut normal = glam::DVec3::new(line.normal.x, line.normal.y, line.normal.z);
|
||||
match field {
|
||||
"normal_x" => normal.x = v,
|
||||
"normal_y" => normal.y = v,
|
||||
"normal_z" => normal.z = v,
|
||||
_ => unreachable!(),
|
||||
}
|
||||
if normal.is_finite() && normal.length_squared() > 1.0e-18 {
|
||||
let normal = normal.normalize();
|
||||
line.normal = acadrust::types::Vector3::new(normal.x, normal.y, normal.z);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,10 @@
|
||||
//! CCONSTRAINT — manual Coincident constraint input.
|
||||
//! unlike the rest of this module's plain select-and-click constraints,
|
||||
//! Coincident addresses a *point* on an entity (an endpoint, a circle/arc
|
||||
//! center), not the whole entity, so it needs two point picks instead of a
|
||||
//! selection.
|
||||
//!
|
||||
//! A `CadCommand` has no document access (see `value.rs`'s module doc
|
||||
//! comment for the same constraint on `DCONSTRAINT`/`ACONSTRAINT`), so this
|
||||
//! command only accumulates the two raw points `on_point` receives and hands
|
||||
//! them back as `CmdResult::AddCoincidentConstraint` — the host resolves
|
||||
//! each point to a real `ParametricRef` (`parametric_constraints::nearest_parametric_point`)
|
||||
//! and adds the constraint, the same split `ReassociateCenterMark` already
|
||||
//! uses for a similar reason.
|
||||
//! Interactive Coincident input: ordered point/curve picks, repeated
|
||||
//! point-to-curve placement, and Coincident-only automatic inference.
|
||||
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, CoincidentPick};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub mod coincident_tool {
|
||||
@@ -31,45 +21,272 @@ pub mod coincident_tool {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
#[derive(Clone)]
|
||||
enum Step {
|
||||
First,
|
||||
FirstObject,
|
||||
Second(CoincidentPick),
|
||||
PointsOnCurve {
|
||||
curve: CoincidentPick,
|
||||
multiple: bool,
|
||||
},
|
||||
AutoConstrain {
|
||||
handles: Vec<Handle>,
|
||||
},
|
||||
}
|
||||
|
||||
pub struct CoincidentConstraintCommand {
|
||||
first: Option<DVec3>,
|
||||
step: Step,
|
||||
picked_entity: Option<EntityType>,
|
||||
}
|
||||
|
||||
impl CoincidentConstraintCommand {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
Self {
|
||||
step: Step::First,
|
||||
picked_entity: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn point_pick(handle: Option<Handle>, point: DVec3) -> CoincidentPick {
|
||||
CoincidentPick {
|
||||
handle,
|
||||
point,
|
||||
whole_curve: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn curve_pick(handle: Handle, point: DVec3) -> CoincidentPick {
|
||||
CoincidentPick {
|
||||
handle: Some(handle),
|
||||
point,
|
||||
whole_curve: true,
|
||||
}
|
||||
}
|
||||
|
||||
fn picked_constraint_point(entity: &EntityType, point: DVec3) -> bool {
|
||||
crate::scene::parametric_constraints::is_parametric_point_near(
|
||||
entity,
|
||||
acadrust::types::Vector3::new(point.x, point.y, point.z),
|
||||
)
|
||||
}
|
||||
|
||||
fn valid_curve(entity: &EntityType) -> bool {
|
||||
matches!(
|
||||
entity,
|
||||
EntityType::Line(_)
|
||||
| EntityType::LwPolyline(_)
|
||||
| EntityType::Polyline2D(_)
|
||||
| EntityType::Circle(_)
|
||||
| EntityType::Arc(_)
|
||||
| EntityType::Ellipse(_)
|
||||
| EntityType::Spline(_)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for CoincidentConstraintCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"CCONSTRAINT"
|
||||
"GCCOINCIDENT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.first.is_none() {
|
||||
"COINCIDENT Specify first point:".to_string()
|
||||
} else {
|
||||
"COINCIDENT Specify second point:".to_string()
|
||||
match self.step.clone() {
|
||||
Step::First => {
|
||||
"COINCIDENT Select first point or [Object/Autoconstrain] <Object>:".to_string()
|
||||
}
|
||||
Step::FirstObject => "COINCIDENT Select object:".to_string(),
|
||||
Step::Second(_) => "COINCIDENT Select second point or object:".to_string(),
|
||||
Step::PointsOnCurve { multiple: false, .. } => {
|
||||
"COINCIDENT Select point or [Multiple]:".to_string()
|
||||
}
|
||||
Step::PointsOnCurve { multiple: true, .. } => {
|
||||
"COINCIDENT Select points to coincide with the first object (Enter = done):"
|
||||
.to_string()
|
||||
}
|
||||
Step::AutoConstrain { handles } if handles.is_empty() => {
|
||||
"COINCIDENT Select objects:".to_string()
|
||||
}
|
||||
Step::AutoConstrain { handles } => format!(
|
||||
"COINCIDENT Select objects ({} selected, Enter = apply):",
|
||||
handles.len()
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
match self.first {
|
||||
None => {
|
||||
self.first = Some(pt);
|
||||
match &self.step {
|
||||
Step::First => {
|
||||
self.step = Step::Second(Self::point_pick(None, pt));
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Some(first) => CmdResult::AddCoincidentConstraint {
|
||||
point_a: first,
|
||||
point_b: pt,
|
||||
Step::FirstObject => CmdResult::NeedPoint,
|
||||
Step::Second(first) => CmdResult::AddCoincidentConstraint {
|
||||
first: *first,
|
||||
second: Self::point_pick(None, pt),
|
||||
multiple: false,
|
||||
label: "Coincident constraint",
|
||||
},
|
||||
Step::PointsOnCurve { curve, multiple } => CmdResult::AddCoincidentConstraint {
|
||||
first: *curve,
|
||||
second: Self::point_pick(None, pt),
|
||||
multiple: *multiple,
|
||||
label: "Coincident constraint",
|
||||
},
|
||||
Step::AutoConstrain { .. } => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step.clone() {
|
||||
Step::First => vec![
|
||||
CmdOption::new("Object", "O"),
|
||||
CmdOption::new("Autoconstrain", "A"),
|
||||
],
|
||||
Step::PointsOnCurve { multiple: false, .. } => vec![
|
||||
CmdOption::new("Point", "P"),
|
||||
CmdOption::new("Multiple", "M"),
|
||||
],
|
||||
Step::PointsOnCurve { multiple: true, .. } => vec![CmdOption::enter("Done")],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
!matches!(&self.step, Step::AutoConstrain { .. })
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let keyword = text
|
||||
.trim()
|
||||
.trim_start_matches('_')
|
||||
.to_ascii_uppercase();
|
||||
match (&mut self.step, keyword.as_str()) {
|
||||
(Step::First, "O" | "OBJECT") => {
|
||||
self.step = Step::FirstObject;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
(Step::First, "A" | "AUTOCONSTRAIN") => {
|
||||
self.step = Step::AutoConstrain { handles: Vec::new() };
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
(Step::PointsOnCurve { multiple, .. }, "M" | "MULTIPLE") => {
|
||||
*multiple = true;
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
(Step::PointsOnCurve { .. }, "P" | "POINT") => Some(CmdResult::NeedPoint),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
!matches!(&self.step, Step::AutoConstrain { .. })
|
||||
}
|
||||
|
||||
fn entity_pick_accepts_points(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn entity_pick_highlights_hover(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
self.picked_entity = Some(entity);
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, point: DVec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let is_point = self
|
||||
.picked_entity
|
||||
.as_ref()
|
||||
.is_some_and(|entity| Self::picked_constraint_point(entity, point));
|
||||
let is_curve = self
|
||||
.picked_entity
|
||||
.as_ref()
|
||||
.is_some_and(Self::valid_curve);
|
||||
self.picked_entity = None;
|
||||
|
||||
match self.step.clone() {
|
||||
Step::FirstObject if is_curve => {
|
||||
self.step = Step::PointsOnCurve {
|
||||
curve: Self::curve_pick(handle, point),
|
||||
multiple: false,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::First if is_point => {
|
||||
self.step = Step::Second(Self::point_pick(Some(handle), point));
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::First if is_curve => {
|
||||
self.step = Step::PointsOnCurve {
|
||||
curve: Self::curve_pick(handle, point),
|
||||
multiple: false,
|
||||
};
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Second(first) if is_point => CmdResult::AddCoincidentConstraint {
|
||||
first,
|
||||
second: Self::point_pick(Some(handle), point),
|
||||
multiple: false,
|
||||
label: "Coincident constraint",
|
||||
},
|
||||
Step::Second(first) if is_curve => CmdResult::AddCoincidentConstraint {
|
||||
first,
|
||||
second: Self::curve_pick(handle, point),
|
||||
multiple: false,
|
||||
label: "Coincident constraint",
|
||||
},
|
||||
Step::PointsOnCurve { curve, multiple } if is_point => {
|
||||
CmdResult::AddCoincidentConstraint {
|
||||
first: curve,
|
||||
second: Self::point_pick(Some(handle), point),
|
||||
multiple,
|
||||
label: "Coincident constraint",
|
||||
}
|
||||
}
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_selection_gathering(&self) -> bool {
|
||||
matches!(&self.step, Step::AutoConstrain { .. })
|
||||
}
|
||||
|
||||
fn selection_forces_add(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_selection_complete(&mut self, handles: Vec<Handle>) -> CmdResult {
|
||||
if let Step::AutoConstrain { handles: selected } = &mut self.step {
|
||||
*selected = handles;
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if matches!(&self.step, Step::First) {
|
||||
self.step = Step::FirstObject;
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if let Step::AutoConstrain { handles } = &self.step {
|
||||
if !handles.is_empty() {
|
||||
return CmdResult::AddAutoCoincidentConstraints {
|
||||
handles: handles.clone(),
|
||||
};
|
||||
}
|
||||
}
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
@@ -80,5 +297,5 @@ impl CadCommand for CoincidentConstraintCommand {
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["CCONSTRAINT"]
|
||||
names: &["CCONSTRAINT", "GCCOINCIDENT"]
|
||||
});
|
||||
@@ -0,0 +1,87 @@
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult};
|
||||
|
||||
/// Command-line front end for the standard geometric-constraint family.
|
||||
/// Each choice is handed to the existing focused command so ribbon and typed
|
||||
/// entry use one implementation.
|
||||
pub struct GeomConstraintCommand;
|
||||
|
||||
impl GeomConstraintCommand {
|
||||
pub fn new() -> Self {
|
||||
Self
|
||||
}
|
||||
|
||||
fn dispatch(keyword: &str) -> Option<&'static str> {
|
||||
Some(match keyword {
|
||||
"H" | "HORIZONTAL" => "HCONSTRAINT",
|
||||
"V" | "VERTICAL" => "VCONSTRAINT",
|
||||
"P" | "PERPENDICULAR" => "QCONSTRAINT",
|
||||
"PA" | "PARALLEL" => "PCONSTRAINT",
|
||||
"T" | "TANGENT" => "TCONSTRAINT",
|
||||
"SM" | "SMOOTH" => "SMOOTHCONSTRAINT",
|
||||
"C" | "COINCIDENT" => "GCCOINCIDENT",
|
||||
"CON" | "CONCENTRIC" => "NCONSTRAINT",
|
||||
"COL" | "COLLINEAR" | "COLINEAR" => "LCONSTRAINT",
|
||||
"SY" | "SYMMETRIC" => "SYCONSTRAINT",
|
||||
"E" | "EQUAL" => "ECONSTRAINT",
|
||||
"F" | "FIX" => "FXCONSTRAINT",
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for GeomConstraintCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"GEOMCONSTRAINT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
"GEOMCONSTRAINT Enter constraint type [Horizontal/Vertical/Perpendicular/Parallel/Tangent/Smooth/Coincident/Concentric/Collinear/Symmetric/Equal/Fix] <Coincident>:".to_string()
|
||||
}
|
||||
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
vec![
|
||||
CmdOption::new("Horizontal", "H"),
|
||||
CmdOption::new("Vertical", "V"),
|
||||
CmdOption::new("Perpendicular", "P"),
|
||||
CmdOption::new("Parallel", "PA"),
|
||||
CmdOption::new("Tangent", "T"),
|
||||
CmdOption::new("Smooth", "SM"),
|
||||
CmdOption::new("Coincident", "C"),
|
||||
CmdOption::new("Concentric", "CON"),
|
||||
CmdOption::new("Collinear", "COL"),
|
||||
CmdOption::new("Symmetric", "SY"),
|
||||
CmdOption::new("Equal", "E"),
|
||||
CmdOption::new("Fix", "F"),
|
||||
]
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let keyword = text
|
||||
.trim()
|
||||
.trim_start_matches('_')
|
||||
.to_ascii_uppercase();
|
||||
Self::dispatch(&keyword).map(|command| CmdResult::Dispatch(command.to_string()))
|
||||
}
|
||||
|
||||
fn on_point(&mut self, _point: DVec3) -> CmdResult {
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Dispatch("GCCOINCIDENT".to_string())
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
inventory::submit!(crate::command::CommandRegistration {
|
||||
names: &["GEOMCONSTRAINT"]
|
||||
});
|
||||
@@ -4,11 +4,13 @@
|
||||
|
||||
mod coincident;
|
||||
mod equal_distance;
|
||||
mod geom_constraint;
|
||||
mod point_on_entity;
|
||||
mod tools;
|
||||
mod value;
|
||||
pub use coincident::{coincident_tool, CoincidentConstraintCommand};
|
||||
pub use equal_distance::{equal_distance_tool, EqualDistanceConstraintCommand};
|
||||
pub use geom_constraint::GeomConstraintCommand;
|
||||
pub use point_on_entity::{
|
||||
center_point_tool, midpoint_tool, point_on_curve_tool, PointOnEntityConstraintCommand,
|
||||
};
|
||||
|
||||
Vendored
+1
-10
@@ -32,7 +32,7 @@ pub fn general_section(entity: &EntityType) -> PropSection {
|
||||
|
||||
let hyperlink = crate::scene::pe_url_of(entity).unwrap_or_default().to_owned();
|
||||
|
||||
let mut section = PropSection {
|
||||
let section = PropSection {
|
||||
title: t!("General").into_owned(),
|
||||
props: vec![
|
||||
Property {
|
||||
@@ -88,15 +88,6 @@ pub fn general_section(entity: &EntityType) -> PropSection {
|
||||
],
|
||||
};
|
||||
|
||||
// Thickness (DXF 39) is a General-group property, but only the entity
|
||||
// types that carry an extrusion thickness expose it (line, circle, arc,
|
||||
// polyline, text, 2D solid, …). Show it right after Hyperlink for those.
|
||||
if let Some(t) = crate::scene::view::dispatch::entity_thickness(entity) {
|
||||
section
|
||||
.props
|
||||
.push(crate::entities::common::edit_prop(t!("Thickness").as_ref(), "thickness", t));
|
||||
}
|
||||
|
||||
section
|
||||
}
|
||||
|
||||
|
||||
+9
-1
@@ -2836,6 +2836,14 @@ impl Scene {
|
||||
}
|
||||
|
||||
pub fn bump_entities(&mut self, changes: &[(Handle, ChangeKind)]) {
|
||||
self.bump_entities_with_parametric_driven(changes, &[]);
|
||||
}
|
||||
|
||||
pub fn bump_entities_with_parametric_driven(
|
||||
&mut self,
|
||||
changes: &[(Handle, ChangeKind)],
|
||||
driven_refs: &[parametric_constraints::ParametricRef],
|
||||
) {
|
||||
if changes.iter().any(|(handle, kind)| {
|
||||
matches!(kind, ChangeKind::Removed)
|
||||
|| self
|
||||
@@ -2869,7 +2877,7 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
if !self.parametric_constraints.is_empty() {
|
||||
for change in self.refresh_parametric_constraints(&changes) {
|
||||
for change in self.refresh_parametric_constraints_with_driven(&changes, driven_refs) {
|
||||
if !changes.iter().any(|(handle, _)| *handle == change.0) {
|
||||
changes.push(change);
|
||||
}
|
||||
|
||||
@@ -361,6 +361,7 @@ pub(crate) fn resolve_point(entity: &acadrust::EntityType, marker: i32) -> Optio
|
||||
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,
|
||||
};
|
||||
}
|
||||
@@ -392,6 +393,23 @@ pub(crate) fn resolve_point(entity: &acadrust::EntityType, marker: i32) -> Optio
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let bulge = match entity {
|
||||
acadrust::EntityType::LwPolyline(polyline) => {
|
||||
polyline.vertices.get(segment)?.bulge
|
||||
}
|
||||
acadrust::EntityType::Polyline2D(polyline) => {
|
||||
polyline.vertices.get(segment)?.bulge
|
||||
}
|
||||
_ => 0.0,
|
||||
};
|
||||
if let Some(arc) = cadkernel::geom2d::BulgeArc::from_bulge(
|
||||
[a.x, a.y],
|
||||
[b.x, b.y],
|
||||
bulge,
|
||||
) {
|
||||
let midpoint = arc.sample(0.5);
|
||||
return Some(Vector3::new(midpoint[0], midpoint[1], (a.z + b.z) * 0.5));
|
||||
}
|
||||
return Some((a + b) * 0.5);
|
||||
}
|
||||
if marker < 0 {
|
||||
@@ -406,6 +424,97 @@ pub(crate) fn resolve_point(entity: &acadrust::EntityType, marker: i32) -> Optio
|
||||
/// 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();
|
||||
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])));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let polyline_midpoints = |vertices: &[Vector3], bulges: &[f64], closed: bool| {
|
||||
let mut result = Vec::new();
|
||||
for (index, bulge) in bulges.iter().copied().enumerate() {
|
||||
let Some(a) = vertices.get(index).copied() else {
|
||||
continue;
|
||||
};
|
||||
let b = vertices
|
||||
.get(index + 1)
|
||||
.copied()
|
||||
.or_else(|| closed.then(|| vertices.first().copied()).flatten());
|
||||
if let Some(b) = b {
|
||||
let midpoint = cadkernel::geom2d::BulgeArc::from_bulge(
|
||||
[a.x, a.y],
|
||||
[b.x, b.y],
|
||||
bulge,
|
||||
)
|
||||
.map(|arc| {
|
||||
let point = arc.sample(0.5);
|
||||
Vector3::new(point[0], point[1], (a.z + b.z) * 0.5)
|
||||
})
|
||||
.unwrap_or((a + b) * 0.5);
|
||||
result.push((
|
||||
POLYLINE_SEGMENT_MIDPOINT_MARKER_BASE - index as i32,
|
||||
midpoint,
|
||||
));
|
||||
}
|
||||
}
|
||||
result
|
||||
};
|
||||
match entity {
|
||||
acadrust::EntityType::LwPolyline(polyline) => {
|
||||
let vertices = super::dimension_assoc::source_points(entity);
|
||||
let bulges: Vec<_> = polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| vertex.bulge)
|
||||
.collect();
|
||||
points.extend(polyline_midpoints(&vertices, &bulges, polyline.is_closed));
|
||||
}
|
||||
acadrust::EntityType::Polyline2D(polyline) => {
|
||||
let vertices = super::dimension_assoc::source_points(entity);
|
||||
let bulges: Vec<_> = polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| vertex.bulge)
|
||||
.collect();
|
||||
points.extend(polyline_midpoints(&vertices, &bulges, polyline.is_closed()));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
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.
|
||||
@@ -431,21 +540,96 @@ pub(crate) fn nearest_parametric_point(
|
||||
if common.owner_handle != owner || Some(common.handle) == exclude {
|
||||
continue;
|
||||
}
|
||||
for (marker, point) in super::dimension_assoc::source_points(candidate)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
{
|
||||
consider(common.handle, marker as i32, point);
|
||||
}
|
||||
match candidate {
|
||||
acadrust::EntityType::Circle(c) => consider(common.handle, -3, c.center_wcs()),
|
||||
acadrust::EntityType::Arc(a) => consider(common.handle, -3, a.center_wcs()),
|
||||
_ => {}
|
||||
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)
|
||||
}
|
||||
|
||||
fn point_segment_distance_squared(point: Vector3, start: Vector3, end: Vector3) -> f64 {
|
||||
let edge = end - start;
|
||||
let denominator = edge.length_squared();
|
||||
if denominator <= f64::EPSILON {
|
||||
return (point - start).length_squared();
|
||||
}
|
||||
let unit = ((point - start).dot(&edge) / denominator).clamp(0.0, 1.0);
|
||||
(point - (start + edge * unit)).length_squared()
|
||||
}
|
||||
|
||||
/// Resolve the whole curve or the picked straight 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(polyline) => {
|
||||
let vertices = super::dimension_assoc::source_points(entity);
|
||||
(0..polyline.vertices.len())
|
||||
.filter_map(|index| {
|
||||
let start = vertices.get(index).copied()?;
|
||||
let end = vertices
|
||||
.get(index + 1)
|
||||
.copied()
|
||||
.or_else(|| polyline.is_closed.then(|| vertices.first().copied()).flatten())?;
|
||||
Some((point_segment_distance_squared(world_point, start, end), index))
|
||||
})
|
||||
.min_by(|(a, _), (b, _)| a.total_cmp(b))
|
||||
.map(|(_, index)| ParametricRef::segment(handle, index))
|
||||
}
|
||||
acadrust::EntityType::Polyline2D(polyline) => {
|
||||
let vertices = super::dimension_assoc::source_points(entity);
|
||||
(0..polyline.vertices.len())
|
||||
.filter_map(|index| {
|
||||
let start = vertices.get(index).copied()?;
|
||||
let end = vertices
|
||||
.get(index + 1)
|
||||
.copied()
|
||||
.or_else(|| polyline.is_closed().then(|| vertices.first().copied()).flatten())?;
|
||||
Some((point_segment_distance_squared(world_point, start, end), index))
|
||||
})
|
||||
.min_by(|(a, _), (b, _)| a.total_cmp(b))
|
||||
.map(|(_, index)| ParametricRef::segment(handle, index))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
impl ConstraintKind {
|
||||
/// The short symbol a constraint glyph shows — matches the existing
|
||||
/// ribbon icons (`crate::modules::parametric::{tools,value}`) for
|
||||
@@ -568,6 +752,49 @@ pub(crate) fn glyph_placement(
|
||||
}
|
||||
|
||||
impl super::Scene {
|
||||
/// Expand a set of entities to the full enabled constraint component in
|
||||
/// one scope. Whole-object translations use this to keep every connected
|
||||
/// entity's own shape while moving the assembly.
|
||||
pub(crate) fn parametric_connected_handles(
|
||||
&self,
|
||||
scope: ParametricScope,
|
||||
seeds: &[Handle],
|
||||
) -> 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
|
||||
&& 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,
|
||||
@@ -724,6 +951,68 @@ impl super::Scene {
|
||||
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
|
||||
}
|
||||
|
||||
pub fn smooth_constraint_refs(&self, handles: &[Handle]) -> Option<Vec<ParametricRef>> {
|
||||
let [first, second] = handles else {
|
||||
return None;
|
||||
|
||||
+277
-43
@@ -45,6 +45,7 @@ enum EntityGeom {
|
||||
Polyline {
|
||||
points: Rc<Vec<GPoint>>,
|
||||
straight: Rc<Vec<bool>>,
|
||||
arcs: Rc<Vec<Option<PolylineArc>>>,
|
||||
closed: bool,
|
||||
},
|
||||
Circle(GCircle),
|
||||
@@ -61,6 +62,12 @@ enum EntityGeom {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct PolylineArc {
|
||||
arc: GArc,
|
||||
sweep: f64,
|
||||
}
|
||||
|
||||
impl EntityGeom {
|
||||
/// The `cadkernel_constraints::geo::Point` for a marker on this entity — `None` for
|
||||
/// a marker this entity type/value doesn't support (see
|
||||
@@ -91,6 +98,7 @@ impl EntityGeom {
|
||||
points,
|
||||
straight,
|
||||
closed,
|
||||
..
|
||||
} = self
|
||||
else {
|
||||
return None;
|
||||
@@ -108,6 +116,13 @@ impl EntityGeom {
|
||||
};
|
||||
Some(GLine { p1, p2 })
|
||||
}
|
||||
|
||||
fn arc_segment(&self, index: usize) -> Option<PolylineArc> {
|
||||
let EntityGeom::Polyline { arcs, .. } = self else {
|
||||
return None;
|
||||
};
|
||||
arcs.get(index).copied().flatten()
|
||||
}
|
||||
}
|
||||
|
||||
/// `Tangent`/`Normal` only ever care about "a whole circle-shaped curve" or
|
||||
@@ -131,8 +146,14 @@ fn as_circle_or_line(g: EntityGeom, reference: ParametricRef) -> CircleOrLine {
|
||||
EntityGeom::Spline { .. } => CircleOrLine::Other,
|
||||
EntityGeom::Polyline { .. } => reference
|
||||
.segment_index()
|
||||
.and_then(|index| g.line_segment(index))
|
||||
.map(CircleOrLine::Line)
|
||||
.and_then(|index| {
|
||||
g.line_segment(index)
|
||||
.map(CircleOrLine::Line)
|
||||
.or_else(|| {
|
||||
g.arc_segment(index)
|
||||
.map(|segment| CircleOrLine::Circle(segment.arc.circle))
|
||||
})
|
||||
})
|
||||
.unwrap_or(CircleOrLine::Other),
|
||||
}
|
||||
}
|
||||
@@ -182,42 +203,114 @@ fn register_entity(
|
||||
sys.add_param(table.insertion_point.x, false),
|
||||
sys.add_param(table.insertion_point.y, false),
|
||||
))),
|
||||
EntityType::LwPolyline(polyline) => Some(EntityGeom::Polyline {
|
||||
points: Rc::new(
|
||||
super::dimension_assoc::source_points(entity)
|
||||
.into_iter()
|
||||
.map(|point| {
|
||||
GPoint::new(sys.add_param(point.x, false), sys.add_param(point.y, false))
|
||||
EntityType::LwPolyline(polyline) => {
|
||||
let source = super::dimension_assoc::source_points(entity);
|
||||
let points: Vec<_> = source
|
||||
.iter()
|
||||
.map(|point| {
|
||||
GPoint::new(sys.add_param(point.x, false), sys.add_param(point.y, false))
|
||||
})
|
||||
.collect();
|
||||
let arcs = (0..polyline.vertices.len())
|
||||
.map(|index| {
|
||||
let next = if index + 1 < source.len() {
|
||||
index + 1
|
||||
} else if polyline.is_closed {
|
||||
0
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let bulge = polyline.vertices[index].bulge;
|
||||
let shape = cadkernel::geom2d::BulgeArc::from_bulge(
|
||||
[source[index].x, source[index].y],
|
||||
[source[next].x, source[next].y],
|
||||
bulge,
|
||||
)?;
|
||||
Some(PolylineArc {
|
||||
arc: GArc {
|
||||
circle: GCircle {
|
||||
center: GPoint::new(
|
||||
sys.add_param(shape.center[0], false),
|
||||
sys.add_param(shape.center[1], false),
|
||||
),
|
||||
rad: sys.add_param(shape.radius, false),
|
||||
},
|
||||
start: points[index],
|
||||
end: points[next],
|
||||
start_angle: sys.add_param(shape.start_angle, false),
|
||||
end_angle: sys.add_param(shape.start_angle + shape.sweep, false),
|
||||
},
|
||||
sweep: shape.sweep,
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
straight: Rc::new(
|
||||
polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| vertex.bulge.abs() <= 1e-9)
|
||||
.collect(),
|
||||
),
|
||||
closed: polyline.is_closed,
|
||||
}),
|
||||
EntityType::Polyline2D(polyline) => Some(EntityGeom::Polyline {
|
||||
points: Rc::new(
|
||||
super::dimension_assoc::source_points(entity)
|
||||
.into_iter()
|
||||
.map(|point| {
|
||||
GPoint::new(sys.add_param(point.x, false), sys.add_param(point.y, false))
|
||||
})
|
||||
.collect();
|
||||
Some(EntityGeom::Polyline {
|
||||
points: Rc::new(points),
|
||||
straight: Rc::new(
|
||||
polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| vertex.bulge.abs() <= 1e-9)
|
||||
.collect(),
|
||||
),
|
||||
arcs: Rc::new(arcs),
|
||||
closed: polyline.is_closed,
|
||||
})
|
||||
}
|
||||
EntityType::Polyline2D(polyline) => {
|
||||
let source = super::dimension_assoc::source_points(entity);
|
||||
let points: Vec<_> = source
|
||||
.iter()
|
||||
.map(|point| {
|
||||
GPoint::new(sys.add_param(point.x, false), sys.add_param(point.y, false))
|
||||
})
|
||||
.collect();
|
||||
let arcs = (0..polyline.vertices.len())
|
||||
.map(|index| {
|
||||
let next = if index + 1 < source.len() {
|
||||
index + 1
|
||||
} else if polyline.is_closed() {
|
||||
0
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
let bulge = polyline.vertices[index].bulge;
|
||||
let shape = cadkernel::geom2d::BulgeArc::from_bulge(
|
||||
[source[index].x, source[index].y],
|
||||
[source[next].x, source[next].y],
|
||||
bulge,
|
||||
)?;
|
||||
Some(PolylineArc {
|
||||
arc: GArc {
|
||||
circle: GCircle {
|
||||
center: GPoint::new(
|
||||
sys.add_param(shape.center[0], false),
|
||||
sys.add_param(shape.center[1], false),
|
||||
),
|
||||
rad: sys.add_param(shape.radius, false),
|
||||
},
|
||||
start: points[index],
|
||||
end: points[next],
|
||||
start_angle: sys.add_param(shape.start_angle, false),
|
||||
end_angle: sys.add_param(shape.start_angle + shape.sweep, false),
|
||||
},
|
||||
sweep: shape.sweep,
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
straight: Rc::new(
|
||||
polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| vertex.bulge.abs() <= 1e-9)
|
||||
.collect(),
|
||||
),
|
||||
closed: polyline.is_closed(),
|
||||
}),
|
||||
})
|
||||
.collect();
|
||||
Some(EntityGeom::Polyline {
|
||||
points: Rc::new(points),
|
||||
straight: Rc::new(
|
||||
polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| vertex.bulge.abs() <= 1e-9)
|
||||
.collect(),
|
||||
),
|
||||
arcs: Rc::new(arcs),
|
||||
closed: polyline.is_closed(),
|
||||
})
|
||||
}
|
||||
EntityType::Line(l) => {
|
||||
let p1 = GPoint::new(
|
||||
sys.add_param(l.start.x, false),
|
||||
@@ -439,6 +532,42 @@ fn resolve_constraint_point(
|
||||
)));
|
||||
return Some(point);
|
||||
}
|
||||
if let Some(segment) = reference
|
||||
.segment_midpoint_index()
|
||||
.and_then(|index| geometry.arc_segment(index))
|
||||
{
|
||||
let (start, end) = {
|
||||
let store = sys.store();
|
||||
(
|
||||
store.get(segment.arc.start_angle),
|
||||
store.get(segment.arc.end_angle),
|
||||
)
|
||||
};
|
||||
let parameter_value = (start + end) * 0.5;
|
||||
let parameter = sys.add_param(parameter_value, false);
|
||||
sys.add_constraint(Rc::new(CenterOfGravity::new(
|
||||
parameter,
|
||||
vec![segment.arc.start_angle, segment.arc.end_angle],
|
||||
vec![0.5, 0.5],
|
||||
)));
|
||||
let value = cadkernel_constraints::geo::Curve::value(
|
||||
&segment.arc,
|
||||
sys.store(),
|
||||
parameter_value,
|
||||
0.0,
|
||||
None,
|
||||
);
|
||||
let point = GPoint::new(sys.add_param(value.x, false), sys.add_param(value.y, false));
|
||||
let curve = Rc::new(segment.arc);
|
||||
sys.add_constraint(Rc::new(CurveValue::new(
|
||||
point,
|
||||
point.x,
|
||||
curve.clone(),
|
||||
parameter,
|
||||
)));
|
||||
sys.add_constraint(Rc::new(CurveValue::new(point, point.y, curve, parameter)));
|
||||
return Some(point);
|
||||
}
|
||||
if marker != -2 {
|
||||
return None;
|
||||
}
|
||||
@@ -935,8 +1064,22 @@ fn build_constraint(
|
||||
}
|
||||
EntityGeom::Polyline { .. } => b
|
||||
.segment_index()
|
||||
.and_then(|index| geom.line_segment(index))
|
||||
.map(|line| vec![Rc::new(PointOnLine::new(p, line)) as Rc<dyn Constraint>])
|
||||
.and_then(|index| {
|
||||
geom.line_segment(index)
|
||||
.map(|line| {
|
||||
vec![Rc::new(PointOnLine::new(p, line)) as Rc<dyn Constraint>]
|
||||
})
|
||||
.or_else(|| {
|
||||
geom.arc_segment(index).map(|segment| {
|
||||
let zero = sys.add_param(0.0, true);
|
||||
vec![Rc::new(P2CDistance::new(
|
||||
segment.arc.circle,
|
||||
p,
|
||||
zero,
|
||||
)) as Rc<dyn Constraint>]
|
||||
})
|
||||
})
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
EntityGeom::Point(_) => Vec::new(),
|
||||
}
|
||||
@@ -1564,6 +1707,7 @@ fn solve_scope(
|
||||
document: &acadrust::CadDocument,
|
||||
drawing_params: &ParameterTable,
|
||||
set: &ParametricConstraintSet,
|
||||
driven_refs: &[ParametricRef],
|
||||
) -> Option<SolveResult> {
|
||||
let params = if set.local_parameters.is_empty() {
|
||||
drawing_params
|
||||
@@ -1604,9 +1748,8 @@ fn solve_scope(
|
||||
// its endpoint. Not tracked in `owner`: these are solver-internal
|
||||
// bookkeeping, never redundant with anything a user-facing constraint
|
||||
// could name, so there's no `ConstraintId` for them to report against.
|
||||
for geom in cache.values() {
|
||||
let EntityGeom::Arc(arc) = geom else { continue };
|
||||
let curve = Rc::new(*arc);
|
||||
let add_arc_rules = |sys: &mut System, arc: GArc, sweep: Option<f64>| {
|
||||
let curve = Rc::new(arc);
|
||||
sys.add_constraint(Rc::new(CurveValue::new(
|
||||
arc.start,
|
||||
arc.start.x,
|
||||
@@ -1631,6 +1774,25 @@ fn solve_scope(
|
||||
curve,
|
||||
arc.end_angle,
|
||||
)));
|
||||
if let Some(sweep) = sweep {
|
||||
let target = sys.add_param(sweep, true);
|
||||
sys.add_constraint(Rc::new(Difference::new(
|
||||
arc.start_angle,
|
||||
arc.end_angle,
|
||||
target,
|
||||
)));
|
||||
}
|
||||
};
|
||||
for geom in cache.values() {
|
||||
match geom {
|
||||
EntityGeom::Arc(arc) => add_arc_rules(&mut sys, *arc, None),
|
||||
EntityGeom::Polyline { arcs, .. } => {
|
||||
for segment in arcs.iter().flatten() {
|
||||
add_arc_rules(&mut sys, segment.arc, Some(segment.sweep));
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// Ellipse rules (same motivation as arc rules, different mechanism):
|
||||
@@ -1667,6 +1829,66 @@ fn solve_scope(
|
||||
sys.add_constraint(Rc::new(Difference::new(el.center.y, el.focus1.y, dy)));
|
||||
}
|
||||
|
||||
// The first ordered Coincident selection is the anchor for this solve.
|
||||
// Pin either its addressed point or every defining parameter of its whole
|
||||
// curve temporarily; the relation remains persistent, while these pins do
|
||||
// not enter the drawing's constraint graph.
|
||||
for reference in driven_refs {
|
||||
if !set
|
||||
.constraints
|
||||
.iter()
|
||||
.any(|constraint| constraint.refs.iter().any(|item| item.entity == reference.entity))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let params_to_pin = if reference.marker.is_some() && reference.segment_index().is_none() {
|
||||
resolve_constraint_point(document, &mut sys, &mut cache, *reference)
|
||||
.map(|point| vec![point.x, point.y])
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
match resolve_ref(document, &mut sys, &mut cache, *reference) {
|
||||
Some(EntityGeom::Point(point)) => vec![point.x, point.y],
|
||||
Some(EntityGeom::Line(line))
|
||||
| Some(EntityGeom::Ray(line))
|
||||
| Some(EntityGeom::XLine(line)) => {
|
||||
vec![line.p1.x, line.p1.y, line.p2.x, line.p2.y]
|
||||
}
|
||||
Some(EntityGeom::Polyline { points, .. }) => points
|
||||
.iter()
|
||||
.flat_map(|point| [point.x, point.y])
|
||||
.collect(),
|
||||
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.focus1.x,
|
||||
ellipse.focus1.y,
|
||||
ellipse.radmin,
|
||||
],
|
||||
Some(EntityGeom::Spline { curve, .. }) => curve
|
||||
.poles
|
||||
.iter()
|
||||
.flat_map(|point| [point.x, point.y])
|
||||
.collect(),
|
||||
None => Vec::new(),
|
||||
}
|
||||
};
|
||||
for parameter in params_to_pin {
|
||||
let value = sys.store().get(parameter);
|
||||
let target = sys.add_param(value, true);
|
||||
sys.add_constraint(Rc::new(Equal::new(parameter, target, 1.0)));
|
||||
}
|
||||
}
|
||||
|
||||
let partitions = sys.partition();
|
||||
for sub in &partitions {
|
||||
solve_dl(sub, sys.store_mut());
|
||||
@@ -1683,7 +1905,9 @@ fn solve_scope(
|
||||
.map(|g| match g {
|
||||
EntityGeom::Point(_) => 2,
|
||||
EntityGeom::Line(_) | EntityGeom::Ray(_) | EntityGeom::XLine(_) => 4,
|
||||
EntityGeom::Polyline { points, .. } => points.len() * 2,
|
||||
EntityGeom::Polyline { points, arcs, .. } => {
|
||||
points.len() * 2 + arcs.iter().flatten().count() * 5
|
||||
}
|
||||
EntityGeom::Circle(_) => 3,
|
||||
// Raw param count (center×2, rad, start×2, end×2, both
|
||||
// angles) — same "raw, not netted against its own
|
||||
@@ -2151,9 +2375,18 @@ impl Scene {
|
||||
/// the resulting `(Handle, ChangeKind::Modified)` entries the same way
|
||||
/// `refresh_associative_dimensions`/`_hatches` do, for `bump_entities`
|
||||
/// to fold into its own `changes` vec.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn refresh_parametric_constraints(
|
||||
&mut self,
|
||||
changes: &[(Handle, ChangeKind)],
|
||||
) -> Vec<(Handle, ChangeKind)> {
|
||||
self.refresh_parametric_constraints_with_driven(changes, &[])
|
||||
}
|
||||
|
||||
pub(crate) fn refresh_parametric_constraints_with_driven(
|
||||
&mut self,
|
||||
changes: &[(Handle, ChangeKind)],
|
||||
driven_refs: &[ParametricRef],
|
||||
) -> Vec<(Handle, ChangeKind)> {
|
||||
// An erased entity takes its constraints with it instead of leaving
|
||||
// dangling references. Done first,
|
||||
@@ -2200,6 +2433,7 @@ impl Scene {
|
||||
&self.document,
|
||||
&self.named_parameters,
|
||||
&self.parametric_constraints[i],
|
||||
driven_refs,
|
||||
) else {
|
||||
continue;
|
||||
};
|
||||
@@ -2237,7 +2471,7 @@ impl Scene {
|
||||
continue;
|
||||
}
|
||||
let Some((solved, _dof, _conflicts)) =
|
||||
solve_scope(&self.document, &self.named_parameters, set)
|
||||
solve_scope(&self.document, &self.named_parameters, set, &[])
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
@@ -80,6 +80,25 @@ pub fn properties_sectioned(
|
||||
} else {
|
||||
sections.extend(groups);
|
||||
}
|
||||
if let Some(thickness) = entity_thickness(entity) {
|
||||
let already_present = sections.iter().flat_map(|section| §ion.props).any(|property| {
|
||||
property.field == "thickness"
|
||||
|| property.field == "sl_thickness"
|
||||
|| property.label == crate::t!("Thickness").as_ref()
|
||||
});
|
||||
if !already_present {
|
||||
if let Some(geometry) = sections
|
||||
.iter_mut()
|
||||
.find(|section| section.title == crate::t!("Geometry").as_ref())
|
||||
{
|
||||
geometry.props.push(crate::entities::common::edit_prop(
|
||||
crate::t!("Thickness").as_ref(),
|
||||
"thickness",
|
||||
thickness,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
sections
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user