Complete parametric drawing interoperability

This commit is contained in:
Hakan Seven committed 2026-09-14 23:51:37 +03:00
1 parent 7aff9fa046
commit 753df67ee4
13 files changed
+1602 -138

No files matched your search

Generated
+3 -3
View File
@@ -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
View File
@@ -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"] }
+2 -2
View File
@@ -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"
+1 -1
View File
@@ -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"
+1 -1
View File
@@ -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"] }
+1 -1
View File
@@ -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" }
+1 -1
View File
@@ -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,
+3 -4
View File
@@ -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
+16
View File
@@ -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
View File
@@ -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
View File
@@ -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:?}");
+206
View File
@@ -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();