forked from 1b-agent/opencadstudio-upstream
Complete parametric drawing interoperability
This commit is contained in:
1 parent
7aff9fa046
commit
753df67ee4
13 files changed
+1602
-138
No files matched your search
Generated
+3
-3
@@ -83,7 +83,7 @@ checksum = "366ffbaa4442f4684d91e2cd7c5ea7c4ed8add41959a31447066e279e432b618"
|
||||
[[package]]
|
||||
name = "acadrust"
|
||||
version = "0.5.5"
|
||||
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=4d7b359b0571f9b6e0f85a62a21c67508722b83e#4d7b359b0571f9b6e0f85a62a21c67508722b83e"
|
||||
source = "git+https://github.com/HakanSeven12/cadcodec.git?rev=7709884f301cfe292f58a002a514939932be67d0#7709884f301cfe292f58a002a514939932be67d0"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"anyhow",
|
||||
@@ -919,7 +919,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
[[package]]
|
||||
name = "cadkernel"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=50f020377c01fcebb5f31844b9900f046b05192f#50f020377c01fcebb5f31844b9900f046b05192f"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=2db3ac9aa8ce5e75b53f21fefd4b40ca1afaff8b#2db3ac9aa8ce5e75b53f21fefd4b40ca1afaff8b"
|
||||
dependencies = [
|
||||
"acadrust",
|
||||
"cavalier_contours",
|
||||
@@ -930,7 +930,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "cadkernel-constraints"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=50f020377c01fcebb5f31844b9900f046b05192f#50f020377c01fcebb5f31844b9900f046b05192f"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=2db3ac9aa8ce5e75b53f21fefd4b40ca1afaff8b#2db3ac9aa8ce5e75b53f21fefd4b40ca1afaff8b"
|
||||
dependencies = [
|
||||
"nalgebra",
|
||||
]
|
||||
|
||||
+3
-3
@@ -32,9 +32,9 @@ glam = { version = "0.33", features = ["bytemuck"] }
|
||||
rfd = "0.17"
|
||||
clap = { version = "4", features = ["derive", "string"] }
|
||||
env_logger = "0.11"
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4d7b359b0571f9b6e0f85a62a21c67508722b83e", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "50f020377c01fcebb5f31844b9900f046b05192f", features = ["acis", "offset"] }
|
||||
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "50f020377c01fcebb5f31844b9900f046b05192f" }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7709884f301cfe292f58a002a514939932be67d0", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "2db3ac9aa8ce5e75b53f21fefd4b40ca1afaff8b", features = ["acis", "offset"] }
|
||||
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "2db3ac9aa8ce5e75b53f21fefd4b40ca1afaff8b" }
|
||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||
flate2 = "1"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||
|
||||
@@ -14,7 +14,7 @@ serde = { version = "1", features = ["derive"] }
|
||||
# Pulled in only by the `host` feature, which adds the `acadrust`-typed
|
||||
# `HostApi` runtime surface. The default crate stays dependency-free so engine
|
||||
# crates and external tooling can depend on the manifest/ribbon contract cheaply.
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4d7b359b0571f9b6e0f85a62a21c67508722b83e", optional = true, features = ["serde"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7709884f301cfe292f58a002a514939932be67d0", optional = true, features = ["serde"] }
|
||||
|
||||
# Runtime IPC and serialization (host feature only).
|
||||
interprocess = { version = "2", optional = true }
|
||||
@@ -38,7 +38,7 @@ serde_json = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
cargo-lock = "11"
|
||||
# acadrust is scanned at build time to generate the embedded type registry.
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4d7b359b0571f9b6e0f85a62a21c67508722b83e", features = ["serde"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7709884f301cfe292f58a002a514939932be67d0", features = ["serde"] }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = "1"
|
||||
@@ -8,7 +8,7 @@ publish = false
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4d7b359b0571f9b6e0f85a62a21c67508722b83e", features = ["serde"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7709884f301cfe292f58a002a514939932be67d0", features = ["serde"] }
|
||||
bincode = "1.3"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
console_error_panic_hook = "0.1"
|
||||
|
||||
@@ -10,4 +10,4 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
ocs_plugin_api = { path = "../ocs_plugin_api", features = ["host"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4d7b359b0571f9b6e0f85a62a21c67508722b83e", features = ["serde"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7709884f301cfe292f58a002a514939932be67d0", features = ["serde"] }
|
||||
@@ -12,4 +12,4 @@ crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
ocs_plugin_api = { path = "../../crates/ocs_plugin_api", features = ["host"] }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "4d7b359b0571f9b6e0f85a62a21c67508722b83e" }
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "7709884f301cfe292f58a002a514939932be67d0" }
|
||||
@@ -317,8 +317,8 @@ impl OpenCADStudio {
|
||||
let i = self.active_tab;
|
||||
self.tabs[i].scene.clear();
|
||||
self.tabs[i].scene.document = doc;
|
||||
self.tabs[i].scene.load_sketch_constraints_from_document();
|
||||
self.tabs[i].scene.load_named_parameters_from_document();
|
||||
self.tabs[i].scene.load_sketch_constraints_from_document();
|
||||
self.tabs[i].scene.material_base_dir = path_buf.parent().map(PathBuf::from);
|
||||
crate::app::style_ops::ensure_standard_styles(
|
||||
&mut self.tabs[i].scene.document,
|
||||
|
||||
@@ -1446,11 +1446,10 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
|
||||
self.tabs[i].scene.material_base_dir =
|
||||
path.parent().map(std::path::Path::to_path_buf);
|
||||
self.tabs[i].scene.document = doc;
|
||||
// Load persisted constraints after installing the document.
|
||||
self.tabs[i].scene.load_sketch_constraints_from_document();
|
||||
// named_parameters_design.md stage 2: same load-time hook,
|
||||
// for the document-wide parameter table.
|
||||
// Load parameters first so imported dimensional constraints
|
||||
// can resolve their named driving values.
|
||||
self.tabs[i].scene.load_named_parameters_from_document();
|
||||
self.tabs[i].scene.load_sketch_constraints_from_document();
|
||||
self.tabs[i].active_layer = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -27,6 +27,8 @@ pub struct SketchRef {
|
||||
pub marker: Option<i32>,
|
||||
}
|
||||
|
||||
const POLYLINE_SEGMENT_MARKER_BASE: i32 = -1_000_000;
|
||||
|
||||
impl SketchRef {
|
||||
pub fn whole(entity: Handle) -> Self {
|
||||
Self {
|
||||
@@ -51,6 +53,20 @@ impl SketchRef {
|
||||
marker: Some(-3),
|
||||
}
|
||||
}
|
||||
|
||||
/// Select one straight segment of a polyline.
|
||||
pub fn segment(entity: Handle, index: usize) -> Self {
|
||||
Self {
|
||||
entity,
|
||||
marker: Some(POLYLINE_SEGMENT_MARKER_BASE - index as i32),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn segment_index(self) -> Option<usize> {
|
||||
let marker = self.marker?;
|
||||
(marker <= POLYLINE_SEGMENT_MARKER_BASE)
|
||||
.then(|| (POLYLINE_SEGMENT_MARKER_BASE - marker) as usize)
|
||||
}
|
||||
}
|
||||
|
||||
/// The friendly, user-facing constraint types — the "what button did they
|
||||
|
||||
+18
-16
@@ -140,26 +140,28 @@ impl Scene {
|
||||
.map(|record| record.handle)
|
||||
.collect();
|
||||
for owner in owners {
|
||||
let Some(record) = self.document.xrecord(owner, XRECORD_KEY) else {
|
||||
continue;
|
||||
};
|
||||
// Concatenate every Chunk entry in order, not just the first —
|
||||
// a set materialized past `MAX_CHUNK_BYTES` spans more than one
|
||||
// same-code `310` entry (see that constant's doc comment).
|
||||
let mut bytes = Vec::new();
|
||||
for entry in &record.entries {
|
||||
if let acadrust::objects::XRecordValue::Chunk(chunk) = &entry.value {
|
||||
if bytes.len().saturating_add(chunk.len()) > MAX_RECORD_BYTES {
|
||||
bytes.clear();
|
||||
break;
|
||||
if let Some(record) = self.document.xrecord(owner, XRECORD_KEY) {
|
||||
let mut bytes = Vec::new();
|
||||
for entry in &record.entries {
|
||||
if let acadrust::objects::XRecordValue::Chunk(chunk) = &entry.value {
|
||||
if bytes.len().saturating_add(chunk.len()) > MAX_RECORD_BYTES {
|
||||
bytes.clear();
|
||||
break;
|
||||
}
|
||||
bytes.extend_from_slice(chunk);
|
||||
}
|
||||
bytes.extend_from_slice(chunk);
|
||||
}
|
||||
}
|
||||
if bytes.is_empty() {
|
||||
if let Some(set) = decode(&bytes) {
|
||||
self.sketch_constraints.push(set);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if let Some(set) = decode(&bytes) {
|
||||
|
||||
if let Some(set) = super::dwg_native_constraints::native_constraint_set(
|
||||
&self.document,
|
||||
owner,
|
||||
&mut self.named_parameters,
|
||||
) {
|
||||
self.sketch_constraints.push(set);
|
||||
}
|
||||
}
|
||||
|
||||
+385
-54
@@ -5,12 +5,15 @@ use std::collections::HashMap;
|
||||
use std::rc::Rc;
|
||||
|
||||
use acadrust::entities::EntityType;
|
||||
use acadrust::types::Handle;
|
||||
use acadrust::types::{Handle, Vector3};
|
||||
|
||||
use cadkernel_constraints::constraints::angle_distance::L2LAngle;
|
||||
use cadkernel_constraints::constraints::circle_arc::{
|
||||
ArcLength, C2LDistance, P2CDistance, TangentCircumf,
|
||||
};
|
||||
use cadkernel_constraints::constraints::conic::{
|
||||
EqualMajorAxesConic, PointOnEllipse, TangentEllipseLine,
|
||||
};
|
||||
use cadkernel_constraints::constraints::curve_generic::CurveValue;
|
||||
use cadkernel_constraints::constraints::point_line::{
|
||||
CenterOfGravity, Difference, Equal, EqualLineLength, MidpointOnLine, P2PDistance,
|
||||
@@ -18,7 +21,8 @@ use cadkernel_constraints::constraints::point_line::{
|
||||
};
|
||||
use cadkernel_constraints::constraints::Constraint;
|
||||
use cadkernel_constraints::geo::{
|
||||
Arc as GArc, Circle as GCircle, Ellipse as GEllipse, Line as GLine, Point as GPoint,
|
||||
Arc as GArc, Circle as GCircle, Conic as GConic, Ellipse as GEllipse, Line as GLine,
|
||||
Point as GPoint,
|
||||
};
|
||||
use cadkernel_constraints::solvers::dogleg::solve_dl;
|
||||
use cadkernel_constraints::system::System;
|
||||
@@ -31,22 +35,22 @@ use super::{ChangeKind, Scene};
|
||||
|
||||
/// One referenced entity's geometry, registered into an `cadkernel_constraints::System`'s
|
||||
/// parameter store.
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone)]
|
||||
enum EntityGeom {
|
||||
Point(GPoint),
|
||||
Line(GLine),
|
||||
Polyline {
|
||||
points: Rc<Vec<GPoint>>,
|
||||
straight: Rc<Vec<bool>>,
|
||||
closed: bool,
|
||||
},
|
||||
Circle(GCircle),
|
||||
/// Full `geo::Arc` — center/radius (via `.circle`), `start_angle`/
|
||||
/// `end_angle`, and real `start`/`end` points kept consistent with
|
||||
/// those by the arc-rules `CurveValue` constraints `solve_scope` adds
|
||||
/// for every registered arc. See this module's doc comment.
|
||||
Arc(GArc),
|
||||
/// Center-and-shape support only — see `register_entity`'s `Ellipse`
|
||||
/// arm for the acadrust-to-`cadkernel_constraints` parametrization conversion, the
|
||||
/// ellipse-rules `Difference` constraints `solve_scope` adds for every
|
||||
/// registered ellipse to keep `focus1` translating rigidly with
|
||||
/// `center` (this module's doc comment), and that same doc comment for
|
||||
/// what isn't wired up yet (major/minor axis dimensional constraints,
|
||||
/// `PointOnEllipse`-based Coincident/Tangent).
|
||||
/// See `register_entity` for the entity-to-kernel parametrization.
|
||||
Ellipse(GEllipse),
|
||||
}
|
||||
|
||||
@@ -57,8 +61,12 @@ impl EntityGeom {
|
||||
/// read side).
|
||||
fn point_for_marker(&self, marker: i32) -> Option<GPoint> {
|
||||
match (self, marker) {
|
||||
(EntityGeom::Point(point), 0) => Some(*point),
|
||||
(EntityGeom::Line(l), 0) => Some(l.p1),
|
||||
(EntityGeom::Line(l), 1) => Some(l.p2),
|
||||
(EntityGeom::Polyline { points, .. }, marker) if marker >= 0 => {
|
||||
points.get(marker as usize).copied()
|
||||
}
|
||||
(EntityGeom::Circle(c), -3) => Some(c.center),
|
||||
(EntityGeom::Arc(a), 0) => Some(a.start),
|
||||
(EntityGeom::Arc(a), 1) => Some(a.end),
|
||||
@@ -67,6 +75,29 @@ impl EntityGeom {
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn line_segment(&self, index: usize) -> Option<GLine> {
|
||||
let EntityGeom::Polyline {
|
||||
points,
|
||||
straight,
|
||||
closed,
|
||||
} = self
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if !straight.get(index).copied().unwrap_or(false) {
|
||||
return None;
|
||||
}
|
||||
let p1 = *points.get(index)?;
|
||||
let p2 = if index + 1 < points.len() {
|
||||
points[index + 1]
|
||||
} else if *closed {
|
||||
*points.first()?
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
Some(GLine { p1, p2 })
|
||||
}
|
||||
}
|
||||
|
||||
/// `Tangent`/`Normal` only ever care about "a whole circle-shaped curve" or
|
||||
@@ -76,15 +107,22 @@ impl EntityGeom {
|
||||
enum CircleOrLine {
|
||||
Circle(GCircle),
|
||||
Line(GLine),
|
||||
Ellipse(GEllipse),
|
||||
Other,
|
||||
}
|
||||
|
||||
fn as_circle_or_line(g: EntityGeom) -> CircleOrLine {
|
||||
fn as_circle_or_line(g: EntityGeom, reference: SketchRef) -> CircleOrLine {
|
||||
match g {
|
||||
EntityGeom::Circle(c) => CircleOrLine::Circle(c),
|
||||
EntityGeom::Arc(a) => CircleOrLine::Circle(a.circle),
|
||||
EntityGeom::Line(l) => CircleOrLine::Line(l),
|
||||
EntityGeom::Ellipse(_) => CircleOrLine::Other,
|
||||
EntityGeom::Ellipse(ellipse) => CircleOrLine::Ellipse(ellipse),
|
||||
EntityGeom::Point(_) => CircleOrLine::Other,
|
||||
EntityGeom::Polyline { .. } => reference
|
||||
.segment_index()
|
||||
.and_then(|index| g.line_segment(index))
|
||||
.map(CircleOrLine::Line)
|
||||
.unwrap_or(CircleOrLine::Other),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,7 +141,72 @@ fn register_entity(
|
||||
sys: &mut System,
|
||||
handle: Handle,
|
||||
) -> Option<EntityGeom> {
|
||||
match document.get_entity(handle)? {
|
||||
let entity = document.get_entity(handle)?;
|
||||
match entity {
|
||||
EntityType::Point(point) => Some(EntityGeom::Point(GPoint::new(
|
||||
sys.add_param(point.location.x, false),
|
||||
sys.add_param(point.location.y, false),
|
||||
))),
|
||||
EntityType::Insert(insert) => Some(EntityGeom::Point(GPoint::new(
|
||||
sys.add_param(insert.insert_point.x, false),
|
||||
sys.add_param(insert.insert_point.y, false),
|
||||
))),
|
||||
EntityType::Text(text) => Some(EntityGeom::Point(GPoint::new(
|
||||
sys.add_param(text.insertion_point.x, false),
|
||||
sys.add_param(text.insertion_point.y, false),
|
||||
))),
|
||||
EntityType::MText(text) => Some(EntityGeom::Point(GPoint::new(
|
||||
sys.add_param(text.insertion_point.x, false),
|
||||
sys.add_param(text.insertion_point.y, false),
|
||||
))),
|
||||
EntityType::AttributeDefinition(attribute) => Some(EntityGeom::Point(GPoint::new(
|
||||
sys.add_param(attribute.insertion_point.x, false),
|
||||
sys.add_param(attribute.insertion_point.y, false),
|
||||
))),
|
||||
EntityType::AttributeEntity(attribute) => Some(EntityGeom::Point(GPoint::new(
|
||||
sys.add_param(attribute.insertion_point.x, false),
|
||||
sys.add_param(attribute.insertion_point.y, false),
|
||||
))),
|
||||
EntityType::Table(table) => Some(EntityGeom::Point(GPoint::new(
|
||||
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))
|
||||
})
|
||||
.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(),
|
||||
),
|
||||
straight: Rc::new(
|
||||
polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| vertex.bulge.abs() <= 1e-9)
|
||||
.collect(),
|
||||
),
|
||||
closed: polyline.is_closed(),
|
||||
}),
|
||||
EntityType::Line(l) => {
|
||||
let p1 = GPoint::new(
|
||||
sys.add_param(l.start.x, false),
|
||||
@@ -202,11 +305,11 @@ fn resolve_ref(
|
||||
cache: &mut HashMap<Handle, EntityGeom>,
|
||||
r: SketchRef,
|
||||
) -> Option<EntityGeom> {
|
||||
if let Some(&geometry) = cache.get(&r.entity) {
|
||||
return Some(geometry);
|
||||
if let Some(geometry) = cache.get(&r.entity) {
|
||||
return Some(geometry.clone());
|
||||
}
|
||||
let geometry = register_entity(document, sys, r.entity)?;
|
||||
cache.insert(r.entity, geometry);
|
||||
cache.insert(r.entity, geometry.clone());
|
||||
Some(geometry)
|
||||
}
|
||||
|
||||
@@ -221,6 +324,17 @@ fn is_world_z(normal: acadrust::types::Vector3) -> bool {
|
||||
/// Returns the world-XY plane elevation supported by the current 2D bridge.
|
||||
fn entity_plane_z(entity: &EntityType) -> Option<f64> {
|
||||
let z = match entity {
|
||||
EntityType::Point(point) if is_world_z(point.normal) => point.location.z,
|
||||
EntityType::Insert(insert) if is_world_z(insert.normal) => insert.insert_point.z,
|
||||
EntityType::Text(text) if is_world_z(text.normal) => text.insertion_point.z,
|
||||
EntityType::MText(text) if is_world_z(text.normal) => text.insertion_point.z,
|
||||
EntityType::AttributeDefinition(attribute) if is_world_z(attribute.normal) => {
|
||||
attribute.insertion_point.z
|
||||
}
|
||||
EntityType::AttributeEntity(attribute) if is_world_z(attribute.normal) => {
|
||||
attribute.insertion_point.z
|
||||
}
|
||||
EntityType::Table(table) if is_world_z(table.normal) => table.insertion_point.z,
|
||||
EntityType::Line(line)
|
||||
if is_world_z(line.normal) && (line.start.z - line.end.z).abs() <= PLANE_EPS =>
|
||||
{
|
||||
@@ -330,11 +444,13 @@ fn build_constraint(
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
let whole_line = |sys: &mut System, cache: &mut HashMap<_, _>, r: SketchRef| match resolve_ref(
|
||||
document, sys, cache, r,
|
||||
)? {
|
||||
EntityGeom::Line(l) => Some(l),
|
||||
EntityGeom::Circle(_) | EntityGeom::Arc(_) | EntityGeom::Ellipse(_) => None,
|
||||
let whole_line = |sys: &mut System, cache: &mut HashMap<_, _>, r: SketchRef| {
|
||||
let geometry = resolve_ref(document, sys, cache, r)?;
|
||||
match geometry {
|
||||
EntityGeom::Line(line) => Some(line),
|
||||
EntityGeom::Polyline { .. } => geometry.line_segment(r.segment_index()?),
|
||||
_ => None,
|
||||
}
|
||||
};
|
||||
let whole_circle =
|
||||
|sys: &mut System, cache: &mut HashMap<_, _>, r: SketchRef| match resolve_ref(
|
||||
@@ -342,7 +458,10 @@ fn build_constraint(
|
||||
)? {
|
||||
EntityGeom::Circle(circ) => Some(circ),
|
||||
EntityGeom::Arc(a) => Some(a.circle),
|
||||
EntityGeom::Line(_) | EntityGeom::Ellipse(_) => None,
|
||||
EntityGeom::Point(_)
|
||||
| EntityGeom::Line(_)
|
||||
| EntityGeom::Polyline { .. }
|
||||
| EntityGeom::Ellipse(_) => None,
|
||||
};
|
||||
let point_ref = |sys: &mut System, cache: &mut HashMap<_, _>, r: SketchRef| {
|
||||
let marker = r.marker?;
|
||||
@@ -371,24 +490,30 @@ fn build_constraint(
|
||||
ConstraintKind::Coincident | ConstraintKind::Concentric | ConstraintKind::CenterPoint => {
|
||||
point_pair_equal(sys, cache, &c.refs)
|
||||
}
|
||||
ConstraintKind::Horizontal => {
|
||||
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 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::Horizontal => match c.refs.as_slice() {
|
||||
[r] => whole_line(sys, cache, *r)
|
||||
.map(|line| {
|
||||
vec![Rc::new(Equal::new(line.p1.y, line.p2.y, 1.0)) as Rc<dyn Constraint>]
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
[a, b] => match (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) {
|
||||
(Some(a), Some(b)) => vec![Rc::new(Equal::new(a.y, b.y, 1.0))],
|
||||
_ => Vec::new(),
|
||||
},
|
||||
_ => Vec::new(),
|
||||
},
|
||||
ConstraintKind::Vertical => match c.refs.as_slice() {
|
||||
[r] => whole_line(sys, cache, *r)
|
||||
.map(|line| {
|
||||
vec![Rc::new(Equal::new(line.p1.x, line.p2.x, 1.0)) as Rc<dyn Constraint>]
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
[a, b] => match (point_ref(sys, cache, *a), point_ref(sys, cache, *b)) {
|
||||
(Some(a), Some(b)) => vec![Rc::new(Equal::new(a.x, b.x, 1.0))],
|
||||
_ => Vec::new(),
|
||||
},
|
||||
_ => Vec::new(),
|
||||
},
|
||||
ConstraintKind::Parallel => {
|
||||
let [a, b] = c.refs.as_slice() else {
|
||||
return Vec::new();
|
||||
@@ -422,6 +547,15 @@ fn build_constraint(
|
||||
if let (Some(la), Some(lb)) = (whole_line(sys, cache, *a), whole_line(sys, cache, *b)) {
|
||||
return vec![Rc::new(EqualLineLength::new(lb, la))];
|
||||
}
|
||||
if let (Some(EntityGeom::Ellipse(a)), Some(EntityGeom::Ellipse(b))) = (
|
||||
resolve_ref(document, sys, cache, *a),
|
||||
resolve_ref(document, sys, cache, *b),
|
||||
) {
|
||||
return vec![Rc::new(EqualMajorAxesConic::new(
|
||||
GConic::Ellipse(a),
|
||||
GConic::Ellipse(b),
|
||||
))];
|
||||
}
|
||||
let (Some(ca), Some(cb)) = (whole_circle(sys, cache, *a), whole_circle(sys, cache, *b))
|
||||
else {
|
||||
return Vec::new();
|
||||
@@ -507,10 +641,15 @@ fn build_constraint(
|
||||
let zero = sys.add_param(0.0, true);
|
||||
vec![Rc::new(P2CDistance::new(a.circle, p, zero))]
|
||||
}
|
||||
// `PointOnEllipse` (`cadkernel_constraints::constraints::conic`) isn't
|
||||
// wired up yet — deliberately deferred alongside
|
||||
// ellipse-tangency, same as this module's doc comment.
|
||||
EntityGeom::Ellipse(_) => Vec::new(),
|
||||
EntityGeom::Ellipse(ellipse) => {
|
||||
vec![Rc::new(PointOnEllipse::new(p, ellipse))]
|
||||
}
|
||||
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>])
|
||||
.unwrap_or_default(),
|
||||
EntityGeom::Point(_) => Vec::new(),
|
||||
}
|
||||
}
|
||||
ConstraintKind::Symmetric => {
|
||||
@@ -534,10 +673,73 @@ fn build_constraint(
|
||||
let Some(r) = c.refs.first() else {
|
||||
return Vec::new();
|
||||
};
|
||||
if r.segment_index().is_some() {
|
||||
let Some(line) = whole_line(sys, cache, *r) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let (x1, y1, x2, y2) = {
|
||||
let store = sys.store();
|
||||
(
|
||||
store.get(line.p1.x),
|
||||
store.get(line.p1.y),
|
||||
store.get(line.p2.x),
|
||||
store.get(line.p2.y),
|
||||
)
|
||||
};
|
||||
return vec![
|
||||
Rc::new(Equal::new(line.p1.x, sys.add_param(x1, true), 1.0)),
|
||||
Rc::new(Equal::new(line.p1.y, sys.add_param(y1, true), 1.0)),
|
||||
Rc::new(Equal::new(line.p2.x, sys.add_param(x2, true), 1.0)),
|
||||
Rc::new(Equal::new(line.p2.y, sys.add_param(y2, true), 1.0)),
|
||||
];
|
||||
}
|
||||
if r.marker.is_some() {
|
||||
let Some(point) = point_ref(sys, cache, *r) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let (x, y) = {
|
||||
let store = sys.store();
|
||||
(store.get(point.x), store.get(point.y))
|
||||
};
|
||||
return vec![
|
||||
Rc::new(Equal::new(point.x, sys.add_param(x, true), 1.0)),
|
||||
Rc::new(Equal::new(point.y, sys.add_param(y, true), 1.0)),
|
||||
];
|
||||
}
|
||||
let Some(geom) = resolve_ref(document, sys, cache, *r) else {
|
||||
return Vec::new();
|
||||
};
|
||||
match geom {
|
||||
EntityGeom::Point(point) => {
|
||||
let (x, y) = {
|
||||
let store = sys.store();
|
||||
(store.get(point.x), store.get(point.y))
|
||||
};
|
||||
vec![
|
||||
Rc::new(Equal::new(point.x, sys.add_param(x, true), 1.0)),
|
||||
Rc::new(Equal::new(point.y, sys.add_param(y, true), 1.0)),
|
||||
]
|
||||
}
|
||||
EntityGeom::Polyline { points, .. } => {
|
||||
let values: Vec<_> = points
|
||||
.iter()
|
||||
.map(|point| {
|
||||
let store = sys.store();
|
||||
(point, store.get(point.x), store.get(point.y))
|
||||
})
|
||||
.collect();
|
||||
values
|
||||
.into_iter()
|
||||
.flat_map(|(point, x, y)| {
|
||||
[
|
||||
Rc::new(Equal::new(point.x, sys.add_param(x, true), 1.0))
|
||||
as Rc<dyn Constraint>,
|
||||
Rc::new(Equal::new(point.y, sys.add_param(y, true), 1.0))
|
||||
as Rc<dyn Constraint>,
|
||||
]
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
EntityGeom::Line(l) => {
|
||||
let (x1, y1, x2, y2) = {
|
||||
let store = sys.store();
|
||||
@@ -724,7 +926,7 @@ fn build_constraint(
|
||||
// for tangency math — normalize both refs down first so the
|
||||
// match below doesn't need every Circle/Arc combination
|
||||
// written out separately.
|
||||
match (as_circle_or_line(ga), as_circle_or_line(gb)) {
|
||||
match (as_circle_or_line(ga, *a), as_circle_or_line(gb, *b)) {
|
||||
(CircleOrLine::Circle(c1), CircleOrLine::Circle(c2)) => {
|
||||
let store = sys.store();
|
||||
let (x1, y1, r1) = (
|
||||
@@ -765,9 +967,10 @@ fn build_constraint(
|
||||
let zero = sys.add_param(0.0, true);
|
||||
vec![Rc::new(C2LDistance::new(circ, line, zero, ccw, false))]
|
||||
}
|
||||
// Line-Line tangency has no meaning; either side being an
|
||||
// Ellipse falls here too (`PointOnEllipse`/tangency isn't
|
||||
// wired up yet).
|
||||
(CircleOrLine::Ellipse(ellipse), CircleOrLine::Line(line))
|
||||
| (CircleOrLine::Line(line), CircleOrLine::Ellipse(ellipse)) => {
|
||||
vec![Rc::new(TangentEllipseLine::new(line, ellipse))]
|
||||
}
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
@@ -788,7 +991,7 @@ fn build_constraint(
|
||||
) else {
|
||||
return Vec::new();
|
||||
};
|
||||
match (as_circle_or_line(ga), as_circle_or_line(gb)) {
|
||||
match (as_circle_or_line(ga, *a), as_circle_or_line(gb, *b)) {
|
||||
(CircleOrLine::Circle(circ), CircleOrLine::Line(line))
|
||||
| (CircleOrLine::Line(line), CircleOrLine::Circle(circ)) => {
|
||||
vec![Rc::new(PointOnLine::new(circ.center, line))]
|
||||
@@ -1031,7 +1234,9 @@ fn solve_scope(
|
||||
let total_free: usize = cache
|
||||
.values()
|
||||
.map(|g| match g {
|
||||
EntityGeom::Point(_) => 2,
|
||||
EntityGeom::Line(_) => 4,
|
||||
EntityGeom::Polyline { points, .. } => points.len() * 2,
|
||||
EntityGeom::Circle(_) => 3,
|
||||
// Raw param count (center×2, rad, start×2, end×2, both
|
||||
// angles) — same "raw, not netted against its own
|
||||
@@ -1077,6 +1282,98 @@ fn solve_scope(
|
||||
continue;
|
||||
};
|
||||
match (entity, geom) {
|
||||
(EntityType::Point(point), EntityGeom::Point(geometry)) => {
|
||||
let x = store.get(geometry.x);
|
||||
let y = store.get(geometry.y);
|
||||
if (x - point.location.x).abs() > MOVE_EPS
|
||||
|| (y - point.location.y).abs() > MOVE_EPS
|
||||
{
|
||||
let mut updated = point.clone();
|
||||
updated.location.x = x;
|
||||
updated.location.y = y;
|
||||
results.push((handle, EntityType::Point(updated)));
|
||||
}
|
||||
}
|
||||
(EntityType::Insert(insert), EntityGeom::Point(geometry)) => {
|
||||
let mut updated = insert.clone();
|
||||
let next = Vector3::new(
|
||||
store.get(geometry.x),
|
||||
store.get(geometry.y),
|
||||
insert.insert_point.z,
|
||||
);
|
||||
if (next - insert.insert_point).length() > MOVE_EPS {
|
||||
updated.insert_point = next;
|
||||
results.push((handle, EntityType::Insert(updated)));
|
||||
}
|
||||
}
|
||||
(EntityType::Text(text), EntityGeom::Point(geometry)) => {
|
||||
let mut updated = text.clone();
|
||||
let next = Vector3::new(
|
||||
store.get(geometry.x),
|
||||
store.get(geometry.y),
|
||||
text.insertion_point.z,
|
||||
);
|
||||
let delta = next - text.insertion_point;
|
||||
if delta.length() > MOVE_EPS {
|
||||
updated.insertion_point = next;
|
||||
if let Some(alignment) = &mut updated.alignment_point {
|
||||
*alignment = *alignment + delta;
|
||||
}
|
||||
results.push((handle, EntityType::Text(updated)));
|
||||
}
|
||||
}
|
||||
(EntityType::MText(text), EntityGeom::Point(geometry)) => {
|
||||
let mut updated = text.clone();
|
||||
let next = Vector3::new(
|
||||
store.get(geometry.x),
|
||||
store.get(geometry.y),
|
||||
text.insertion_point.z,
|
||||
);
|
||||
if (next - text.insertion_point).length() > MOVE_EPS {
|
||||
updated.insertion_point = next;
|
||||
results.push((handle, EntityType::MText(updated)));
|
||||
}
|
||||
}
|
||||
(EntityType::AttributeDefinition(attribute), EntityGeom::Point(geometry)) => {
|
||||
let mut updated = attribute.clone();
|
||||
let next = Vector3::new(
|
||||
store.get(geometry.x),
|
||||
store.get(geometry.y),
|
||||
attribute.insertion_point.z,
|
||||
);
|
||||
let delta = next - attribute.insertion_point;
|
||||
if delta.length() > MOVE_EPS {
|
||||
updated.insertion_point = next;
|
||||
updated.alignment_point = updated.alignment_point + delta;
|
||||
results.push((handle, EntityType::AttributeDefinition(updated)));
|
||||
}
|
||||
}
|
||||
(EntityType::AttributeEntity(attribute), EntityGeom::Point(geometry)) => {
|
||||
let mut updated = attribute.clone();
|
||||
let next = Vector3::new(
|
||||
store.get(geometry.x),
|
||||
store.get(geometry.y),
|
||||
attribute.insertion_point.z,
|
||||
);
|
||||
let delta = next - attribute.insertion_point;
|
||||
if delta.length() > MOVE_EPS {
|
||||
updated.insertion_point = next;
|
||||
updated.alignment_point = updated.alignment_point + delta;
|
||||
results.push((handle, EntityType::AttributeEntity(updated)));
|
||||
}
|
||||
}
|
||||
(EntityType::Table(table), EntityGeom::Point(geometry)) => {
|
||||
let mut updated = table.clone();
|
||||
let next = Vector3::new(
|
||||
store.get(geometry.x),
|
||||
store.get(geometry.y),
|
||||
table.insertion_point.z,
|
||||
);
|
||||
if (next - table.insertion_point).length() > MOVE_EPS {
|
||||
updated.insertion_point = next;
|
||||
results.push((handle, EntityType::Table(updated)));
|
||||
}
|
||||
}
|
||||
(EntityType::Line(l), EntityGeom::Line(g)) => {
|
||||
let (x1, y1, x2, y2) = (
|
||||
store.get(g.p1.x),
|
||||
@@ -1097,6 +1394,42 @@ fn solve_scope(
|
||||
results.push((handle, EntityType::Line(updated)));
|
||||
}
|
||||
}
|
||||
(EntityType::LwPolyline(polyline), EntityGeom::Polyline { points, .. }) => {
|
||||
let mut updated = polyline.clone();
|
||||
let mut changed = false;
|
||||
for (vertex, point) in updated.vertices.iter_mut().zip(points.iter()) {
|
||||
let x = store.get(point.x);
|
||||
let y = store.get(point.y);
|
||||
if (x - vertex.location.x).abs() > MOVE_EPS
|
||||
|| (y - vertex.location.y).abs() > MOVE_EPS
|
||||
{
|
||||
vertex.location.x = x;
|
||||
vertex.location.y = y;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
results.push((handle, EntityType::LwPolyline(updated)));
|
||||
}
|
||||
}
|
||||
(EntityType::Polyline2D(polyline), EntityGeom::Polyline { points, .. }) => {
|
||||
let mut updated = polyline.clone();
|
||||
let mut changed = false;
|
||||
for (vertex, point) in updated.vertices.iter_mut().zip(points.iter()) {
|
||||
let x = store.get(point.x);
|
||||
let y = store.get(point.y);
|
||||
if (x - vertex.location.x).abs() > MOVE_EPS
|
||||
|| (y - vertex.location.y).abs() > MOVE_EPS
|
||||
{
|
||||
vertex.location.x = x;
|
||||
vertex.location.y = y;
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
results.push((handle, EntityType::Polyline2D(updated)));
|
||||
}
|
||||
}
|
||||
(EntityType::Circle(c), EntityGeom::Circle(g)) => {
|
||||
let (cx, cy, r) = (
|
||||
store.get(g.center.x),
|
||||
@@ -1671,11 +2004,9 @@ mod tests {
|
||||
panic!("expected spline");
|
||||
};
|
||||
let curve = crate::entities::spline::nurbs3(spline).unwrap();
|
||||
let jet = cadkernel::space::CurveJet::from_nurbs(
|
||||
&curve,
|
||||
cadkernel::space::SplineEnd::Start,
|
||||
)
|
||||
.unwrap();
|
||||
let jet =
|
||||
cadkernel::space::CurveJet::from_nurbs(&curve, cadkernel::space::SplineEnd::Start)
|
||||
.unwrap();
|
||||
assert!((jet.point[0] - 0.0).abs() < 1e-9);
|
||||
assert!((jet.point[1] - 1.0).abs() < 1e-9);
|
||||
assert!(jet.tangent[1].abs() < 1e-9, "jet={jet:?}");
|
||||
|
||||
@@ -127,6 +127,77 @@ fn horizontal_constraint_levels_the_line_when_an_endpoint_moves() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_constraint_levels_a_polyline_segment() {
|
||||
let mut scene = Scene::new();
|
||||
let polyline = scene.add_entity(EntityType::LwPolyline(
|
||||
acadrust::entities::LwPolyline::from_points(vec![
|
||||
acadrust::types::Vector2::new(0.0, 0.0),
|
||||
acadrust::types::Vector2::new(5.0, 2.0),
|
||||
acadrust::types::Vector2::new(10.0, 7.0),
|
||||
]),
|
||||
));
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Horizontal,
|
||||
vec![SketchRef::segment(polyline, 1)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(polyline, ChangeKind::Modified)]);
|
||||
let EntityType::LwPolyline(polyline) = scene.document.get_entity(polyline).unwrap() else {
|
||||
panic!("expected a lightweight polyline");
|
||||
};
|
||||
assert!(
|
||||
(polyline.vertices[1].location.y - polyline.vertices[2].location.y).abs() < 1e-6,
|
||||
"the selected segment should be horizontal"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_constraint_does_not_flatten_a_polyline_arc_segment() {
|
||||
let mut source = acadrust::entities::LwPolyline::new();
|
||||
source.add_point_with_bulge(acadrust::types::Vector2::new(0.0, 0.0), 0.5);
|
||||
source.add_point(acadrust::types::Vector2::new(5.0, 2.0));
|
||||
let mut scene = Scene::new();
|
||||
let polyline = scene.add_entity(EntityType::LwPolyline(source));
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Horizontal,
|
||||
vec![SketchRef::segment(polyline, 0)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(polyline, ChangeKind::Modified)]);
|
||||
let EntityType::LwPolyline(polyline) = scene.document.get_entity(polyline).unwrap() else {
|
||||
panic!("expected a lightweight polyline");
|
||||
};
|
||||
assert_eq!(polyline.vertices[0].location.y, 0.0);
|
||||
assert_eq!(polyline.vertices[1].location.y, 2.0);
|
||||
assert_eq!(polyline.vertices[0].bulge, 0.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_constraint_aligns_two_selected_points() {
|
||||
let mut scene = Scene::new();
|
||||
let a = add_line(&mut scene, 0.0, 1.0, 4.0, 3.0);
|
||||
let b = add_line(&mut scene, 8.0, 7.0, 12.0, 9.0);
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Horizontal,
|
||||
vec![SketchRef::point(a, 0), SketchRef::point(b, 0)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(b, ChangeKind::Modified)]);
|
||||
let (a_start, _) = line_endpoints(&scene, a);
|
||||
let (b_start, _) = line_endpoints(&scene, b);
|
||||
assert!((a_start.y - b_start.y).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parallel_constraint_rotates_the_other_line_when_one_moves() {
|
||||
let mut scene = Scene::new();
|
||||
@@ -655,6 +726,55 @@ fn midpoint_constraint_pulls_a_point_onto_a_lines_midpoint() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn midpoint_constraint_supports_a_point_entity() {
|
||||
let mut scene = Scene::new();
|
||||
let base = add_line(&mut scene, 0.0, 0.0, 10.0, 6.0);
|
||||
let point = scene.add_entity(EntityType::Point(acadrust::entities::Point::at(
|
||||
Vector3::new(20.0, 20.0, 0.0),
|
||||
)));
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Midpoint,
|
||||
vec![SketchRef::point(point, 0), SketchRef::whole(base)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(base, ChangeKind::Modified)]);
|
||||
let (start, end) = line_endpoints(&scene, base);
|
||||
let EntityType::Point(point) = scene.document.get_entity(point).unwrap() else {
|
||||
panic!("expected a point");
|
||||
};
|
||||
assert!((point.location.x - (start.x + end.x) / 2.0).abs() < 1e-6);
|
||||
assert!((point.location.y - (start.y + end.y) / 2.0).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coincident_constraint_supports_a_block_insertion_point() {
|
||||
let mut scene = Scene::new();
|
||||
let line = add_line(&mut scene, 0.0, 0.0, 10.0, 4.0);
|
||||
let insert = scene.add_entity(EntityType::Insert(acadrust::entities::Insert::new(
|
||||
"fixture",
|
||||
Vector3::new(30.0, 20.0, 0.0),
|
||||
)));
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Coincident,
|
||||
vec![SketchRef::point(line, 1), SketchRef::point(insert, 0)],
|
||||
None,
|
||||
);
|
||||
|
||||
scene.bump_entities(&[(line, ChangeKind::Modified)]);
|
||||
let (_, end) = line_endpoints(&scene, line);
|
||||
let EntityType::Insert(insert) = scene.document.get_entity(insert).unwrap() else {
|
||||
panic!("expected a block reference");
|
||||
};
|
||||
assert!((insert.insert_point.x - end.x).abs() < 1e-6);
|
||||
assert!((insert.insert_point.y - end.y).abs() < 1e-6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fixed_constraint_holds_an_entity_in_place_despite_a_connected_edit() {
|
||||
let mut scene = Scene::new();
|
||||
@@ -723,6 +843,92 @@ fn point_on_curve_constraint_pulls_a_point_onto_a_circles_circumference() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn point_on_curve_constraint_pulls_a_point_onto_an_ellipse() {
|
||||
let mut scene = Scene::new();
|
||||
let ellipse = add_ellipse(&mut scene, 0.0, 0.0, (5.0, 0.0), 0.6);
|
||||
let marker = add_line(&mut scene, 12.0, 8.0, 13.0, 8.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::PointOnCurve,
|
||||
vec![SketchRef::point(marker, 0), SketchRef::whole(ellipse)],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[(marker, ChangeKind::Modified)]);
|
||||
|
||||
let (center, major, ratio) = ellipse_geom(&scene, ellipse);
|
||||
let (point, _) = line_endpoints(&scene, marker);
|
||||
let a = major.length();
|
||||
let b = a * ratio;
|
||||
let u = major / a;
|
||||
let v = Vector3::new(-u.y, u.x, 0.0);
|
||||
let offset = point - center;
|
||||
let equation = (offset.dot(&u) / a).powi(2) + (offset.dot(&v) / b).powi(2);
|
||||
assert!(
|
||||
(equation - 1.0).abs() < 1e-5,
|
||||
"point should satisfy the solved ellipse equation: {equation}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tangent_constraint_solves_an_ellipse_and_line() {
|
||||
let mut scene = Scene::new();
|
||||
let ellipse = add_ellipse(&mut scene, 0.0, 0.0, (5.0, 0.0), 0.6);
|
||||
let line = add_line(&mut scene, -8.0, 8.0, 8.0, 8.0);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Tangent,
|
||||
vec![SketchRef::whole(ellipse), SketchRef::whole(line)],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[(line, ChangeKind::Modified)]);
|
||||
|
||||
let (center, major, ratio) = ellipse_geom(&scene, ellipse);
|
||||
let (start, end) = line_endpoints(&scene, line);
|
||||
let direction = (end - start).normalize();
|
||||
let normal = Vector3::new(-direction.y, direction.x, 0.0);
|
||||
let unit_major = major.normalize();
|
||||
let unit_minor = Vector3::new(-unit_major.y, unit_major.x, 0.0);
|
||||
let a = major.length();
|
||||
let b = a * ratio;
|
||||
let support =
|
||||
((a * normal.dot(&unit_major)).powi(2) + (b * normal.dot(&unit_minor)).powi(2)).sqrt();
|
||||
let distance = (center - start).dot(&normal).abs();
|
||||
assert!(
|
||||
(distance - support).abs() < 1e-5,
|
||||
"line should be tangent to the solved ellipse: distance={distance} support={support}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_constraint_matches_ellipse_major_axes() {
|
||||
let mut scene = Scene::new();
|
||||
let a = add_ellipse(&mut scene, 0.0, 0.0, (5.0, 0.0), 0.6);
|
||||
let b = add_ellipse(&mut scene, 20.0, 0.0, (2.0, 0.0), 0.5);
|
||||
|
||||
scene
|
||||
.sketch_constraint_set_mut(SketchScope::ModelSpace)
|
||||
.add(
|
||||
ConstraintKind::Equal,
|
||||
vec![SketchRef::whole(a), SketchRef::whole(b)],
|
||||
None,
|
||||
);
|
||||
scene.bump_entities(&[(b, ChangeKind::Modified)]);
|
||||
|
||||
let (_, major_a, _) = ellipse_geom(&scene, a);
|
||||
let (_, major_b, _) = ellipse_geom(&scene, b);
|
||||
assert!(
|
||||
(major_a.length() - major_b.length()).abs() < 1e-5,
|
||||
"ellipse major radii should match: {} vs {}",
|
||||
major_a.length(),
|
||||
major_b.length()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn equal_distance_constraint_matches_a_second_point_pairs_separation() {
|
||||
let mut scene = Scene::new();
|
||||
|
||||
Reference in new issue
Block a user