forked from 1b-agent/opencadstudio-upstream
Add 8 geometric constraint kinds and an opt-in DWG-native persistence toggle
New ConstraintKind variants — Concentric, CenterPoint, Colinear, Midpoint, Fixed, PointOnCurve, EqualDistance, Symmetric — each built entirely from existing ocs_gcs (FreeCAD planegcs) primitives, no new solver math needed. build_constraint now returns Vec<Rc<dyn Constraint>> instead of Option<Rc<...>>, generalizing the old Coincident-only "needs a second system-level constraint" special case to every kind that needs one. Wired into the ribbon (7 new buttons), a new point+pre-selected-entity command for CenterPoint/Midpoint/PointOnCurve, a new 4-point command for EqualDistance, and the existing DWG-native class-name mapping (all 8 reuse the same generic "plain Geometrical node" path already built for Coincident/Horizontal/etc.). Deliberately not modeled: Normal and EqualCurvature, which for this app's Line/Circle-only entity model would just be Perpendicular and Equal wearing different DWG class names. Also makes DWG-native constraint persistence opt-in (Options > General, off by default) rather than unconditional: a scope that doesn't yet have the native graph is only given one when the setting is on, but a scope that already has one — from an earlier save, or a file that came from real AutoCAD — keeps getting it resynced regardless of the setting, so toggling it off never freezes a stale graph in place. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
cf33a1d019
commit
31b5ef4ec3
18 files changed
+1076
-106
No files matched your search
@@ -2375,6 +2375,62 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
}
|
||||
CmdResult::AddPointOnEntityConstraint { point, target, kind, label } => {
|
||||
let scope = self.tabs[i].current_sketch_scope();
|
||||
let to_world = |p: glam::DVec3| acadrust::types::Vector3::new(p.x, p.y, p.z);
|
||||
let resolved = crate::scene::sketch_constraints::nearest_sketch_point(&self.tabs[i].scene.document, scope, to_world(point), Some(target));
|
||||
match resolved {
|
||||
Some(point_ref) => {
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
|
||||
// `CenterPoint` addresses its whole-entity side via
|
||||
// the center marker (same solve as Coincident /
|
||||
// Concentric — `ConstraintKind::CenterPoint`'s own
|
||||
// doc comment); `Midpoint`/`PointOnCurve` address it
|
||||
// as a whole entity.
|
||||
let target_ref =
|
||||
if kind == ConstraintKind::CenterPoint { SketchRef::center(target) } else { SketchRef::whole(target) };
|
||||
return self.apply_cmd_result(CmdResult::AddSketchConstraint {
|
||||
kind,
|
||||
refs: vec![point_ref, target_ref],
|
||||
driving_param: None,
|
||||
label,
|
||||
});
|
||||
}
|
||||
None => {
|
||||
self.command_line.push_error(
|
||||
"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;
|
||||
}
|
||||
}
|
||||
}
|
||||
CmdResult::AddEqualDistanceConstraint { points, label } => {
|
||||
let scope = self.tabs[i].current_sketch_scope();
|
||||
let to_world = |p: glam::DVec3| acadrust::types::Vector3::new(p.x, p.y, p.z);
|
||||
let mut refs = Vec::with_capacity(4);
|
||||
for p in points {
|
||||
let Some(r) = crate::scene::sketch_constraints::nearest_sketch_point(&self.tabs[i].scene.document, scope, to_world(p), None)
|
||||
else {
|
||||
refs.clear();
|
||||
break;
|
||||
};
|
||||
refs.push(r);
|
||||
}
|
||||
if refs.len() == 4 {
|
||||
return self.apply_cmd_result(CmdResult::AddSketchConstraint {
|
||||
kind: crate::scene::sketch_constraints::ConstraintKind::EqualDistance,
|
||||
refs,
|
||||
driving_param: None,
|
||||
label,
|
||||
});
|
||||
}
|
||||
self.command_line.push_error(
|
||||
"Equal Distance: a 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;
|
||||
}
|
||||
CmdResult::ReassociateCenterMark { target, source, point } => {
|
||||
if self.reject_locked_edit(i, target) {
|
||||
return Task::none();
|
||||
|
||||
@@ -959,7 +959,7 @@ impl OpenCADStudio {
|
||||
// ── Constraints (persistent: added to the scope's SketchConstraintSet,
|
||||
// solved via Scene::bump_entities, re-solved on every later edit —
|
||||
// see docs/parametric_system_design.md §6.1) ──
|
||||
"HCONSTRAINT" | "VCONSTRAINT" => {
|
||||
"HCONSTRAINT" | "VCONSTRAINT" | "FXCONSTRAINT" => {
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
@@ -968,14 +968,14 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else if handles.len() != 1 {
|
||||
self.command_line
|
||||
.push_output("Select exactly one line, then run this constraint again.");
|
||||
.push_output("Select exactly one entity, then run this constraint again.");
|
||||
} else {
|
||||
use crate::command::CmdResult;
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
|
||||
let (kind, label) = if cmd == "HCONSTRAINT" {
|
||||
(ConstraintKind::Horizontal, "Horizontal constraint")
|
||||
} else {
|
||||
(ConstraintKind::Vertical, "Vertical constraint")
|
||||
let (kind, label) = match cmd {
|
||||
"HCONSTRAINT" => (ConstraintKind::Horizontal, "Horizontal constraint"),
|
||||
"VCONSTRAINT" => (ConstraintKind::Vertical, "Vertical constraint"),
|
||||
_ => (ConstraintKind::Fixed, "Fixed constraint"),
|
||||
};
|
||||
return Some(self.apply_cmd_result(CmdResult::AddSketchConstraint {
|
||||
kind,
|
||||
@@ -993,7 +993,14 @@ impl OpenCADStudio {
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
"PCONSTRAINT" | "QCONSTRAINT" | "ECONSTRAINT" | "TCONSTRAINT" => {
|
||||
"EDCONSTRAINT" => {
|
||||
use crate::modules::draw::constrain::EqualDistanceConstraintCommand;
|
||||
let new_cmd = EqualDistanceConstraintCommand::new();
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
||||
"PCONSTRAINT" | "QCONSTRAINT" | "ECONSTRAINT" | "TCONSTRAINT" | "LCONSTRAINT" => {
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
@@ -1011,6 +1018,7 @@ impl OpenCADStudio {
|
||||
"PCONSTRAINT" => (ConstraintKind::Parallel, "Parallel constraint"),
|
||||
"QCONSTRAINT" => (ConstraintKind::Perpendicular, "Perpendicular constraint"),
|
||||
"TCONSTRAINT" => (ConstraintKind::Tangent, "Tangent constraint"),
|
||||
"LCONSTRAINT" => (ConstraintKind::Colinear, "Colinear constraint"),
|
||||
_ => (ConstraintKind::Equal, "Equal constraint"),
|
||||
};
|
||||
return Some(self.apply_cmd_result(CmdResult::AddSketchConstraint {
|
||||
@@ -1022,6 +1030,75 @@ impl OpenCADStudio {
|
||||
}
|
||||
}
|
||||
|
||||
"NCONSTRAINT" => {
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let sel = SelectObjectsCommand::new(cmd);
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else if handles.len() != 2 {
|
||||
self.command_line.push_output("Select exactly two circles/arcs, then run this constraint again.");
|
||||
} else {
|
||||
use crate::command::CmdResult;
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
|
||||
return Some(self.apply_cmd_result(CmdResult::AddSketchConstraint {
|
||||
kind: ConstraintKind::Concentric,
|
||||
refs: vec![SketchRef::center(handles[0]), SketchRef::center(handles[1])],
|
||||
driving_param: None,
|
||||
label: "Concentric constraint",
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
"SYCONSTRAINT" => {
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let sel = SelectObjectsCommand::new(cmd);
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else if handles.len() != 3 {
|
||||
self.command_line.push_output(
|
||||
"Select exactly three entities (two circles/arcs, then the mirror line), then run this constraint again.",
|
||||
);
|
||||
} else {
|
||||
use crate::command::CmdResult;
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
|
||||
return Some(self.apply_cmd_result(CmdResult::AddSketchConstraint {
|
||||
kind: ConstraintKind::Symmetric,
|
||||
refs: vec![SketchRef::center(handles[0]), SketchRef::center(handles[1]), SketchRef::whole(handles[2])],
|
||||
driving_param: None,
|
||||
label: "Symmetric constraint",
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
"CPCONSTRAINT" | "MPCONSTRAINT" | "OCCONSTRAINT" => {
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
use crate::modules::draw::select::SelectObjectsCommand;
|
||||
let sel = SelectObjectsCommand::new(cmd);
|
||||
self.command_line.push_info(&sel.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(sel));
|
||||
} else if handles.len() != 1 {
|
||||
self.command_line.push_output(
|
||||
"Select exactly one entity (the circle or line the point should attach to), then run this constraint again.",
|
||||
);
|
||||
} else {
|
||||
use crate::modules::draw::constrain::PointOnEntityConstraintCommand;
|
||||
use crate::scene::sketch_constraints::ConstraintKind;
|
||||
let (name, kind, label) = match cmd {
|
||||
"CPCONSTRAINT" => ("CPCONSTRAINT", ConstraintKind::CenterPoint, "Center point constraint"),
|
||||
"MPCONSTRAINT" => ("MPCONSTRAINT", ConstraintKind::Midpoint, "Midpoint constraint"),
|
||||
_ => ("OCCONSTRAINT", ConstraintKind::PointOnCurve, "Point on curve constraint"),
|
||||
};
|
||||
let new_cmd = PointOnEntityConstraintCommand::new(name, kind, handles[0], label);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
}
|
||||
|
||||
"DCONSTRAINT" => {
|
||||
let handles = self.tabs[i].scene.selected_handles_in_order();
|
||||
if handles.is_empty() {
|
||||
|
||||
@@ -533,6 +533,19 @@ pub(super) struct OpenCADStudio {
|
||||
/// When true (default), the app registers itself as a .dwg/.dxf/.bak file
|
||||
/// handler on each launch. Toggle with the FILEASSOC command.
|
||||
pub file_assoc_enabled: bool,
|
||||
/// When true, saving a drawing with sketch constraints also materializes
|
||||
/// them as AutoCAD's own native `AssocNetwork`/`Assoc2dConstraintGroup`
|
||||
/// object graph (`scene::dwg_native_constraints`), so AutoCAD/BricsCAD
|
||||
/// recognize them as real constraints rather than an opaque OCS-only
|
||||
/// XRecord. Off by default (#opt-in — adds objects/file weight every
|
||||
/// user pays for save-after-save, whether or not they need AutoCAD
|
||||
/// interop). A drawing that already carries this native graph (from a
|
||||
/// prior save with this on, or from real AutoCAD) keeps it synced on
|
||||
/// every save regardless of this setting — see
|
||||
/// `Scene::materialize_dwg_native_constraints_for_save`'s doc comment;
|
||||
/// this only governs whether the graph is *created* in a scope that
|
||||
/// doesn't have one yet.
|
||||
pub write_dwg_native_constraints: bool,
|
||||
/// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command);
|
||||
/// 0 disables autosave.
|
||||
pub savetime_min: i32,
|
||||
@@ -2002,6 +2015,9 @@ pub enum Message {
|
||||
/// Register or unregister as the .dwg/.dxf handler, from Options. Same
|
||||
/// setting the FILEASSOC command carries.
|
||||
FileAssocChanged(bool),
|
||||
/// Toggle writing AutoCAD-native constraint objects on save, from
|
||||
/// Options. See `write_dwg_native_constraints`'s doc comment.
|
||||
WriteDwgNativeConstraintsChanged(bool),
|
||||
/// Switch the interface language and redraw localized views.
|
||||
LanguageChanged(crate::i18n::Language),
|
||||
/// Drop every entity from the active drawing.
|
||||
@@ -3426,6 +3442,7 @@ impl OpenCADStudio {
|
||||
dimension_continue_mode: 1,
|
||||
backup_on_save: true,
|
||||
file_assoc_enabled: true,
|
||||
write_dwg_native_constraints: false,
|
||||
savetime_min: 10,
|
||||
default_bg_color: None,
|
||||
default_paper_bg_color: None,
|
||||
|
||||
@@ -225,6 +225,15 @@ pub struct UserSettings {
|
||||
/// When true (default), the app (re)registers itself as a .dwg/.dxf/.bak
|
||||
/// handler on every launch. Toggle with the FILEASSOC command.
|
||||
pub file_assoc_enabled: bool,
|
||||
/// When true, saving a drawing also writes its sketch constraints as
|
||||
/// AutoCAD's own native object graph (`scene::dwg_native_constraints`),
|
||||
/// not just the app's own XRecord format. Off by default — opt-in, since
|
||||
/// it adds file weight every save whether or not AutoCAD/BricsCAD
|
||||
/// interop is needed. See `write_dwg_native_constraints` on the app
|
||||
/// struct for the full contract (a drawing that already has this native
|
||||
/// graph keeps it synced regardless of this setting).
|
||||
#[serde(default)]
|
||||
pub write_dwg_native_constraints: bool,
|
||||
/// Minutes between autosaves to a `.sv$` recovery file (SAVETIME command).
|
||||
/// 0 disables autosave.
|
||||
pub savetime_min: i32,
|
||||
@@ -338,6 +347,7 @@ impl Default for UserSettings {
|
||||
textfill: true,
|
||||
backup_on_save: true,
|
||||
file_assoc_enabled: true,
|
||||
write_dwg_native_constraints: false,
|
||||
savetime_min: 10,
|
||||
default_save_format: crate::io::DEFAULT_SAVE_FORMAT.to_string(),
|
||||
pick_add: true,
|
||||
|
||||
@@ -393,6 +393,7 @@ impl OpenCADStudio {
|
||||
textfill: crate::scene::text::sdf_atlas::textfill(),
|
||||
backup_on_save: self.backup_on_save,
|
||||
file_assoc_enabled: self.file_assoc_enabled,
|
||||
write_dwg_native_constraints: self.write_dwg_native_constraints,
|
||||
savetime_min: self.savetime_min,
|
||||
default_save_format: self.default_save_format.clone(),
|
||||
pick_add: self.pick_add,
|
||||
@@ -459,6 +460,7 @@ impl OpenCADStudio {
|
||||
crate::scene::text::sdf_atlas::set_textfill(s.textfill);
|
||||
self.backup_on_save = s.backup_on_save;
|
||||
self.file_assoc_enabled = s.file_assoc_enabled;
|
||||
self.write_dwg_native_constraints = s.write_dwg_native_constraints;
|
||||
self.savetime_min = s.savetime_min;
|
||||
self.default_save_format =
|
||||
crate::io::canonical_save_format(&s.default_save_format).to_string();
|
||||
@@ -1671,7 +1673,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
// docs/dwg_constraint_compatibility_design.md: additive DWG/DXF-
|
||||
// native constraint graph, alongside (not instead of) the XRecord
|
||||
// above — same save-time hook.
|
||||
self.tabs[i].scene.materialize_dwg_native_constraints_for_save();
|
||||
self.tabs[i].scene.materialize_dwg_native_constraints_for_save(self.write_dwg_native_constraints);
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
@@ -2624,7 +2626,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
self.sync_solid_models_for_save(i);
|
||||
self.tabs[i].scene.materialize_sketch_constraints_for_save();
|
||||
self.tabs[i].scene.materialize_named_parameters_for_save();
|
||||
self.tabs[i].scene.materialize_dwg_native_constraints_for_save();
|
||||
self.tabs[i].scene.materialize_dwg_native_constraints_for_save(self.write_dwg_native_constraints);
|
||||
let tab_id = self.tabs[i].id;
|
||||
let bounds = crate::ui::wrap_bar::dropdown_bounds(
|
||||
crate::app::view::VIEWPORT_CAPTURE_BOUNDS_ID,
|
||||
|
||||
@@ -6198,6 +6198,11 @@ impl OpenCADStudio {
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::WriteDwgNativeConstraintsChanged(enabled) => {
|
||||
self.write_dwg_native_constraints = enabled;
|
||||
self.persist_settings_if_changed();
|
||||
Task::none()
|
||||
}
|
||||
Message::LanguageChanged(language) => {
|
||||
if self.language == language {
|
||||
return Task::none();
|
||||
|
||||
@@ -195,6 +195,7 @@ impl OpenCADStudio {
|
||||
crate::ui::window::options::view_window(
|
||||
&self.default_save_format,
|
||||
self.file_assoc_enabled,
|
||||
self.write_dwg_native_constraints,
|
||||
&self.ui_theme,
|
||||
&self.theme_color_inputs,
|
||||
self.language,
|
||||
|
||||
@@ -1360,6 +1360,32 @@ pub enum CmdResult {
|
||||
point_b: DVec3,
|
||||
label: &'static str,
|
||||
},
|
||||
/// Add a persistent `CenterPoint`/`Midpoint`/`PointOnCurve` constraint
|
||||
/// (`crate::modules::draw::constrain::point_on_entity`) between one
|
||||
/// picked point and `target`, a whole entity selected before the tool
|
||||
/// ran. Like `AddCoincidentConstraint`, the host resolves `point` via
|
||||
/// `sketch_constraints::nearest_sketch_point` (a `CadCommand` has no
|
||||
/// document access) and, for `CenterPoint` specifically, addresses
|
||||
/// `target` via its center marker rather than as a whole entity — see
|
||||
/// `ConstraintKind::CenterPoint`'s own doc comment for why it's the
|
||||
/// same solve as `Coincident`/`Concentric` under a different DWG-native
|
||||
/// class name.
|
||||
AddPointOnEntityConstraint {
|
||||
point: DVec3,
|
||||
target: Handle,
|
||||
kind: crate::scene::sketch_constraints::ConstraintKind,
|
||||
label: &'static str,
|
||||
},
|
||||
/// Add a persistent `EqualDistance` constraint
|
||||
/// (`crate::modules::draw::constrain::equal_distance`): the distance
|
||||
/// between `points[0]`/`points[1]` equals the distance between
|
||||
/// `points[2]`/`points[3]`. The host resolves each point via
|
||||
/// `sketch_constraints::nearest_sketch_point`, same reasoning as
|
||||
/// `AddCoincidentConstraint`.
|
||||
AddEqualDistanceConstraint {
|
||||
points: [DVec3; 4],
|
||||
label: &'static str,
|
||||
},
|
||||
/// Attach one smart centre mark to a newly selected circular source.
|
||||
ReassociateCenterMark {
|
||||
target: Handle,
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
//! EDCONSTRAINT — AutoCAD's fourth `Equal` sub-kind
|
||||
//! (`ACEQUALDISTANCECONSTRAINT`): the distance between one point pair
|
||||
//! equals the distance between another, rather than two whole entities
|
||||
//! being equal length/radius. Needs four point picks (unlike the two
|
||||
//! whole-entity selections `ECONSTRAINT` uses), so — like
|
||||
//! `CoincidentConstraintCommand` — it accumulates raw points and hands them
|
||||
//! back for the host to resolve (`CadCommand` has no document access).
|
||||
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
|
||||
pub mod equal_distance_tool {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "EDCONSTRAINT",
|
||||
label: "Equal Distance",
|
||||
icon: IconKind::Glyph("≐"),
|
||||
event: ModuleEvent::Command("EDCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct EqualDistanceConstraintCommand {
|
||||
points: Vec<DVec3>,
|
||||
}
|
||||
|
||||
impl EqualDistanceConstraintCommand {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for EqualDistanceConstraintCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
"EDCONSTRAINT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match self.points.len() {
|
||||
0 => "EQUAL DISTANCE Specify first point of first pair:".to_string(),
|
||||
1 => "EQUAL DISTANCE Specify second point of first pair:".to_string(),
|
||||
2 => "EQUAL DISTANCE Specify first point of second pair:".to_string(),
|
||||
_ => "EQUAL DISTANCE Specify second point of second pair:".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
self.points.push(pt);
|
||||
if self.points.len() < 4 {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let [a, b, c, d] = self.points[..] else { unreachable!("just checked len == 4") };
|
||||
CmdResult::AddEqualDistanceConstraint { points: [a, b, c, d], label: "Equal distance constraint" }
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,36 @@
|
||||
//! Ribbon tools and typed-value commands for the persistent "Constraints"
|
||||
//! group (design doc `docs/parametric_system_design.md` §6.1).
|
||||
//!
|
||||
//! All eight — Horizontal, Vertical, Parallel, Perpendicular, Equal,
|
||||
//! Tangent, Distance, Angle — add a [`crate::scene::sketch_constraints::SketchConstraint`]
|
||||
//! to the current scope and let `Scene::bump_entities` solve it, the same
|
||||
//! Every one adds a [`crate::scene::sketch_constraints::SketchConstraint`]
|
||||
//! to the current scope and lets `Scene::bump_entities` solve it, the same
|
||||
//! path any later edit to the constrained entities re-solves through
|
||||
//! (`src/scene/sketch_solve.rs`). Horizontal/Vertical/Parallel/Perpendicular/
|
||||
//! Equal/Tangent dispatch straight to `CmdResult::AddSketchConstraint` from
|
||||
//! `src/app/commands/draw.rs` (no typed value needed, so no `CadCommand` of
|
||||
//! their own); Distance and Angle need one (`value.rs`), since the target
|
||||
//! value comes from the command line after the tool is clicked; Coincident
|
||||
//! needs one too (`coincident.rs`), since it addresses a *point* on an
|
||||
//! entity rather than the whole thing, so it can't reuse plain selection.
|
||||
//! (`src/scene/sketch_solve.rs`). By how they gather input:
|
||||
//! - **Plain selection, no picking** — Horizontal, Vertical, Fixed (select
|
||||
//! one entity); Parallel, Perpendicular, Equal, Tangent, Colinear,
|
||||
//! Concentric (select two); Symmetric (select three: the mirrored pair,
|
||||
//! then the mirror line) — dispatch straight to
|
||||
//! `CmdResult::AddSketchConstraint` from `src/app/commands/draw.rs`.
|
||||
//! - **A typed value** — Distance, Angle (`value.rs`): the target value
|
||||
//! comes from the command line after the tool is clicked.
|
||||
//! - **A picked point, entity pre-selected** — CenterPoint, Midpoint,
|
||||
//! PointOnCurve (`point_on_entity.rs`): the entity side has no ambiguity
|
||||
//! (select it first, like Distance/Angle's target), but the point side
|
||||
//! does (which of a line's two endpoints?), so it's picked interactively
|
||||
//! and resolved by the host.
|
||||
//! - **Picked points only, nothing pre-selected** — Coincident
|
||||
//! (`coincident.rs`, two points), EqualDistance (`equal_distance.rs`,
|
||||
//! four points): every side is a point, so plain selection can't express
|
||||
//! it at all.
|
||||
|
||||
mod coincident;
|
||||
mod equal_distance;
|
||||
mod point_on_entity;
|
||||
mod tools;
|
||||
mod value;
|
||||
pub use coincident::{coincident_tool, CoincidentConstraintCommand};
|
||||
pub use tools::{equal, horizontal, parallel, perpendicular, tangent, vertical};
|
||||
pub use equal_distance::{equal_distance_tool, EqualDistanceConstraintCommand};
|
||||
pub use point_on_entity::{center_point_tool, midpoint_tool, point_on_curve_tool, PointOnEntityConstraintCommand};
|
||||
pub use tools::{
|
||||
concentric, equal, fixed, horizontal, colinear, parallel, perpendicular, symmetric, tangent, vertical,
|
||||
};
|
||||
pub use value::{angle_tool, distance_tool, AngleConstraintCommand, DistanceConstraintCommand};
|
||||
@@ -0,0 +1,91 @@
|
||||
//! CENTERPOINT / MIDPOINT / ONCURVE — constraints between one picked point
|
||||
//! and a whole entity selected *before* the tool runs (same "select first,
|
||||
//! then invoke" convention `DistanceConstraintCommand`
|
||||
//! (`src/modules/draw/constrain/value.rs`) already uses for its own target
|
||||
//! entity). The entity side has no ambiguity (a whole circle, or a whole
|
||||
//! line whose midpoint/curve is being addressed, not one specific
|
||||
//! endpoint) so it doesn't need interactive picking; the point side does
|
||||
//! (which of a line's two endpoints, or some other addressable point), so
|
||||
//! this command accumulates just that one point pick and hands it back for
|
||||
//! the host to resolve via `nearest_sketch_point` — the same split
|
||||
//! `CoincidentConstraintCommand` uses and for the same reason (`CadCommand`
|
||||
//! has no document access).
|
||||
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::sketch_constraints::ConstraintKind;
|
||||
use acadrust::types::Handle;
|
||||
|
||||
pub mod center_point_tool {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "CPCONSTRAINT",
|
||||
label: "Center Point",
|
||||
icon: IconKind::Glyph("⊕"),
|
||||
event: ModuleEvent::Command("CPCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod midpoint_tool {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "MPCONSTRAINT",
|
||||
label: "Midpoint",
|
||||
icon: IconKind::Glyph("M"),
|
||||
event: ModuleEvent::Command("MPCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod point_on_curve_tool {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "OCCONSTRAINT",
|
||||
label: "Point on Curve",
|
||||
icon: IconKind::Glyph("∈"),
|
||||
event: ModuleEvent::Command("OCCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Shared by all three tools above — only `name`/`kind`/`label` differ.
|
||||
pub struct PointOnEntityConstraintCommand {
|
||||
name: &'static str,
|
||||
kind: ConstraintKind,
|
||||
target: Handle,
|
||||
label: &'static str,
|
||||
}
|
||||
|
||||
impl PointOnEntityConstraintCommand {
|
||||
pub fn new(name: &'static str, kind: ConstraintKind, target: Handle, label: &'static str) -> Self {
|
||||
Self { name, kind, target, label }
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for PointOnEntityConstraintCommand {
|
||||
fn name(&self) -> &'static str {
|
||||
self.name
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
"Specify point:".to_string()
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
CmdResult::AddPointOnEntityConstraint { point: pt, target: self.target, kind: self.kind, label: self.label }
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
@@ -73,3 +73,51 @@ pub mod tangent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod concentric {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "NCONSTRAINT",
|
||||
label: "Concentric",
|
||||
icon: IconKind::Glyph("◎"),
|
||||
event: ModuleEvent::Command("NCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod colinear {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "LCONSTRAINT",
|
||||
label: "Colinear",
|
||||
icon: IconKind::Glyph("L"),
|
||||
event: ModuleEvent::Command("LCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod fixed {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "FXCONSTRAINT",
|
||||
label: "Fixed",
|
||||
icon: IconKind::Glyph("F"),
|
||||
event: ModuleEvent::Command("FXCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub mod symmetric {
|
||||
use super::*;
|
||||
pub fn tool() -> ToolDef {
|
||||
ToolDef {
|
||||
id: "SYCONSTRAINT",
|
||||
label: "Symmetric",
|
||||
icon: IconKind::Glyph("S"),
|
||||
event: ModuleEvent::Command("SYCONSTRAINT".to_string()),
|
||||
}
|
||||
}
|
||||
}
|
||||
+13
-1
@@ -34,7 +34,11 @@ impl CadModule for DrawModule {
|
||||
};
|
||||
use crate::modules::insert::{create_block, insert_block};
|
||||
use clipboard::{copy_clip, cut, paste};
|
||||
use constrain::{angle_tool, coincident_tool, distance_tool, equal, horizontal, parallel, perpendicular, tangent, vertical};
|
||||
use constrain::{
|
||||
angle_tool, center_point_tool, coincident_tool, colinear, concentric, distance_tool, equal,
|
||||
equal_distance_tool, fixed, horizontal, midpoint_tool, parallel, perpendicular, point_on_curve_tool,
|
||||
symmetric, tangent, vertical,
|
||||
};
|
||||
use draw::{arc, circle, ellipse, hatch, line, polyline, shapes};
|
||||
use groups::{group, ungroup};
|
||||
use layers::{
|
||||
@@ -134,6 +138,14 @@ impl CadModule for DrawModule {
|
||||
coincident_tool::tool().into(),
|
||||
distance_tool::tool().into(),
|
||||
angle_tool::tool().into(),
|
||||
concentric::tool().into(),
|
||||
colinear::tool().into(),
|
||||
fixed::tool().into(),
|
||||
symmetric::tool().into(),
|
||||
center_point_tool::tool().into(),
|
||||
midpoint_tool::tool().into(),
|
||||
point_on_curve_tool::tool().into(),
|
||||
equal_distance_tool::tool().into(),
|
||||
],
|
||||
},
|
||||
RibbonGroup {
|
||||
|
||||
@@ -269,6 +269,18 @@ fn geometrical_class_name(kind: ConstraintKind) -> Option<&'static str> {
|
||||
ConstraintKind::Vertical => Some("ACVERTICALCONSTRAINT"),
|
||||
ConstraintKind::Perpendicular => Some("ACPERPENDICULARCONSTRAINT"),
|
||||
ConstraintKind::Tangent => Some("ACTANGENTCONSTRAINT"),
|
||||
ConstraintKind::Concentric => Some("ACCONCENTRICCONSTRAINT"),
|
||||
ConstraintKind::CenterPoint => Some("ACCENTERPOINTCONSTRAINT"),
|
||||
ConstraintKind::Colinear => Some("ACCOLINEARCONSTRAINT"),
|
||||
ConstraintKind::Fixed => Some("ACFIXEDCONSTRAINT"),
|
||||
ConstraintKind::Midpoint => Some("ACMIDPOINTCONSTRAINT"),
|
||||
ConstraintKind::PointOnCurve => Some("ACPOINTCURVECONSTRAINT"),
|
||||
ConstraintKind::Symmetric => Some("ACSYMMETRICCONSTRAINT"),
|
||||
// Also a plain `Geometrical`-shaped node in AutoCAD's own class list
|
||||
// (`is_plain_geometrical_constraint`) — unlike `Equal`, its class
|
||||
// name never branches on entity type, so it belongs here rather
|
||||
// than in the caller's per-entity-type dispatch.
|
||||
ConstraintKind::EqualDistance => Some("ACEQUALDISTANCECONSTRAINT"),
|
||||
// `Equal` isn't one native kind — the line/circle split below
|
||||
// (design doc §5) needs the resolved entity types, not just the
|
||||
// ConstraintKind, so it's handled by the caller instead of here.
|
||||
@@ -664,6 +676,24 @@ fn remove_owned_recursive(document: &mut CadDocument, root: Handle) {
|
||||
document.objects.remove(&root);
|
||||
}
|
||||
|
||||
/// Whether `owner`'s scope already carries a materialized native graph —
|
||||
/// from an earlier save with the setting on, or from a file that came from
|
||||
/// real AutoCAD/BricsCAD. Drives the "sync-if-present, create-only-if-
|
||||
/// enabled" rule in [`Scene::materialize_dwg_native_constraints_for_save`]:
|
||||
/// a scope that already has this graph keeps getting it rebuilt on every
|
||||
/// save regardless of the current setting, so turning the setting off never
|
||||
/// leaves a stale, out-of-sync graph behind — only a scope that doesn't have
|
||||
/// one yet is gated by the setting.
|
||||
fn has_native_network(document: &CadDocument, owner: Handle) -> bool {
|
||||
let Some(dictionary_handle) = document.extension_dictionary_handle(owner) else {
|
||||
return false;
|
||||
};
|
||||
let Some(ObjectType::Dictionary(dictionary)) = document.objects.get(&dictionary_handle) else {
|
||||
return false;
|
||||
};
|
||||
dictionary.get(NETWORK_DICTIONARY_KEY).is_some()
|
||||
}
|
||||
|
||||
/// One scope's worth of
|
||||
/// [`Scene::materialize_dwg_native_constraints_for_save`] — see that
|
||||
/// method's doc comment for the save-time contract this implements.
|
||||
@@ -784,14 +814,30 @@ impl Scene {
|
||||
/// "materialize right before save, from scratch every time" convention
|
||||
/// `sketch_persist.rs`'s `materialize_sketch_constraints_for_save` uses,
|
||||
/// called alongside it, not instead of it (design doc §6: additive).
|
||||
pub(crate) fn materialize_dwg_native_constraints_for_save(&mut self) {
|
||||
ensure_associative_classes_registered(&mut self.document);
|
||||
///
|
||||
/// `enabled` is the app's `write_dwg_native_constraints` Options toggle
|
||||
/// (off by default — this graph adds file weight every save, whether or
|
||||
/// not AutoCAD/BricsCAD interop is wanted). It only gates *creating* the
|
||||
/// graph in a scope that doesn't have one yet: a scope that already
|
||||
/// carries one — from an earlier save with the setting on, or from a
|
||||
/// file that came from real AutoCAD — keeps getting it rebuilt in sync
|
||||
/// regardless of the current setting ([`has_native_network`]), so
|
||||
/// toggling this off never leaves a stale graph sitting in the file.
|
||||
pub(crate) fn materialize_dwg_native_constraints_for_save(&mut self, enabled: bool) {
|
||||
let mut classes_registered = false;
|
||||
for index in 0..self.sketch_constraints.len() {
|
||||
let set = self.sketch_constraints[index].clone();
|
||||
let owner = set.scope.owner_handle(&self.document);
|
||||
if owner.is_null() {
|
||||
continue;
|
||||
}
|
||||
if !enabled && !has_native_network(&self.document, owner) {
|
||||
continue;
|
||||
}
|
||||
if !classes_registered {
|
||||
ensure_associative_classes_registered(&mut self.document);
|
||||
classes_registered = true;
|
||||
}
|
||||
materialize_scope(&mut self.document, owner, &set, &self.named_parameters);
|
||||
}
|
||||
}
|
||||
@@ -801,7 +847,7 @@ impl Scene {
|
||||
mod tests {
|
||||
use super::super::sketch_constraints::SketchScope;
|
||||
use super::*;
|
||||
use acadrust::entities::Line;
|
||||
use acadrust::entities::{Circle, Line};
|
||||
|
||||
fn line_entity(scene: &mut Scene, start: (f64, f64), end: (f64, f64)) -> Handle {
|
||||
scene.add_entity(EntityType::Line(Line::from_points(
|
||||
@@ -810,6 +856,10 @@ mod tests {
|
||||
)))
|
||||
}
|
||||
|
||||
fn circle_entity(scene: &mut Scene, center: (f64, f64), radius: f64) -> Handle {
|
||||
scene.add_entity(EntityType::Circle(Circle::from_center_radius(Vector3::new(center.0, center.1, 0.0), radius)))
|
||||
}
|
||||
|
||||
/// Digs a scope's materialized native graph back out of `document`:
|
||||
/// `owner`'s extension dictionary -> `ACAD_ASSOCNETWORK` entry ->
|
||||
/// its one owned action, which (for this module) is always the
|
||||
@@ -854,7 +904,7 @@ mod tests {
|
||||
None,
|
||||
);
|
||||
|
||||
scene.materialize_dwg_native_constraints_for_save();
|
||||
scene.materialize_dwg_native_constraints_for_save(true);
|
||||
let owner = scene.document.header.model_space_block_handle;
|
||||
// Sanity before the round trip even happens: root + 2 geometry
|
||||
// nodes + 2 constraint nodes.
|
||||
@@ -915,7 +965,7 @@ mod tests {
|
||||
#[test]
|
||||
fn a_dwg_save_keeps_the_full_dependency_chain_including_the_named_variable() {
|
||||
let (mut scene, owner) = distance_with_named_parameter_scene();
|
||||
scene.materialize_dwg_native_constraints_for_save();
|
||||
scene.materialize_dwg_native_constraints_for_save(true);
|
||||
assert!(has_variable_named(&scene.document, "gap"), "expected an AssocVariable BEFORE serialization");
|
||||
|
||||
let bytes = crate::io::save_to_bytes(&scene.document, "dwg", scene.document.version)
|
||||
@@ -951,7 +1001,7 @@ mod tests {
|
||||
#[test]
|
||||
fn a_dxf_save_keeps_only_the_constraint_group_shell() {
|
||||
let (mut scene, owner) = distance_with_named_parameter_scene();
|
||||
scene.materialize_dwg_native_constraints_for_save();
|
||||
scene.materialize_dwg_native_constraints_for_save(true);
|
||||
|
||||
let bytes = crate::io::save_to_bytes(&scene.document, "dxf", scene.document.version)
|
||||
.unwrap_or_else(|e| panic!("save to dxf: {e}"));
|
||||
@@ -981,9 +1031,9 @@ mod tests {
|
||||
None,
|
||||
);
|
||||
|
||||
scene.materialize_dwg_native_constraints_for_save();
|
||||
scene.materialize_dwg_native_constraints_for_save(true);
|
||||
let first_count = scene.document.objects.len();
|
||||
scene.materialize_dwg_native_constraints_for_save();
|
||||
scene.materialize_dwg_native_constraints_for_save(true);
|
||||
let second_count = scene.document.objects.len();
|
||||
assert_eq!(first_count, second_count, "a resave with the same constraints must not accumulate new objects");
|
||||
}
|
||||
@@ -992,7 +1042,7 @@ mod tests {
|
||||
fn an_empty_constraint_set_leaves_no_native_network() {
|
||||
let mut scene = Scene::new();
|
||||
let _ = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
scene.materialize_dwg_native_constraints_for_save();
|
||||
scene.materialize_dwg_native_constraints_for_save(true);
|
||||
|
||||
let owner = scene.document.header.model_space_block_handle;
|
||||
let dict = scene.document.extension_dictionary_handle(owner);
|
||||
@@ -1005,4 +1055,141 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn model_space_has_native_network(scene: &Scene) -> bool {
|
||||
has_native_network(&scene.document, scene.document.header.model_space_block_handle)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_setting_off_skips_a_scope_with_no_existing_native_graph() {
|
||||
let mut scene = Scene::new();
|
||||
let a = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Horizontal,
|
||||
vec![SketchRef::whole(a)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.materialize_dwg_native_constraints_for_save(false);
|
||||
assert!(
|
||||
!model_space_has_native_network(&scene),
|
||||
"a scope with no prior native graph should stay untouched while the setting is off"
|
||||
);
|
||||
}
|
||||
|
||||
/// The "sync-if-present, create-only-if-enabled" rule from
|
||||
/// `Scene::materialize_dwg_native_constraints_for_save`'s doc comment:
|
||||
/// once a scope has the native graph (here, from an earlier save with
|
||||
/// the setting on), turning the setting off must not freeze that graph
|
||||
/// out of date — it should keep tracking the current constraints.
|
||||
#[test]
|
||||
fn the_setting_off_still_resyncs_a_scope_that_already_has_a_native_graph() {
|
||||
let mut scene = Scene::new();
|
||||
let a = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Horizontal,
|
||||
vec![SketchRef::whole(a)],
|
||||
None,
|
||||
);
|
||||
scene.materialize_dwg_native_constraints_for_save(true);
|
||||
assert!(model_space_has_native_network(&scene), "sanity: the graph should exist after the first, enabled save");
|
||||
|
||||
let b = line_entity(&mut scene, (0.0, 0.0), (0.0, 10.0));
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Vertical,
|
||||
vec![SketchRef::whole(b)],
|
||||
None,
|
||||
);
|
||||
scene.materialize_dwg_native_constraints_for_save(false);
|
||||
|
||||
let owner = scene.document.header.model_space_block_handle;
|
||||
let group = native_group(&scene.document, owner);
|
||||
assert!(
|
||||
group.nodes.iter().any(|n| n.class_name == "ACVERTICALCONSTRAINT"),
|
||||
"the second, disabled-setting save should still pick up the new Vertical constraint, got {:?}",
|
||||
group.nodes.iter().map(|n| &n.class_name).collect::<Vec<_>>()
|
||||
);
|
||||
}
|
||||
|
||||
/// The other half of the same rule: disabling the setting after a scope
|
||||
/// already has the graph must not make that graph disappear either —
|
||||
/// only remove it by clearing the scope's constraints (existing
|
||||
/// `an_empty_constraint_set_leaves_no_native_network` behavior), not by
|
||||
/// toggling the setting off.
|
||||
#[test]
|
||||
fn the_setting_off_does_not_remove_an_existing_native_graph() {
|
||||
let mut scene = Scene::new();
|
||||
let a = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Horizontal,
|
||||
vec![SketchRef::whole(a)],
|
||||
None,
|
||||
);
|
||||
scene.materialize_dwg_native_constraints_for_save(true);
|
||||
scene.materialize_dwg_native_constraints_for_save(false);
|
||||
assert!(
|
||||
model_space_has_native_network(&scene),
|
||||
"an existing native graph must survive a disabled-setting resave, not be silently dropped"
|
||||
);
|
||||
}
|
||||
|
||||
/// The 8 new `ConstraintKind`s all reuse the same generic
|
||||
/// `Geometrical`-shaped node path `geometrical_class_name` drives (see
|
||||
/// its own doc comment) — one representative mix of ref shapes (2 whole
|
||||
/// entities, an asymmetric point+whole pair, and a 3-ref case) is
|
||||
/// enough to confirm that path handles them all, without one dedicated
|
||||
/// test per kind.
|
||||
#[test]
|
||||
fn new_constraint_kinds_round_trip_with_their_own_dwg_native_class_names() {
|
||||
for ext in ["dxf", "dwg"] {
|
||||
let mut scene = Scene::new();
|
||||
let circle_a = circle_entity(&mut scene, (0.0, 0.0), 3.0);
|
||||
let circle_b = circle_entity(&mut scene, (5.0, 5.0), 1.0);
|
||||
let line_a = line_entity(&mut scene, (0.0, 0.0), (10.0, 0.0));
|
||||
let line_b = line_entity(&mut scene, (3.0, 4.0), (7.0, 6.0));
|
||||
let marker = line_entity(&mut scene, (20.0, 20.0), (21.0, 21.0));
|
||||
let axis = line_entity(&mut scene, (0.0, 0.0), (0.0, 10.0));
|
||||
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
set.add(ConstraintKind::Concentric, vec![SketchRef::center(circle_a), SketchRef::center(circle_b)], None);
|
||||
set.add(ConstraintKind::Colinear, vec![SketchRef::whole(line_a), SketchRef::whole(line_b)], None);
|
||||
set.add(ConstraintKind::Fixed, vec![SketchRef::whole(line_a)], None);
|
||||
set.add(ConstraintKind::CenterPoint, vec![SketchRef::point(marker, 0), SketchRef::center(circle_a)], None);
|
||||
set.add(ConstraintKind::Midpoint, vec![SketchRef::point(marker, 1), SketchRef::whole(line_a)], None);
|
||||
set.add(ConstraintKind::PointOnCurve, vec![SketchRef::point(marker, 0), SketchRef::whole(circle_a)], None);
|
||||
set.add(
|
||||
ConstraintKind::EqualDistance,
|
||||
vec![SketchRef::point(line_b, 0), SketchRef::point(line_b, 1), SketchRef::point(line_a, 0), SketchRef::point(line_a, 1)],
|
||||
None,
|
||||
);
|
||||
set.add(
|
||||
ConstraintKind::Symmetric,
|
||||
vec![SketchRef::center(circle_a), SketchRef::center(circle_b), SketchRef::whole(axis)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.materialize_dwg_native_constraints_for_save(true);
|
||||
let owner = scene.document.header.model_space_block_handle;
|
||||
|
||||
let bytes = crate::io::save_to_bytes(&scene.document, ext, scene.document.version)
|
||||
.unwrap_or_else(|e| panic!("save to {ext}: {e}"));
|
||||
let reloaded = crate::io::load_bytes(&format!("new_kinds.{ext}"), bytes)
|
||||
.unwrap_or_else(|e| panic!("reload {ext}: {e}"));
|
||||
|
||||
let group = native_group(&reloaded, owner);
|
||||
let class_names: Vec<&str> = group.nodes.iter().map(|n| n.class_name.as_str()).collect();
|
||||
for expected in [
|
||||
"ACCONCENTRICCONSTRAINT",
|
||||
"ACCOLINEARCONSTRAINT",
|
||||
"ACFIXEDCONSTRAINT",
|
||||
"ACCENTERPOINTCONSTRAINT",
|
||||
"ACMIDPOINTCONSTRAINT",
|
||||
"ACPOINTCURVECONSTRAINT",
|
||||
"ACEQUALDISTANCECONSTRAINT",
|
||||
"ACSYMMETRICCONSTRAINT",
|
||||
] {
|
||||
assert!(class_names.contains(&expected), "{ext}: missing {expected} node, got {class_names:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -79,6 +79,41 @@ pub enum ConstraintKind {
|
||||
Angle,
|
||||
Radius,
|
||||
Tangent,
|
||||
/// Two circles/arcs share a center — `refs`: `[center(a), center(b)]`.
|
||||
/// Solves identically to `Coincident` (`sketch_solve.rs` broadens that
|
||||
/// match arm rather than duplicating it) — only the DWG-native class
|
||||
/// name (`ACCONCENTRICCONSTRAINT` vs `ACPOINTCOINCIDENCECONSTRAINT`)
|
||||
/// and the UI entry point differ.
|
||||
Concentric,
|
||||
/// A point sits at a circle/arc's center — `refs`: `[point, center(circle)]`.
|
||||
/// Same solver math as `Coincident`/`Concentric`, different DWG class
|
||||
/// name (`ACCENTERPOINTCONSTRAINT`).
|
||||
CenterPoint,
|
||||
/// Two lines share the same infinite line — `refs`: `[whole(a), whole(b)]`.
|
||||
Colinear,
|
||||
/// A point sits at another line's midpoint — `refs`: `[point, whole(line)]`.
|
||||
Midpoint,
|
||||
/// Locks a whole entity at its current position — `refs`: `[whole(entity)]`.
|
||||
/// No `driving_param`: the target is the entity's own live geometry at
|
||||
/// solve time, not a typed value (see `sketch_solve.rs`'s `Fixed` arm).
|
||||
Fixed,
|
||||
/// A point lies anywhere along a line's or circle's curve (not
|
||||
/// restricted to an endpoint/center) — `refs`: `[point, whole(entity)]`.
|
||||
PointOnCurve,
|
||||
/// The distance between one point pair equals the distance between
|
||||
/// another — `refs`: `[p1, p2, p3, p4]` (`dist(p1,p2) == dist(p3,p4)`).
|
||||
/// AutoCAD's fourth `Equal` sub-kind (`ACEQUALDISTANCECONSTRAINT`) —
|
||||
/// see the design doc for why `EqualCurvature`, the other missing
|
||||
/// sub-kind, isn't modeled: for the only entity types this system
|
||||
/// solves (Line, Circle), it would be mathematically identical to this
|
||||
/// `Equal`'s existing circle/circle (radius) branch.
|
||||
EqualDistance,
|
||||
/// Two circles/arcs are mirror images of each other across a line —
|
||||
/// `refs`: `[center(a), center(b), whole(mirror_line)]`. Scoped to the
|
||||
/// circle-center-pair case (unambiguous with whole-entity selection);
|
||||
/// point-symmetry about a point, and symmetry between two lines, aren't
|
||||
/// modeled.
|
||||
Symmetric,
|
||||
}
|
||||
|
||||
pub type ConstraintId = u32;
|
||||
@@ -345,6 +380,14 @@ impl ConstraintKind {
|
||||
ConstraintKind::Angle => "∠",
|
||||
ConstraintKind::Radius => "R",
|
||||
ConstraintKind::Tangent => "T",
|
||||
ConstraintKind::Concentric => "◎",
|
||||
ConstraintKind::CenterPoint => "⊕",
|
||||
ConstraintKind::Colinear => "L",
|
||||
ConstraintKind::Midpoint => "M",
|
||||
ConstraintKind::Fixed => "F",
|
||||
ConstraintKind::PointOnCurve => "∈",
|
||||
ConstraintKind::EqualDistance => "≐",
|
||||
ConstraintKind::Symmetric => "S",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+161
-76
@@ -29,9 +29,10 @@ use acadrust::entities::EntityType;
|
||||
use acadrust::types::Handle;
|
||||
|
||||
use ocs_gcs::constraints::angle_distance::L2LAngle;
|
||||
use ocs_gcs::constraints::circle_arc::{C2LDistance, TangentCircumf};
|
||||
use ocs_gcs::constraints::circle_arc::{C2LDistance, P2CDistance, TangentCircumf};
|
||||
use ocs_gcs::constraints::point_line::{
|
||||
Equal, EqualLineLength, Parallel as ParallelConstraint, Perpendicular as PerpendicularConstraint, P2PDistance,
|
||||
CenterOfGravity, Equal, EqualLineLength, MidpointOnLine, Parallel as ParallelConstraint,
|
||||
Perpendicular as PerpendicularConstraint, PointOnLine, P2PDistance,
|
||||
};
|
||||
use ocs_gcs::constraints::Constraint;
|
||||
use ocs_gcs::geo::{Circle as GCircle, Line as GLine, Point as GPoint};
|
||||
@@ -111,14 +112,18 @@ fn resolve_ref(
|
||||
Some(*cache.get(&r.entity)?)
|
||||
}
|
||||
|
||||
/// One [`SketchConstraint`]'s `ocs_gcs` construction, or `None` if it can't
|
||||
/// be built (an unsupported/not-yet-mapped kind, a dangling ref, a ref
|
||||
/// shape the kind doesn't expect — e.g. `Parallel` needs two whole-line
|
||||
/// refs — or, for a dimensional kind, a `driving_param` that fails to
|
||||
/// resolve: an undefined named-parameter reference or a division by zero in
|
||||
/// its formula, design doc `named_parameters_design.md` stage 3). A
|
||||
/// constraint that can't be built is simply skipped for this solve, not an
|
||||
/// error: geometry it would have constrained is left alone, matching how a
|
||||
/// One [`SketchConstraint`]'s `ocs_gcs` construction: every system-level
|
||||
/// constraint it contributes (most kinds contribute exactly one; a few —
|
||||
/// `Coincident`/`Concentric`/`CenterPoint`'s X+Y halves, `Colinear`'s two
|
||||
/// endpoints, `Midpoint`'s X+Y, `Symmetric`'s midpoint+perpendicular pair,
|
||||
/// `Fixed`'s per-coordinate pins — contribute more). Empty means it can't be
|
||||
/// built (an unsupported/not-yet-mapped kind, a dangling ref, a ref shape
|
||||
/// the kind doesn't expect — e.g. `Parallel` needs two whole-line refs — or,
|
||||
/// for a dimensional kind, a `driving_param` that fails to resolve: an
|
||||
/// undefined named-parameter reference or a division by zero in its
|
||||
/// formula, design doc `named_parameters_design.md` stage 3). A constraint
|
||||
/// that can't be built is simply skipped for this solve, not an error:
|
||||
/// geometry it would have constrained is left alone, matching how a
|
||||
/// dangling associative-dimension reference degrades today rather than
|
||||
/// aborting the whole recompute. (A named-parameter *cycle* should be
|
||||
/// unreachable here — `ParameterTable::set` already refuses to create one —
|
||||
@@ -130,9 +135,9 @@ fn build_constraint(
|
||||
cache: &mut HashMap<Handle, EntityGeom>,
|
||||
params: &ParameterTable,
|
||||
c: &SketchConstraint,
|
||||
) -> Option<Rc<dyn Constraint>> {
|
||||
) -> Vec<Rc<dyn Constraint>> {
|
||||
if !c.enabled {
|
||||
return None;
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let whole_line = |sys: &mut System, cache: &mut HashMap<_, _>, r: SketchRef| match resolve_ref(document, sys, cache, r)? {
|
||||
@@ -148,64 +153,159 @@ fn build_constraint(
|
||||
resolve_ref(document, sys, cache, r)?.point_for_marker(marker)
|
||||
};
|
||||
|
||||
// `Coincident`/`Concentric`/`CenterPoint` all solve identically — two
|
||||
// points (an endpoint, a circle's center via the existing `-3` marker,
|
||||
// or a plain point) held equal on both axes. Only the DWG-native class
|
||||
// name and the UI entry point that produces their `refs` differ.
|
||||
let point_pair_equal = |sys: &mut System, cache: &mut HashMap<_, _>, refs: &[SketchRef]| -> Vec<Rc<dyn Constraint>> {
|
||||
let [a, b] = refs else { return Vec::new() };
|
||||
let (Some(pa), Some(pb)) = (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) else { return Vec::new() };
|
||||
vec![Rc::new(Equal::new(pa.x, pb.x, 1.0)), Rc::new(Equal::new(pa.y, pb.y, 1.0))]
|
||||
};
|
||||
|
||||
match c.kind {
|
||||
ConstraintKind::Coincident => {
|
||||
let [a, b] = c.refs.as_slice() else { return None };
|
||||
let (pa, pb) = (point_ref(sys, cache, *a)?, point_ref(sys, cache, *b)?);
|
||||
// Two coordinate-equal constraints packaged as one via a small
|
||||
// local combinator isn't worth it for a single call site — the
|
||||
// caller (`solve_scope`) already `extend`s a `Vec` per
|
||||
// constraint, so this returns just the X constraint and Y is
|
||||
// added as a second, independent `SketchConstraint`-less
|
||||
// push — see `solve_scope`'s handling of this arm specifically.
|
||||
Some(Rc::new(Equal::new(pa.x, pb.x, 1.0)))
|
||||
ConstraintKind::Coincident | ConstraintKind::Concentric | ConstraintKind::CenterPoint => {
|
||||
point_pair_equal(sys, cache, &c.refs)
|
||||
}
|
||||
ConstraintKind::Horizontal => {
|
||||
let l = whole_line(sys, cache, *c.refs.first()?)?;
|
||||
Some(Rc::new(Equal::new(l.p1.y, l.p2.y, 1.0)))
|
||||
let Some(r) = c.refs.first() else { return Vec::new() };
|
||||
let Some(l) = whole_line(sys, cache, *r) else { return Vec::new() };
|
||||
vec![Rc::new(Equal::new(l.p1.y, l.p2.y, 1.0))]
|
||||
}
|
||||
ConstraintKind::Vertical => {
|
||||
let l = whole_line(sys, cache, *c.refs.first()?)?;
|
||||
Some(Rc::new(Equal::new(l.p1.x, l.p2.x, 1.0)))
|
||||
let Some(r) = c.refs.first() else { return Vec::new() };
|
||||
let Some(l) = whole_line(sys, cache, *r) else { return Vec::new() };
|
||||
vec![Rc::new(Equal::new(l.p1.x, l.p2.x, 1.0))]
|
||||
}
|
||||
ConstraintKind::Parallel => {
|
||||
let [a, b] = c.refs.as_slice() else { return None };
|
||||
let (fixed, moving) = (whole_line(sys, cache, *a)?, whole_line(sys, cache, *b)?);
|
||||
Some(Rc::new(ParallelConstraint::new(sys.store(), moving, fixed)))
|
||||
let [a, b] = c.refs.as_slice() else { return Vec::new() };
|
||||
let (Some(fixed), Some(moving)) = (whole_line(sys, cache, *a), whole_line(sys, cache, *b)) else { return Vec::new() };
|
||||
vec![Rc::new(ParallelConstraint::new(sys.store(), moving, fixed))]
|
||||
}
|
||||
ConstraintKind::Perpendicular => {
|
||||
let [a, b] = c.refs.as_slice() else { return None };
|
||||
let (fixed, moving) = (whole_line(sys, cache, *a)?, whole_line(sys, cache, *b)?);
|
||||
Some(Rc::new(PerpendicularConstraint::new(sys.store(), moving, fixed)))
|
||||
let [a, b] = c.refs.as_slice() else { return Vec::new() };
|
||||
let (Some(fixed), Some(moving)) = (whole_line(sys, cache, *a), whole_line(sys, cache, *b)) else { return Vec::new() };
|
||||
vec![Rc::new(PerpendicularConstraint::new(sys.store(), moving, fixed))]
|
||||
}
|
||||
ConstraintKind::Equal => {
|
||||
let [a, b] = c.refs.as_slice() else { return None };
|
||||
let [a, b] = c.refs.as_slice() else { return Vec::new() };
|
||||
if let (Some(la), Some(lb)) = (whole_line(sys, cache, *a), whole_line(sys, cache, *b)) {
|
||||
return Some(Rc::new(EqualLineLength::new(lb, la)));
|
||||
return vec![Rc::new(EqualLineLength::new(lb, la))];
|
||||
}
|
||||
let (Some(ca), Some(cb)) = (whole_circle(sys, cache, *a), whole_circle(sys, cache, *b)) else { return Vec::new() };
|
||||
vec![Rc::new(Equal::new(cb.rad, ca.rad, 1.0))]
|
||||
}
|
||||
ConstraintKind::EqualDistance => {
|
||||
let [a, b, c2, d] = c.refs.as_slice() else { return Vec::new() };
|
||||
let (Some(pa), Some(pb), Some(pc), Some(pd)) =
|
||||
(point_ref(sys, cache, *a), point_ref(sys, cache, *b), point_ref(sys, cache, *c2), point_ref(sys, cache, *d))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
vec![Rc::new(EqualLineLength::new(GLine { p1: pc, p2: pd }, GLine { p1: pa, p2: pb }))]
|
||||
}
|
||||
ConstraintKind::Colinear => {
|
||||
let [a, b] = c.refs.as_slice() else { return Vec::new() };
|
||||
let (Some(onto), Some(moving)) = (whole_line(sys, cache, *a), whole_line(sys, cache, *b)) else { return Vec::new() };
|
||||
// Pinning both of `moving`'s endpoints onto `onto`'s infinite
|
||||
// line forces the two to coincide (as long as `moving`'s own
|
||||
// two points stay distinct) — no dedicated "colinear" primitive
|
||||
// needed, `PointOnLine` applied twice does it.
|
||||
vec![Rc::new(PointOnLine::new(moving.p1, onto)), Rc::new(PointOnLine::new(moving.p2, onto))]
|
||||
}
|
||||
ConstraintKind::Midpoint => {
|
||||
let [a, b] = c.refs.as_slice() else { return Vec::new() };
|
||||
let (Some(p), Some(l)) = (point_ref(sys, cache, *a), whole_line(sys, cache, *b)) else { return Vec::new() };
|
||||
vec![
|
||||
Rc::new(CenterOfGravity::new(p.x, vec![l.p1.x, l.p2.x], vec![0.5, 0.5])),
|
||||
Rc::new(CenterOfGravity::new(p.y, vec![l.p1.y, l.p2.y], vec![0.5, 0.5])),
|
||||
]
|
||||
}
|
||||
ConstraintKind::PointOnCurve => {
|
||||
let [a, b] = c.refs.as_slice() else { return Vec::new() };
|
||||
let (Some(p), Some(geom)) = (point_ref(sys, cache, *a), resolve_ref(document, sys, cache, *b)) else {
|
||||
return Vec::new();
|
||||
};
|
||||
match geom {
|
||||
EntityGeom::Line(l) => vec![Rc::new(PointOnLine::new(p, l))],
|
||||
EntityGeom::Circle(circ) => {
|
||||
let zero = sys.add_param(0.0, true);
|
||||
vec![Rc::new(P2CDistance::new(circ, p, zero))]
|
||||
}
|
||||
}
|
||||
}
|
||||
ConstraintKind::Symmetric => {
|
||||
let [a, b, m] = c.refs.as_slice() else { return Vec::new() };
|
||||
let (Some(pa), Some(pb), Some(mirror)) =
|
||||
(point_ref(sys, cache, *a), point_ref(sys, cache, *b), whole_line(sys, cache, *m))
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
let pair = GLine { p1: pa, p2: pb };
|
||||
vec![
|
||||
Rc::new(MidpointOnLine::new(pair, mirror)),
|
||||
Rc::new(PerpendicularConstraint::new(sys.store(), pair, mirror)),
|
||||
]
|
||||
}
|
||||
ConstraintKind::Fixed => {
|
||||
let Some(r) = c.refs.first() else { return Vec::new() };
|
||||
let Some(geom) = resolve_ref(document, sys, cache, *r) else {
|
||||
return Vec::new();
|
||||
};
|
||||
match geom {
|
||||
EntityGeom::Line(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)),
|
||||
]
|
||||
}
|
||||
}
|
||||
let (ca, cb) = (whole_circle(sys, cache, *a)?, whole_circle(sys, cache, *b)?);
|
||||
Some(Rc::new(Equal::new(cb.rad, ca.rad, 1.0)))
|
||||
}
|
||||
ConstraintKind::Distance => {
|
||||
let [a, b] = c.refs.as_slice() else { return None };
|
||||
let (pa, pb) = (point_ref(sys, cache, *a)?, point_ref(sys, cache, *b)?);
|
||||
let target = sys.add_param(c.driving_param.as_ref()?.resolve(params).ok()?, true);
|
||||
Some(Rc::new(P2PDistance::new(pa, pb, target)))
|
||||
let [a, b] = c.refs.as_slice() else { return Vec::new() };
|
||||
let (Some(pa), Some(pb)) = (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) else { return Vec::new() };
|
||||
let Some(Ok(resolved)) = c.driving_param.as_ref().map(|d| d.resolve(params)) else { return Vec::new() };
|
||||
let target = sys.add_param(resolved, true);
|
||||
vec![Rc::new(P2PDistance::new(pa, pb, target))]
|
||||
}
|
||||
ConstraintKind::Angle => {
|
||||
let [a, b] = c.refs.as_slice() else { return None };
|
||||
let (fixed, moving) = (whole_line(sys, cache, *a)?, whole_line(sys, cache, *b)?);
|
||||
let angle = sys.add_param(c.driving_param.as_ref()?.resolve(params).ok()?.to_radians(), true);
|
||||
Some(Rc::new(L2LAngle::new(fixed, moving, angle)))
|
||||
let [a, b] = c.refs.as_slice() else { return Vec::new() };
|
||||
let (Some(fixed), Some(moving)) = (whole_line(sys, cache, *a), whole_line(sys, cache, *b)) else { return Vec::new() };
|
||||
let Some(Ok(resolved)) = c.driving_param.as_ref().map(|d| d.resolve(params)) else { return Vec::new() };
|
||||
let angle = sys.add_param(resolved.to_radians(), true);
|
||||
vec![Rc::new(L2LAngle::new(fixed, moving, angle))]
|
||||
}
|
||||
ConstraintKind::Radius => {
|
||||
let circle = whole_circle(sys, cache, *c.refs.first()?)?;
|
||||
let target = sys.add_param(c.driving_param.as_ref()?.resolve(params).ok()?, true);
|
||||
Some(Rc::new(Equal::new(circle.rad, target, 1.0)))
|
||||
let Some(r) = c.refs.first() else { return Vec::new() };
|
||||
let Some(circle) = whole_circle(sys, cache, *r) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let Some(Ok(resolved)) = c.driving_param.as_ref().map(|d| d.resolve(params)) else { return Vec::new() };
|
||||
let target = sys.add_param(resolved, true);
|
||||
vec![Rc::new(Equal::new(circle.rad, target, 1.0))]
|
||||
}
|
||||
ConstraintKind::Tangent => {
|
||||
let [a, b] = c.refs.as_slice() else { return None };
|
||||
let (ga, gb) = (resolve_ref(document, sys, cache, *a)?, resolve_ref(document, sys, cache, *b)?);
|
||||
let [a, b] = c.refs.as_slice() else { return Vec::new() };
|
||||
let (Some(ga), Some(gb)) = (resolve_ref(document, sys, cache, *a), resolve_ref(document, sys, cache, *b)) else {
|
||||
return Vec::new();
|
||||
};
|
||||
match (ga, gb) {
|
||||
(EntityGeom::Circle(c1), EntityGeom::Circle(c2)) => {
|
||||
let store = sys.store();
|
||||
@@ -217,7 +317,7 @@ fn build_constraint(
|
||||
// the first solve doesn't have to cross the singularity
|
||||
// between the two tangency configurations.
|
||||
let internal = center_dist < (r1 - r2).abs();
|
||||
Some(Rc::new(TangentCircumf::new(c1.center, c2.center, c1.rad, c2.rad, internal)))
|
||||
vec![Rc::new(TangentCircumf::new(c1.center, c2.center, c1.rad, c2.rad, internal))]
|
||||
}
|
||||
(EntityGeom::Circle(circ), EntityGeom::Line(line)) | (EntityGeom::Line(line), EntityGeom::Circle(circ)) => {
|
||||
let store = sys.store();
|
||||
@@ -234,11 +334,11 @@ fn build_constraint(
|
||||
// (see its `error_grad`), i.e. plain tangency rather than
|
||||
// an offset distance.
|
||||
let zero = sys.add_param(0.0, true);
|
||||
Some(Rc::new(C2LDistance::new(circ, line, zero, ccw, false)))
|
||||
vec![Rc::new(C2LDistance::new(circ, line, zero, ccw, false))]
|
||||
}
|
||||
// Line-Line tangency has no meaning; Arc isn't a supported
|
||||
// `EntityGeom` yet (see that type's own doc comment).
|
||||
_ => None,
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -265,35 +365,20 @@ fn solve_scope(
|
||||
) -> Option<(Vec<(Handle, EntityType)>, usize, Vec<(ConstraintId, ocs_gcs::diagnosis::RedundancyKind)>)> {
|
||||
let mut sys = System::new();
|
||||
let mut cache: HashMap<Handle, EntityGeom> = HashMap::new();
|
||||
// Tracks which system-level `ocs_gcs` constraint came from which
|
||||
// Tracks which system-level `ocs_gcs` constraint(s) came from which
|
||||
// `SketchConstraint` — a `SubSystem`'s redundant-row indices (from
|
||||
// `ocs_gcs::diagnosis`) are local to that partition's own constraint
|
||||
// list, not `set.constraints`' indices, and Coincident contributes two
|
||||
// system-level constraints (X and Y halves) for one `SketchConstraint`.
|
||||
// `Rc::ptr_eq` against this after partitioning resolves a row back to
|
||||
// the `ConstraintId` the UI actually names.
|
||||
// list, not `set.constraints`' indices, and several kinds contribute
|
||||
// more than one system-level constraint per `SketchConstraint` (see
|
||||
// `build_constraint`'s doc comment). `Rc::ptr_eq` against this after
|
||||
// partitioning resolves a row back to the `ConstraintId` the UI
|
||||
// actually names.
|
||||
let mut owner: Vec<(Rc<dyn Constraint>, ConstraintId)> = Vec::new();
|
||||
|
||||
for c in &set.constraints {
|
||||
let Some(constraint) = build_constraint(document, &mut sys, &mut cache, params, c) else { continue };
|
||||
owner.push((constraint.clone(), c.id));
|
||||
sys.add_constraint(constraint);
|
||||
// Coincident needs both X and Y equal; `build_constraint` returns
|
||||
// only the X half (see its doc comment there) since one
|
||||
// `SketchConstraint` maps to one `Rc<dyn Constraint>` everywhere
|
||||
// else — add the Y half here instead of complicating that
|
||||
// one-constraint-per-kind contract for a single kind.
|
||||
if c.kind == ConstraintKind::Coincident {
|
||||
if let [a, b] = c.refs.as_slice() {
|
||||
if let (Some(pa), Some(pb)) = (
|
||||
a.marker.and_then(|m| cache.get(&a.entity).and_then(|g| g.point_for_marker(m))),
|
||||
b.marker.and_then(|m| cache.get(&b.entity).and_then(|g| g.point_for_marker(m))),
|
||||
) {
|
||||
let y_half: Rc<dyn Constraint> = Rc::new(Equal::new(pa.y, pb.y, 1.0));
|
||||
owner.push((y_half.clone(), c.id));
|
||||
sys.add_constraint(y_half);
|
||||
}
|
||||
}
|
||||
for constraint in build_constraint(document, &mut sys, &mut cache, params, c) {
|
||||
owner.push((constraint.clone(), c.id));
|
||||
sys.add_constraint(constraint);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -31,6 +31,7 @@ impl<T> fmt::Display for Labelled<T> {
|
||||
pub fn view_window<'a>(
|
||||
default_save_format: &'a str,
|
||||
file_assoc_enabled: bool,
|
||||
write_dwg_native_constraints: bool,
|
||||
ui_theme: &'a UiThemeConfig,
|
||||
theme_color_inputs: &'a [String; 6],
|
||||
language: crate::i18n::Language,
|
||||
@@ -190,6 +191,24 @@ pub fn view_window<'a>(
|
||||
))
|
||||
.size(11)
|
||||
.width(sizing.width),
|
||||
Space::new().height(14),
|
||||
row![
|
||||
iced::widget::checkbox(write_dwg_native_constraints)
|
||||
.on_toggle(Message::WriteDwgNativeConstraintsChanged)
|
||||
.size(15),
|
||||
text(crate::t!("Write AutoCAD-compatible constraint objects on save")).size(12),
|
||||
]
|
||||
.spacing(8)
|
||||
.align_y(iced::Center),
|
||||
Space::new().height(6),
|
||||
text(crate::t!(
|
||||
"Saves sketch constraints as AutoCAD/BricsCAD's own native objects too, alongside \
|
||||
this app's own format, so other CAD software recognizes them. Off by default — \
|
||||
adds some file size on every save. A drawing that already has these objects keeps \
|
||||
them up to date regardless of this setting."
|
||||
))
|
||||
.size(11)
|
||||
.width(sizing.width),
|
||||
]
|
||||
.spacing(0)
|
||||
.width(sizing.width);
|
||||
|
||||
@@ -354,3 +354,212 @@ fn two_conflicting_distance_targets_are_reported_as_conflicting() {
|
||||
assert!(flagged_id == first || flagged_id == second, "the flagged id should be one of the two conflicting Distance constraints");
|
||||
assert_eq!(kind, RedundancyKind::Conflicting);
|
||||
}
|
||||
|
||||
fn set_circle_center(scene: &mut Scene, handle: Handle, center: Vector3) {
|
||||
if let Some(EntityType::Circle(c)) = scene.document.get_entity_mut(handle) {
|
||||
c.center = center;
|
||||
}
|
||||
}
|
||||
|
||||
fn set_line_start(scene: &mut Scene, handle: Handle, start: Vector3) {
|
||||
if let Some(EntityType::Line(l)) = scene.document.get_entity_mut(handle) {
|
||||
l.start = start;
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn concentric_constraint_pulls_the_second_circles_center_onto_the_firsts() {
|
||||
let mut scene = Scene::new();
|
||||
let a = add_circle(&mut scene, 0.0, 0.0, 3.0);
|
||||
let b = add_circle(&mut scene, 5.0, 5.0, 1.0);
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Concentric,
|
||||
vec![SketchRef::center(a), SketchRef::center(b)],
|
||||
None,
|
||||
);
|
||||
|
||||
set_circle_center(&mut scene, a, Vector3::new(2.0, -3.0, 0.0));
|
||||
scene.bump_entities(&[(a, ChangeKind::Modified)]);
|
||||
|
||||
let (ca, ra) = circle_geom(&scene, a);
|
||||
let (cb, rb) = circle_geom(&scene, b);
|
||||
assert!((ca.x - cb.x).abs() < 1e-6 && (ca.y - cb.y).abs() < 1e-6, "centers should coincide: a={ca:?} b={cb:?}");
|
||||
// Radii are untouched by Concentric — only centers move.
|
||||
assert!((ra - 3.0).abs() < 1e-6 && (rb - 1.0).abs() < 1e-6, "radii must not change: ra={ra} rb={rb}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn center_point_constraint_pulls_a_lines_endpoint_onto_a_circles_center() {
|
||||
let mut scene = Scene::new();
|
||||
let circle = add_circle(&mut scene, 0.0, 0.0, 4.0);
|
||||
let line = add_line(&mut scene, 10.0, 10.0, 20.0, 20.0);
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::CenterPoint,
|
||||
vec![SketchRef::point(line, 0), SketchRef::center(circle)],
|
||||
None,
|
||||
);
|
||||
|
||||
set_circle_center(&mut scene, circle, Vector3::new(-6.0, 9.0, 0.0));
|
||||
scene.bump_entities(&[(circle, ChangeKind::Modified)]);
|
||||
|
||||
let (center, _) = circle_geom(&scene, circle);
|
||||
let (line_start, _) = line_endpoints(&scene, line);
|
||||
assert!(
|
||||
(center.x - line_start.x).abs() < 1e-6 && (center.y - line_start.y).abs() < 1e-6,
|
||||
"the line's start should sit exactly at the circle's center: center={center:?} start={line_start:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn colinear_constraint_pulls_the_second_line_onto_the_firsts_infinite_line() {
|
||||
let mut scene = Scene::new();
|
||||
let a = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
|
||||
let b = add_line(&mut scene, 3.0, 4.0, 7.0, 6.0); // off-axis, not on a's line
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Colinear,
|
||||
vec![SketchRef::whole(a), SketchRef::whole(b)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(b, ChangeKind::Modified)]);
|
||||
|
||||
let (a1, a2) = line_endpoints(&scene, a);
|
||||
let (b1, b2) = line_endpoints(&scene, b);
|
||||
// Both of b's endpoints should land on a's infinite line: the signed
|
||||
// area of (a2-a1) × (bN-a1) is ~0 for a point exactly on that line.
|
||||
let area = |p: Vector3| (a2.x - a1.x) * (p.y - a1.y) - (a2.y - a1.y) * (p.x - a1.x);
|
||||
assert!(area(b1).abs() < 1e-5, "b.start should land on a's line, area={}", area(b1));
|
||||
assert!(area(b2).abs() < 1e-5, "b.end should land on a's line, area={}", area(b2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn midpoint_constraint_pulls_a_point_onto_a_lines_midpoint() {
|
||||
let mut scene = Scene::new();
|
||||
let base = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
|
||||
let marker = add_line(&mut scene, 20.0, 20.0, 21.0, 21.0); // marker.start is the tracked point
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Midpoint,
|
||||
vec![SketchRef::point(marker, 0), SketchRef::whole(base)],
|
||||
None,
|
||||
);
|
||||
|
||||
set_line_end(&mut scene, base, Vector3::new(10.0, 8.0, 0.0));
|
||||
scene.bump_entities(&[(base, ChangeKind::Modified)]);
|
||||
|
||||
let (b1, b2) = line_endpoints(&scene, base);
|
||||
let (marker_start, _) = line_endpoints(&scene, marker);
|
||||
let expected = Vector3::new((b1.x + b2.x) / 2.0, (b1.y + b2.y) / 2.0, 0.0);
|
||||
assert!(
|
||||
(marker_start.x - expected.x).abs() < 1e-6 && (marker_start.y - expected.y).abs() < 1e-6,
|
||||
"marker point should sit at base's midpoint: expected={expected:?} got={marker_start:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_constraint_holds_an_entity_in_place_despite_a_connected_edit() {
|
||||
let mut scene = Scene::new();
|
||||
let fixed_line = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
|
||||
let moving_line = add_line(&mut scene, 10.0, 0.0, 10.0, 10.0);
|
||||
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
set.add(ConstraintKind::Fixed, vec![SketchRef::whole(fixed_line)], None);
|
||||
set.add(ConstraintKind::Coincident, vec![SketchRef::point(fixed_line, 1), SketchRef::point(moving_line, 0)], None);
|
||||
|
||||
let (before_start, before_end) = line_endpoints(&scene, fixed_line);
|
||||
|
||||
// Drag the shared point — without Fixed this would pull fixed_line's
|
||||
// end along with it; Fixed should hold fixed_line exactly in place and
|
||||
// let moving_line's start follow back to it instead.
|
||||
set_line_start(&mut scene, moving_line, Vector3::new(15.0, 5.0, 0.0));
|
||||
scene.bump_entities(&[(moving_line, ChangeKind::Modified)]);
|
||||
|
||||
let (after_start, after_end) = line_endpoints(&scene, fixed_line);
|
||||
assert_eq!(before_start, after_start, "Fixed entity's start must not move");
|
||||
assert_eq!(before_end, after_end, "Fixed entity's end must not move");
|
||||
let (moving_start, _) = line_endpoints(&scene, moving_line);
|
||||
assert!(
|
||||
(moving_start.x - before_end.x).abs() < 1e-6 && (moving_start.y - before_end.y).abs() < 1e-6,
|
||||
"moving_line's start should have been pulled back to fixed_line's (unmoved) end"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_on_curve_constraint_pulls_a_point_onto_a_circles_circumference() {
|
||||
let mut scene = Scene::new();
|
||||
let circle = add_circle(&mut scene, 0.0, 0.0, 5.0);
|
||||
let marker = add_line(&mut scene, 100.0, 100.0, 101.0, 101.0); // marker.start is the tracked point
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::PointOnCurve,
|
||||
vec![SketchRef::point(marker, 0), SketchRef::whole(circle)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(marker, ChangeKind::Modified)]);
|
||||
|
||||
let (center, radius) = circle_geom(&scene, circle);
|
||||
let (marker_start, _) = line_endpoints(&scene, marker);
|
||||
let dist = ((marker_start.x - center.x).powi(2) + (marker_start.y - center.y).powi(2)).sqrt();
|
||||
assert!((dist - radius).abs() < 1e-5, "point should land on the circumference: dist={dist} radius={radius}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_distance_constraint_matches_a_second_point_pairs_separation() {
|
||||
let mut scene = Scene::new();
|
||||
// Reference pair: fixed 6 units apart.
|
||||
let a = add_line(&mut scene, 0.0, 0.0, 6.0, 0.0);
|
||||
// Tracked pair: starts at some other separation, should be pulled to 6.
|
||||
let b = add_line(&mut scene, 20.0, 20.0, 25.0, 20.0);
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::EqualDistance,
|
||||
vec![SketchRef::point(b, 0), SketchRef::point(b, 1), SketchRef::point(a, 0), SketchRef::point(a, 1)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(b, ChangeKind::Modified)]);
|
||||
|
||||
let (a1, a2) = line_endpoints(&scene, a);
|
||||
let (b1, b2) = line_endpoints(&scene, b);
|
||||
let dist_a = ((a2.x - a1.x).powi(2) + (a2.y - a1.y).powi(2)).sqrt();
|
||||
let dist_b = ((b2.x - b1.x).powi(2) + (b2.y - b1.y).powi(2)).sqrt();
|
||||
assert!((dist_a - dist_b).abs() < 1e-6, "the two pairs' separations should match: dist_a={dist_a} dist_b={dist_b}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn symmetric_constraint_mirrors_one_circles_center_across_the_axis_line() {
|
||||
let mut scene = Scene::new();
|
||||
let axis = add_line(&mut scene, 0.0, 0.0, 0.0, 10.0); // the Y axis
|
||||
let a = add_circle(&mut scene, 3.0, 4.0, 1.0);
|
||||
// Starts well off the mirrored position (and not coincident with `a` —
|
||||
// a zero-length a/b segment would leave `Perpendicular`'s precomputed
|
||||
// scale dividing by zero at the very first solve iteration).
|
||||
let b = add_circle(&mut scene, 8.0, 9.0, 1.0);
|
||||
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
// Pin the axis and `a` in place — otherwise they're just as free to move
|
||||
// as `b`'s center, and with only 2 equations (MidpointOnLine +
|
||||
// Perpendicular) against that many more unknowns the solver is free to
|
||||
// converge on any of infinitely many valid configurations, not
|
||||
// necessarily "only b moves, to a's exact mirror" (which is the one
|
||||
// deterministic outcome this test actually wants to check).
|
||||
set.add(ConstraintKind::Fixed, vec![SketchRef::whole(axis)], None);
|
||||
set.add(ConstraintKind::Fixed, vec![SketchRef::whole(a)], None);
|
||||
set.add(ConstraintKind::Symmetric, vec![SketchRef::center(a), SketchRef::center(b), SketchRef::whole(axis)], None);
|
||||
|
||||
scene.bump_entities(&[(b, ChangeKind::Modified)]);
|
||||
|
||||
let (ca, _) = circle_geom(&scene, a);
|
||||
let (cb, _) = circle_geom(&scene, b);
|
||||
assert_eq!(ca, Vector3::new(3.0, 4.0, 0.0), "Fixed a's center must not have moved");
|
||||
// With both a and the axis pinned, b's center has a unique valid
|
||||
// position left: a's exact mirror across the Y axis, (-3, 4).
|
||||
assert!(
|
||||
(cb.x - -3.0).abs() < 1e-5 && (cb.y - 4.0).abs() < 1e-5,
|
||||
"b's center should have been pulled to a's mirror image across the axis: expected (-3, 4), got {cb:?}"
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user