forked from 1b-agent/opencadstudio-upstream
Add persistent sketch-constraint system (ocs_gcs port) and named-parameters handoff docs
Ports FreeCAD's planegcs 2D geometric constraint solver to Rust as a new crates/ocs_gcs workspace member, and wires it into the document/scene model as a persistent SketchConstraintSet: Coincident, Horizontal/Vertical, Parallel/Perpendicular, Equal, Tangent, Distance, Angle and Radius constraints that re-solve live (including mid-drag, not just on release), render glyphs and a DOF badge, flag conflicts with a one-click resolver pill, and round-trip through save/load via an XRecord blob. Also includes docs/command_reference.md (a statically generated reference of every registered command and its prompt sequence) and docs/named_parameters_design.md + docs/backlog.md, which hand off the next queued feature (a named-parameter/expression table) and consolidate every deliberate scope-down and open gap found along the way, so a fresh session has a complete starting point. Co-Authored-By: Claude Sonnet 5 <[email protected]>
This commit is contained in:
1 parent
a39c7b8f71
commit
63a74b19a9
63 files changed
+12193
-49
No files matched your search
@@ -0,0 +1,298 @@
|
||||
// Stage 3 of docs/parametric_system_design.md's staged plan:
|
||||
// `refresh_sketch_constraints` wired into `Scene::bump_entities` (§4.1),
|
||||
// exercised against an in-memory-only `SketchConstraintSet` (no save/load —
|
||||
// that's stage 4). Verifies: an edit to one constrained entity moves its
|
||||
// constrained neighbors, produces the expected change list, and an
|
||||
// unrelated edit doesn't touch anything (§4.3's "what does NOT trigger").
|
||||
|
||||
use OpenCADStudio::scene::sketch_constraints::{ConstraintKind, SketchRef, SketchScope};
|
||||
use OpenCADStudio::scene::{ChangeKind, Scene};
|
||||
use acadrust::entities::EntityType;
|
||||
use acadrust::types::{Handle, Vector3};
|
||||
|
||||
fn add_line(scene: &mut Scene, x1: f64, y1: f64, x2: f64, y2: f64) -> Handle {
|
||||
scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
|
||||
Vector3::new(x1, y1, 0.0),
|
||||
Vector3::new(x2, y2, 0.0),
|
||||
)))
|
||||
}
|
||||
|
||||
fn line_endpoints(scene: &Scene, handle: Handle) -> (Vector3, Vector3) {
|
||||
match scene.document.get_entity(handle).expect("entity exists") {
|
||||
EntityType::Line(l) => (l.start, l.end),
|
||||
other => panic!("expected a Line, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_line_end(scene: &mut Scene, handle: Handle, end: Vector3) {
|
||||
if let Some(EntityType::Line(l)) = scene.document.get_entity_mut(handle) {
|
||||
l.end = end;
|
||||
}
|
||||
}
|
||||
|
||||
fn add_circle(scene: &mut Scene, cx: f64, cy: f64, radius: f64) -> Handle {
|
||||
scene.add_entity(EntityType::Circle(acadrust::entities::Circle::from_center_radius(Vector3::new(cx, cy, 0.0), radius)))
|
||||
}
|
||||
|
||||
fn circle_geom(scene: &Scene, handle: Handle) -> (Vector3, f64) {
|
||||
match scene.document.get_entity(handle).expect("entity exists") {
|
||||
EntityType::Circle(c) => (c.center, c.radius),
|
||||
other => panic!("expected a Circle, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn horizontal_constraint_levels_the_line_when_an_endpoint_moves() {
|
||||
let mut scene = Scene::new();
|
||||
let line = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(ConstraintKind::Horizontal, vec![SketchRef::whole(line)], None);
|
||||
|
||||
// Drag the endpoint off-axis — a real edit, the kind of thing a Move
|
||||
// command or grip-drag commit would do.
|
||||
set_line_end(&mut scene, line, Vector3::new(10.0, 4.0, 0.0));
|
||||
scene.bump_entities(&[(line, ChangeKind::Modified)]);
|
||||
|
||||
let (start, end) = line_endpoints(&scene, line);
|
||||
assert!((start.y - end.y).abs() < 1e-6, "line should have been re-leveled: start={start:?} end={end:?}");
|
||||
|
||||
// A line is 4 raw coordinates; Horizontal removes exactly 1 (y1 == y2),
|
||||
// leaving 3 — not 1: the two X coordinates are registered but never
|
||||
// referenced by any constraint, so they must still count as free
|
||||
// (regression guard for the undercounting `solve_scope` bug found while
|
||||
// live-testing this stage: `System::partition`'s subsystems only see
|
||||
// constraint-referenced params, so an untouched one was invisible to
|
||||
// `diagnose` entirely before this was fixed).
|
||||
let dof = scene.sketch_constraint_set(SketchScope::ModelSpace).and_then(|s| s.dof);
|
||||
assert_eq!(dof, Some(3), "DOF should count the two untouched X coordinates too");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parallel_constraint_rotates_the_other_line_when_one_moves() {
|
||||
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, 0.0, 5.0, 10.0, 5.0);
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(ConstraintKind::Parallel, vec![SketchRef::whole(a), SketchRef::whole(b)], None);
|
||||
|
||||
// Rotate `a`; `b` (parallel to it) should follow.
|
||||
set_line_end(&mut scene, a, Vector3::new(10.0, 6.0, 0.0));
|
||||
scene.bump_entities(&[(a, ChangeKind::Modified)]);
|
||||
|
||||
let (a1, a2) = line_endpoints(&scene, a);
|
||||
let (b1, b2) = line_endpoints(&scene, b);
|
||||
let dir_a = ((a2.x - a1.x), (a2.y - a1.y));
|
||||
let dir_b = ((b2.x - b1.x), (b2.y - b1.y));
|
||||
// Parallel: cross product of directions is ~0.
|
||||
let cross = dir_a.0 * dir_b.1 - dir_a.1 * dir_b.0;
|
||||
assert!(cross.abs() < 1e-6, "lines should be parallel after the solve: dir_a={dir_a:?} dir_b={dir_b:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn distance_constraint_holds_the_target_length_after_an_unrelated_endpoint_edit() {
|
||||
let mut scene = Scene::new();
|
||||
let line = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Distance,
|
||||
vec![SketchRef::point(line, 0), SketchRef::point(line, 1)],
|
||||
Some(20.0),
|
||||
);
|
||||
|
||||
set_line_end(&mut scene, line, Vector3::new(5.0, 5.0, 0.0)); // arbitrary edit, not length=20
|
||||
scene.bump_entities(&[(line, ChangeKind::Modified)]);
|
||||
|
||||
let (start, end) = line_endpoints(&scene, line);
|
||||
let len = ((end.x - start.x).powi(2) + (end.y - start.y).powi(2)).sqrt();
|
||||
assert!((len - 20.0).abs() < 1e-6, "length should have been solved to the driving value, got {len}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tangent_constraint_between_a_line_and_a_circle_solves_to_touching() {
|
||||
let mut scene = Scene::new();
|
||||
let circle = add_circle(&mut scene, 0.0, 0.0, 5.0);
|
||||
// A horizontal line well clear of the circle (distance 10, radius 5) —
|
||||
// deliberately not tangent yet.
|
||||
let line = add_line(&mut scene, -10.0, 10.0, 10.0, 10.0);
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(ConstraintKind::Tangent, vec![SketchRef::whole(circle), SketchRef::whole(line)], None);
|
||||
scene.bump_entities(&[(circle, ChangeKind::Modified), (line, ChangeKind::Modified)]);
|
||||
|
||||
let (center, radius) = circle_geom(&scene, circle);
|
||||
let (p1, p2) = line_endpoints(&scene, line);
|
||||
let line_dir = (p2.x - p1.x, p2.y - p1.y);
|
||||
let line_len = (line_dir.0 * line_dir.0 + line_dir.1 * line_dir.1).sqrt();
|
||||
// Signed distance from the circle's center to the (infinite) line.
|
||||
let signed_dist = (line_dir.0 * (center.y - p1.y) - line_dir.1 * (center.x - p1.x)) / line_len;
|
||||
assert!((signed_dist.abs() - radius).abs() < 1e-6, "center-to-line distance should equal the radius after solving: dist={signed_dist} radius={radius}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tangent_constraint_between_two_circles_solves_to_external_tangency() {
|
||||
let mut scene = Scene::new();
|
||||
let a = add_circle(&mut scene, 0.0, 0.0, 3.0);
|
||||
let b = add_circle(&mut scene, 20.0, 0.0, 2.0); // far apart: distance 20, r1+r2 = 5
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(ConstraintKind::Tangent, vec![SketchRef::whole(a), SketchRef::whole(b)], None);
|
||||
scene.bump_entities(&[(a, ChangeKind::Modified), (b, ChangeKind::Modified)]);
|
||||
|
||||
let (ca, ra) = circle_geom(&scene, a);
|
||||
let (cb, rb) = circle_geom(&scene, b);
|
||||
let center_dist = ((ca.x - cb.x).powi(2) + (ca.y - cb.y).powi(2)).sqrt();
|
||||
assert!((center_dist - (ra + rb)).abs() < 1e-6, "circles should sit exactly (r1+r2) apart after solving: dist={center_dist} r1+r2={}", ra + rb);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn coincident_constraint_pulls_the_second_point_onto_the_first_when_it_moves() {
|
||||
let mut scene = Scene::new();
|
||||
let a = add_line(&mut scene, 0.0, 0.0, 5.0, 0.0);
|
||||
let b = add_line(&mut scene, 5.0, 0.0, 5.0, 5.0); // b.start coincident with a.end
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(
|
||||
ConstraintKind::Coincident,
|
||||
vec![SketchRef::point(a, 1), SketchRef::point(b, 0)],
|
||||
None,
|
||||
);
|
||||
|
||||
// Move a's end away — b's start should follow to stay coincident.
|
||||
set_line_end(&mut scene, a, Vector3::new(8.0, 3.0, 0.0));
|
||||
scene.bump_entities(&[(a, ChangeKind::Modified)]);
|
||||
|
||||
let (_, a_end) = line_endpoints(&scene, a);
|
||||
let (b_start, _) = line_endpoints(&scene, b);
|
||||
assert!((a_end.x - b_start.x).abs() < 1e-6 && (a_end.y - b_start.y).abs() < 1e-6, "a.end={a_end:?} b.start={b_start:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unrelated_entity_edit_does_not_touch_constrained_geometry() {
|
||||
let mut scene = Scene::new();
|
||||
let line = add_line(&mut scene, 0.0, 0.0, 10.0, 3.0); // deliberately not horizontal
|
||||
let unrelated = add_line(&mut scene, 100.0, 100.0, 200.0, 100.0);
|
||||
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(ConstraintKind::Horizontal, vec![SketchRef::whole(line)], None);
|
||||
|
||||
let (start_before, end_before) = line_endpoints(&scene, line);
|
||||
set_line_end(&mut scene, unrelated, Vector3::new(200.0, 150.0, 0.0));
|
||||
scene.bump_entities(&[(unrelated, ChangeKind::Modified)]);
|
||||
|
||||
let (start_after, end_after) = line_endpoints(&scene, line);
|
||||
assert_eq!(start_before, start_after, "unrelated edit must not trigger a re-solve of unrelated geometry");
|
||||
assert_eq!(end_before, end_after);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn erasing_a_constrained_entity_drops_its_constraints_instead_of_dangling() {
|
||||
// Design doc §5.3/§12 (open question 4): deletion policy is "silently
|
||||
// drop", matching FreeCAD, rather than leaving a `SketchRef` pointing
|
||||
// at a handle that no longer resolves.
|
||||
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, 0.0, 5.0, 10.0, 5.0);
|
||||
let c = add_line(&mut scene, 20.0, 20.0, 30.0, 20.0); // unrelated, own constraint
|
||||
|
||||
let parallel_id =
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(ConstraintKind::Parallel, vec![SketchRef::whole(a), SketchRef::whole(b)], None);
|
||||
let horizontal_id = scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(ConstraintKind::Horizontal, vec![SketchRef::whole(c)], None);
|
||||
|
||||
scene.erase_entities(&[a]);
|
||||
|
||||
let set = scene.sketch_constraint_set(SketchScope::ModelSpace).expect("scope still exists");
|
||||
assert!(set.get(parallel_id).is_none(), "the Parallel constraint referencing the erased line must be gone");
|
||||
assert!(set.get(horizontal_id).is_some(), "an unrelated constraint on a different entity must survive");
|
||||
|
||||
// And the surviving constraint set still functions normally afterward.
|
||||
set_line_end(&mut scene, c, Vector3::new(30.0, 25.0, 0.0));
|
||||
scene.bump_entities(&[(c, ChangeKind::Modified)]);
|
||||
let (start, end) = line_endpoints(&scene, c);
|
||||
assert!((start.y - end.y).abs() < 1e-6, "remaining Horizontal constraint should still solve: start={start:?} end={end:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn copying_two_constrained_entities_carries_their_constraint_along() {
|
||||
// Design doc §5.3/§12 (open question 3, resolved): a constraint entirely
|
||||
// between the copied entities should follow the copy.
|
||||
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, 0.0, 5.0, 10.0, 3.0); // deliberately not parallel yet
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(ConstraintKind::Parallel, vec![SketchRef::whole(a), SketchRef::whole(b)], None);
|
||||
// Solve once so the source pair is actually parallel before copying.
|
||||
scene.bump_entities(&[(a, ChangeKind::Modified), (b, ChangeKind::Modified)]);
|
||||
let constraints_before = scene.sketch_constraint_set(SketchScope::ModelSpace).unwrap().constraints.len();
|
||||
|
||||
let transform = OpenCADStudio::command::EntityTransform::Translate(glam::DVec3::new(100.0, 100.0, 0.0));
|
||||
let new_handles = scene.copy_entities(&[a, b], &transform);
|
||||
assert_eq!(new_handles.len(), 2);
|
||||
let (new_a, new_b) = (new_handles[0], new_handles[1]);
|
||||
|
||||
let constraints_after = scene.sketch_constraint_set(SketchScope::ModelSpace).unwrap().constraints.len();
|
||||
assert_eq!(constraints_after, constraints_before + 1, "the copy should have gained exactly one new Parallel constraint");
|
||||
|
||||
// Break the copy's parallelism, then confirm its own (copied) constraint
|
||||
// re-solves it — proof this is a real, independent constraint on the
|
||||
// copy, not just coincidentally-parallel geometry from the translate.
|
||||
set_line_end(&mut scene, new_b, Vector3::new(110.0, 108.0, 0.0));
|
||||
scene.bump_entities(&[(new_b, ChangeKind::Modified)]);
|
||||
let (a1, a2) = line_endpoints(&scene, new_a);
|
||||
let (b1, b2) = line_endpoints(&scene, new_b);
|
||||
let dir_a = (a2.x - a1.x, a2.y - a1.y);
|
||||
let dir_b = (b2.x - b1.x, b2.y - b1.y);
|
||||
let cross = dir_a.0 * dir_b.1 - dir_a.1 * dir_b.0;
|
||||
assert!(cross.abs() < 1e-6, "the copied pair should still be held parallel by its own constraint: dir_a={dir_a:?} dir_b={dir_b:?}");
|
||||
|
||||
// The original pair's own constraint must be untouched (still its own
|
||||
// record, independently referencing the original handles).
|
||||
let (oa1, oa2) = line_endpoints(&scene, a);
|
||||
let (ob1, ob2) = line_endpoints(&scene, b);
|
||||
let odir_a = (oa2.x - oa1.x, oa2.y - oa1.y);
|
||||
let odir_b = (ob2.x - ob1.x, ob2.y - ob1.y);
|
||||
let ocross = odir_a.0 * odir_b.1 - odir_a.1 * odir_b.0;
|
||||
assert!(ocross.abs() < 1e-6, "the original pair should remain parallel too");
|
||||
}
|
||||
|
||||
// The "one edit that ripples through a constraint still records as one undo
|
||||
// step" case needs `Scene::record_undo_before`, which is `pub(crate)` (an
|
||||
// integration test crate can't reach it) — covered instead as an internal
|
||||
// unit test in `src/scene/sketch_solve.rs`.
|
||||
|
||||
#[test]
|
||||
fn a_duplicated_horizontal_constraint_is_reported_as_redundant() {
|
||||
// Design doc §6.4 (stage 11): `solve_scope` classifies the redundant row
|
||||
// `ocs_gcs::diagnosis::diagnose` flags and resolves it back to the
|
||||
// `ConstraintId` a `ConflictResolverPanel` would name.
|
||||
use OpenCADStudio::scene::sketch_constraints::ConstraintKind;
|
||||
use ocs_gcs::diagnosis::RedundancyKind;
|
||||
|
||||
let mut scene = Scene::new();
|
||||
let a = add_line(&mut scene, 0.0, 0.0, 10.0, 3.0);
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
let first = set.add(ConstraintKind::Horizontal, vec![SketchRef::whole(a)], None);
|
||||
let second = set.add(ConstraintKind::Horizontal, vec![SketchRef::whole(a)], None); // exact duplicate
|
||||
|
||||
scene.bump_entities(&[(a, ChangeKind::Modified)]);
|
||||
|
||||
let set = scene.sketch_constraint_set(SketchScope::ModelSpace).unwrap();
|
||||
assert_eq!(set.conflicts.len(), 1, "exactly one of the two identical constraints should be flagged");
|
||||
let (flagged_id, kind) = set.conflicts[0];
|
||||
assert!(flagged_id == first || flagged_id == second, "the flagged id should be one of the two duplicates");
|
||||
assert_eq!(kind, RedundancyKind::Redundant);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn two_conflicting_distance_targets_are_reported_as_conflicting() {
|
||||
use OpenCADStudio::scene::sketch_constraints::ConstraintKind;
|
||||
use ocs_gcs::diagnosis::RedundancyKind;
|
||||
|
||||
let mut scene = Scene::new();
|
||||
let a = add_line(&mut scene, 0.0, 0.0, 10.0, 0.0);
|
||||
let set = scene.sketch_constraint_set_mut(SketchScope::ModelSpace);
|
||||
let first = set.add(ConstraintKind::Distance, vec![SketchRef::point(a, 0), SketchRef::point(a, 1)], Some(20.0));
|
||||
let second = set.add(ConstraintKind::Distance, vec![SketchRef::point(a, 0), SketchRef::point(a, 1)], Some(50.0));
|
||||
|
||||
scene.bump_entities(&[(a, ChangeKind::Modified)]);
|
||||
|
||||
let set = scene.sketch_constraint_set(SketchScope::ModelSpace).unwrap();
|
||||
assert_eq!(set.conflicts.len(), 1);
|
||||
let (flagged_id, kind) = set.conflicts[0];
|
||||
assert!(flagged_id == first || flagged_id == second, "the flagged id should be one of the two conflicting Distance constraints");
|
||||
assert_eq!(kind, RedundancyKind::Conflicting);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
// Stage 2 of docs/parametric_system_design.md's staged plan: prove a
|
||||
// SketchConstraintSet, serialized into an XRecord Chunk entry hung off a
|
||||
// block record's extension dictionary, survives a real save/reload through
|
||||
// both supported file formats byte-for-byte. Everything after this stage
|
||||
// (save/load integration, the "lazy" persistence model) is gated on this
|
||||
// test passing — see the design doc §8 stage 2 and §7 open question 1.
|
||||
|
||||
use OpenCADStudio::scene::sketch_constraints::{ConstraintKind, SketchConstraintSet, SketchRef, SketchScope};
|
||||
use OpenCADStudio::scene::Scene;
|
||||
use acadrust::entities::EntityType;
|
||||
use acadrust::objects::{XRecordEntry, XRecordValue};
|
||||
use acadrust::types::{Handle, Vector3};
|
||||
|
||||
const RECORD_KEY: &str = "OCS_SKETCH_CONSTRAINTS_TEST";
|
||||
|
||||
fn sample_set(scope: SketchScope) -> SketchConstraintSet {
|
||||
let mut set = SketchConstraintSet::new(scope);
|
||||
set.add(
|
||||
ConstraintKind::Coincident,
|
||||
vec![SketchRef::point(Handle::new(101), 0), SketchRef::point(Handle::new(102), 1)],
|
||||
None,
|
||||
);
|
||||
set.add(ConstraintKind::Distance, vec![SketchRef::whole(Handle::new(103))], Some(25.0));
|
||||
set.add(ConstraintKind::Angle, vec![SketchRef::whole(Handle::new(104)), SketchRef::whole(Handle::new(105))], Some(45.0));
|
||||
set.add(ConstraintKind::Radius, vec![SketchRef::whole(Handle::new(106))], Some(3.5));
|
||||
set
|
||||
}
|
||||
|
||||
/// Writes `set` into a fresh XRecord Chunk under `owner`'s extension
|
||||
/// dictionary, round-trips the whole document through `ext` (`"dxf"` or
|
||||
/// anything else for DWG), and returns the `SketchConstraintSet` read back
|
||||
/// out of the reloaded document.
|
||||
fn roundtrip_through(scene: &Scene, owner: Handle, set: &SketchConstraintSet, ext: &str) -> SketchConstraintSet {
|
||||
let mut doc = scene.document.clone();
|
||||
let bytes = bincode::serialize(set).expect("serialize SketchConstraintSet");
|
||||
|
||||
doc.ensure_xrecord(owner, RECORD_KEY);
|
||||
let record = doc.xrecord_mut(owner, RECORD_KEY).expect("xrecord_mut after ensure_xrecord");
|
||||
record.entries.push(XRecordEntry::new(310, XRecordValue::Chunk(bytes)));
|
||||
|
||||
let saved = OpenCADStudio::io::save_to_bytes(&doc, ext, doc.version)
|
||||
.unwrap_or_else(|e| panic!("save to {ext} bytes: {e}"));
|
||||
let reloaded = OpenCADStudio::io::load_bytes(&format!("roundtrip.{ext}"), saved)
|
||||
.unwrap_or_else(|e| panic!("reload {ext} bytes: {e}"));
|
||||
|
||||
let record = reloaded
|
||||
.xrecord(owner, RECORD_KEY)
|
||||
.unwrap_or_else(|| panic!("XRecord {RECORD_KEY:?} missing after {ext} round-trip"));
|
||||
let chunk = record
|
||||
.entries
|
||||
.iter()
|
||||
.find_map(|entry| match &entry.value {
|
||||
XRecordValue::Chunk(bytes) => Some(bytes.clone()),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_else(|| panic!("no Chunk entry survived the {ext} round-trip"));
|
||||
|
||||
bincode::deserialize::<SketchConstraintSet>(&chunk).unwrap_or_else(|e| panic!("deserialize round-tripped {ext} blob: {e}"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sketch_constraint_set_survives_a_dxf_roundtrip() {
|
||||
let scene = Scene::new();
|
||||
let owner = scene.document.header.model_space_block_handle;
|
||||
assert!(!owner.is_null(), "a fresh document must already have a model-space block handle");
|
||||
let original = sample_set(SketchScope::ModelSpace);
|
||||
|
||||
let restored = roundtrip_through(&scene, owner, &original, "dxf");
|
||||
|
||||
assert_eq!(restored.scope, original.scope);
|
||||
assert_eq!(restored.constraints.len(), original.constraints.len());
|
||||
for (r, o) in restored.constraints.iter().zip(original.constraints.iter()) {
|
||||
assert_eq!(r.id, o.id);
|
||||
assert_eq!(r.kind, o.kind);
|
||||
assert_eq!(r.refs, o.refs);
|
||||
assert_eq!(r.driving_param, o.driving_param);
|
||||
assert_eq!(r.enabled, o.enabled);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sketch_constraint_set_survives_a_dwg_roundtrip() {
|
||||
let scene = Scene::new();
|
||||
let owner = scene.document.header.model_space_block_handle;
|
||||
let original = sample_set(SketchScope::ModelSpace);
|
||||
|
||||
let restored = roundtrip_through(&scene, owner, &original, "dwg");
|
||||
|
||||
assert_eq!(restored.scope, original.scope);
|
||||
assert_eq!(restored.constraints.len(), original.constraints.len());
|
||||
for (r, o) in restored.constraints.iter().zip(original.constraints.iter()) {
|
||||
assert_eq!(r.id, o.id);
|
||||
assert_eq!(r.kind, o.kind);
|
||||
assert_eq!(r.refs, o.refs);
|
||||
assert_eq!(r.driving_param, o.driving_param);
|
||||
}
|
||||
}
|
||||
|
||||
/// A block-record-scoped set (not model space) — confirms the XRecord
|
||||
/// extension-dictionary mechanism works for an arbitrary owner handle, not
|
||||
/// just the one every document already has.
|
||||
#[test]
|
||||
fn sketch_constraint_set_survives_roundtrip_when_owned_by_a_block_record() {
|
||||
let mut scene = Scene::new();
|
||||
// A real block definition via the app's own `create_block_from_entities`
|
||||
// (needs at least one source entity) — a bare, entity-less BlockRecord
|
||||
// table entry turned out not to survive a DXF round-trip at all (the
|
||||
// writer prunes it as unreferenced), so this exercises the actual shape
|
||||
// `SketchScope::Block` will see in practice.
|
||||
let line_handle = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
|
||||
Vector3::new(0.0, 0.0, 0.0),
|
||||
Vector3::new(1.0, 0.0, 0.0),
|
||||
)));
|
||||
let br_handle = scene
|
||||
.create_block_from_entities(
|
||||
&[line_handle],
|
||||
"OCS_TEST_BLOCK",
|
||||
&acadrust::types::Transform::identity(),
|
||||
&acadrust::types::Transform::identity(),
|
||||
)
|
||||
.expect("create a block definition to own the constraint set");
|
||||
|
||||
let original = sample_set(SketchScope::Block(br_handle));
|
||||
let restored = roundtrip_through(&scene, br_handle, &original, "dxf");
|
||||
|
||||
assert_eq!(restored.scope, SketchScope::Block(br_handle));
|
||||
assert_eq!(restored.constraints.len(), original.constraints.len());
|
||||
}
|
||||
Reference in new issue
Block a user