style: apply cargo fmt across workspace (as on main)

Formats the branch the way main's 9cf9ff2 did, so merging main only
conflicts where the code itself differs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_013RyGiMXcsf9HTKfqDxaQGt
This commit is contained in:
Claude committed 2026-10-11 18:29:37 +00:00
1 parent cc17d10ca9
commit bf1c4ddc74
532 files changed
+56995 -33416

No files matched your search

+32 -8
View File
@@ -9,8 +9,8 @@
// Uses the golden reference `~/Downloads/0718-mbmdmc.dwg` (AC1032/R2018). The
// test skips (does not fail) when that file is absent so it never breaks CI.
use OpenCADStudio::io;
use codec::objects::ObjectType;
use OpenCADStudio::io;
const GOLDEN: &str = "/home/hakanseven/Downloads/0718-mbmdmc.dwg";
@@ -21,7 +21,10 @@ fn leaf_blobs(doc: &codec::CadDocument) -> Vec<Vec<u8>> {
.context_scales
.keys()
.filter_map(|h| match doc.objects.get(h) {
Some(ObjectType::Unknown { raw_dwg_data: Some(raw), .. }) => Some(raw.clone()),
Some(ObjectType::Unknown {
raw_dwg_data: Some(raw),
..
}) => Some(raw.clone()),
_ => None,
})
.collect();
@@ -45,16 +48,26 @@ fn annotative_leaf_contexts_survive_dwg_roundtrip() {
n0,
leaves0.len()
);
assert!(n0 > 0, "golden file must carry per-object annotation contexts");
assert!(
n0 > 0,
"golden file must carry per-object annotation contexts"
);
// Same-version DWG round-trip (AC1032 -> AC1032).
let out = io::save_to_bytes(&doc, "dwg", doc.version).expect("save dwg bytes");
let doc2 = io::load_bytes("roundtrip.dwg", out).expect("reload dwg bytes");
let n1 = doc2.context_scales.len();
let leaves1 = leaf_blobs(&doc2);
eprintln!("reloaded: context_scales={} preserved_leaf_blobs={}", n1, leaves1.len());
eprintln!(
"reloaded: context_scales={} preserved_leaf_blobs={}",
n1,
leaves1.len()
);
assert_eq!(n0, n1, "annotation-context leaf COUNT changed across round-trip");
assert_eq!(
n0, n1,
"annotation-context leaf COUNT changed across round-trip"
);
assert_eq!(
leaves0.len(),
leaves1.len(),
@@ -79,7 +92,12 @@ fn annotative_leaf_type_breakdown() {
let mut tally: std::collections::BTreeMap<String, usize> = Default::default();
let mut sizes: std::collections::BTreeMap<String, (usize, usize)> = Default::default();
for h in doc.context_scales.keys() {
if let Some(ObjectType::Unknown { type_name, raw_dwg_data, .. }) = doc.objects.get(h) {
if let Some(ObjectType::Unknown {
type_name,
raw_dwg_data,
..
}) = doc.objects.get(h)
{
// type_name is "DWG_OBJ_<type_code>"; resolve the code to a class name.
let name = type_name
.rsplit('_')
@@ -95,7 +113,10 @@ fn annotative_leaf_type_breakdown() {
e.1 = e.1.max(len);
}
}
eprintln!("=== context-leaf type breakdown ({} total) ===", doc.context_scales.len());
eprintln!(
"=== context-leaf type breakdown ({} total) ===",
doc.context_scales.len()
);
for (name, n) in &tally {
let (lo, hi) = sizes[name];
eprintln!(" {n:>4} {name} raw_bytes {lo}..{hi}");
@@ -147,5 +168,8 @@ fn dump_one_blkref_leaf() {
}
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect::<Vec<_>>().join(" ")
b.iter()
.map(|x| format!("{x:02x}"))
.collect::<Vec<_>>()
.join(" ")
}
+21 -6
View File
@@ -64,10 +64,14 @@ fn empty_annotation_scales_is_not_annotative() {
/// Walk an entity's xdict → "AcDbContextDataManager" → "ACDB_ANNOTATIONSCALES"
/// and return its leaf handles.
fn context_leaves(doc: &CadDocument, entity: Handle) -> Vec<Handle> {
let Some(xd) = doc.get_entity(entity).and_then(|e| e.common().xdictionary_handle) else {
let Some(xd) = doc
.get_entity(entity)
.and_then(|e| e.common().xdictionary_handle)
else {
return vec![];
};
let Some(mgr) = annotative::as_dict(doc, xd).and_then(|d| d.get("AcDbContextDataManager")) else {
let Some(mgr) = annotative::as_dict(doc, xd).and_then(|d| d.get("AcDbContextDataManager"))
else {
return vec![];
};
let Some(coll) = annotative::as_dict(doc, mgr).and_then(|d| d.get("ACDB_ANNOTATIONSCALES"))
@@ -156,7 +160,10 @@ fn ocs_synthesized_block_context_survives_dwg_roundtrip() {
assert_eq!(leaves.len(), 1, "block context leaf lost");
match doc2.objects.get(&leaves[0]) {
Some(ObjectType::ObjectContextData(c)) => {
assert!(matches!(c.kind, ObjectContextKind::BlkRef { .. }), "not a BLKREF context");
assert!(
matches!(c.kind, ObjectContextKind::BlkRef { .. }),
"not a BLKREF context"
);
}
other => panic!("leaf is not an ObjectContextData: {other:?}"),
}
@@ -190,14 +197,20 @@ fn object_scale_membership_add_remove_roundtrips() {
assert_eq!(names, vec!["1:100", "1:50"], "both memberships expected");
// Remove one, keep the other; survives a round-trip.
assert!(annotative::remove_annotation_context_for_scale(&mut doc, ent, s50));
assert!(annotative::remove_annotation_context_for_scale(
&mut doc, ent, s50
));
let bytes = io::save_to_bytes(&doc, "dwg", DxfVersion::AC1032).unwrap();
let doc2 = io::load_bytes("rt.dwg", bytes).unwrap();
let remaining: Vec<String> = annotative::object_scale_memberships(&doc2, ent)
.into_iter()
.map(|(n, _)| n)
.collect();
assert_eq!(remaining, vec!["1:100"], "exactly the un-removed scale remains");
assert_eq!(
remaining,
vec!["1:100"],
"exactly the un-removed scale remains"
);
// Removing the last representation tears the chain down → non-annotative.
let mut doc3 = doc2.clone();
@@ -207,7 +220,9 @@ fn object_scale_membership_add_remove_roundtrips() {
.iter()
.find_map(|(h, o)| matches!(o, ObjectType::Scale(s) if s.name == "1:100").then_some(*h))
.unwrap();
assert!(annotative::remove_annotation_context_for_scale(&mut doc3, ent, s100b));
assert!(annotative::remove_annotation_context_for_scale(
&mut doc3, ent, s100b
));
assert!(
annotative::object_scale_memberships(&doc3, ent).is_empty(),
"no memberships after removing the last"
+29 -7
View File
@@ -109,7 +109,9 @@ fn pattern_hatch_uses_stored_line_spacing() {
.find(|m| matches!(m.pattern, HatchPattern::Pattern(_)))
.expect("pattern hatch present in export set");
let HatchPattern::Pattern(fams) = &m.pattern else { unreachable!() };
let HatchPattern::Pattern(fams) = &m.pattern else {
unreachable!()
};
// Effective perpendicular spacing = family.dy * model.scale.
let dy = fams[0].dy.abs();
let spacing = dy * m.scale;
@@ -268,7 +270,9 @@ fn app_created_hatch_roundtrips_catalog_spacing() {
.iter()
.find(|m| matches!(m.pattern, HatchPattern::Pattern(_)))
.expect("pattern hatch present after round-trip");
let HatchPattern::Pattern(fams) = &m.pattern else { unreachable!() };
let HatchPattern::Pattern(fams) = &m.pattern else {
unreachable!()
};
let got = perp_spacing(&fams[0], m.scale);
assert!(
(got - expected).abs() < expected * 0.02,
@@ -326,7 +330,13 @@ fn nested_hatch_serializes_only_outer_as_external() {
let dxf = scene
.document
.entities()
.find_map(|e| if let EntityType::Hatch(h) = e { Some(h) } else { None })
.find_map(|e| {
if let EntityType::Hatch(h) = e {
Some(h)
} else {
None
}
})
.expect("nested hatch written to document");
assert_eq!(dxf.paths.len(), 2, "outer boundary + one hole path");
@@ -334,8 +344,20 @@ fn nested_hatch_serializes_only_outer_as_external() {
let ex = BoundaryPathFlags::EXTERNAL.bits();
let out = BoundaryPathFlags::OUTERMOST.bits();
assert!(dxf.paths[0].flags.bits() & ex != 0, "outer path must be flagged external");
assert!(dxf.paths[0].flags.bits() & out != 0, "outer path must be flagged outermost");
assert!(dxf.paths[1].flags.bits() & ex == 0, "hole path must NOT be flagged external");
assert!(dxf.paths[1].flags.bits() & out == 0, "hole path must NOT be flagged outermost");
assert!(
dxf.paths[0].flags.bits() & ex != 0,
"outer path must be flagged external"
);
assert!(
dxf.paths[0].flags.bits() & out != 0,
"outer path must be flagged outermost"
);
assert!(
dxf.paths[1].flags.bits() & ex == 0,
"hole path must NOT be flagged external"
);
assert!(
dxf.paths[1].flags.bits() & out == 0,
"hole path must NOT be flagged outermost"
);
}
+34 -10
View File
@@ -87,20 +87,45 @@ fn block_text_depth_composes_through_the_instance_path() {
);
let cache = BlockCache::build(
&scene.document, 1.0, None, true, [0.0, 0.0, 0.0, 1.0], None, &depths,
&scene.document,
1.0,
None,
true,
[0.0, 0.0, 0.0, 1.0],
None,
&depths,
);
let wires = expand_insert(
&scene.document, &cache, &Insert::new("MARK", Vector3::new(0.0, 0.0, 0.0)),
&scene.document,
&cache,
&Insert::new("MARK", Vector3::new(0.0, 0.0, 0.0)),
ins_handle,
[1.0, 1.0, 1.0, 1.0], 0, 0.0, [0.0; 8], 1.0,
InheritStyle { color: [1.0, 1.0, 1.0, 1.0], pat_len: 0.0, pat: [0.0; 8], lw_px: 1.0 },
0, false, false, 1.0, None, None, false, [0.0, 0.0, 0.0, 1.0], 1.0,
OpenCADStudio::scene::BlockScalePolicy::FromInsert, false,
[1.0, 1.0, 1.0, 1.0],
0,
0.0,
[0.0; 8],
1.0,
InheritStyle {
color: [1.0, 1.0, 1.0, 1.0],
pat_len: 0.0,
pat: [0.0; 8],
lw_px: 1.0,
},
0,
false,
false,
1.0,
None,
None,
false,
[0.0, 0.0, 0.0, 1.0],
1.0,
OpenCADStudio::scene::BlockScalePolicy::FromInsert,
false,
)
.expect("block defn cached");
let text_wires: Vec<&WireModel> =
wires.iter().filter(|w| !w.text_verts.is_empty()).collect();
let text_wires: Vec<&WireModel> = wires.iter().filter(|w| !w.text_verts.is_empty()).collect();
assert!(
!text_wires.is_empty(),
"block TEXT must tessellate into glyph quads"
@@ -137,8 +162,7 @@ fn block_text_depth_composes_through_the_instance_path() {
// The wipeout mask, composed the same way the scene graph does it.
let mask = ins_d + wipe_label * ins_half;
let margin_quanta =
(instance - mask) * DRAW_ORDER_BIAS * DEPTH_QUANTA;
let margin_quanta = (instance - mask) * DRAW_ORDER_BIAS * DEPTH_QUANTA;
let bias = OpenCADStudio::scene::pipeline::WIPEOUT_DEPTH_BIAS_QUANTA as f32;
assert!(
margin_quanta > bias + 2.0,
+5 -5
View File
@@ -26,10 +26,7 @@ fn copy_dimension_duplicates_its_block() {
scene.document.add_entity(sub_e).unwrap();
// A linear dimension whose drawn geometry is that baked block.
let mut dim = DimensionLinear::new(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(10.0, 0.0, 0.0),
);
let mut dim = DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
dim.base.block_name = "*D0".to_string();
let dim_h = scene.add_entity(EntityType::Dimension(Dimension::Linear(dim)));
@@ -67,6 +64,9 @@ fn copy_dimension_duplicates_its_block() {
(l.start.x, l.start.y),
(l.end.x, l.end.y)
),
other => panic!("copy block sub is not a line: {:?}", other.map(std::mem::discriminant)),
other => panic!(
"copy block sub is not a line: {:?}",
other.map(std::mem::discriminant)
),
}
}
+4 -3
View File
@@ -31,7 +31,9 @@ fn leader_color(doc: &CadDocument) -> Option<Color> {
fn dim_color(doc: &CadDocument) -> Option<Color> {
doc.entities()
.find_map(|e| match e {
EntityType::Dimension(d) => Some(dov::color(&d.base().common.extended_data, dov::DIMCLRD)),
EntityType::Dimension(d) => {
Some(dov::color(&d.base().common.extended_data, dov::DIMCLRD))
}
_ => None,
})
.flatten()
@@ -66,8 +68,7 @@ fn dimension_scene(aci: i16) -> Scene {
sub_e.common_mut().owner_handle = br_h;
scene.document.add_entity(sub_e).unwrap();
let mut dim =
DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
let mut dim = DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
dim.base.block_name = "*D0".to_string();
let h = scene.add_entity(EntityType::Dimension(Dimension::Linear(dim)));
dov::set(
+61 -22
View File
@@ -4,8 +4,8 @@
use codec::entities::{
CenterMarkAssociation, CenterMarkSource, CenterMarkSourceKind, Dimension, DimensionLinear,
Insert, Leader, Line, LwPolyline, LwVertex, MLine, Polyline, Polyline2D, Polyline3D,
Vertex2D, Vertex3D,
Insert, Leader, Line, LwPolyline, LwVertex, MLine, Polyline, Polyline2D, Polyline3D, Vertex2D,
Vertex3D,
};
use codec::tables::BlockRecord;
use codec::types::{Vector2, Vector3};
@@ -102,7 +102,11 @@ fn explode_insert_golden_block_members() {
add_test_block(&mut doc);
let insert = Insert::new(BLOCK_NAME, Vector3::new(0.0, 0.0, 0.0));
let pieces = explode_entity(&EntityType::Insert(insert), &doc);
assert_eq!(pieces.len(), 6, "insert must materialize all 6 block members");
assert_eq!(
pieces.len(),
6,
"insert must materialize all 6 block members"
);
assert!(pieces.iter().all(|p| matches!(p, EntityType::Line(_))));
}
@@ -123,10 +127,7 @@ fn explode_mline_golden_three_lines_per_segment() {
#[test]
fn explode_dimension_linear_golden_segments() {
let doc = CadDocument::new();
let mut d = DimensionLinear::new(
Vector3::new(0.0, 0.0, 0.0),
Vector3::new(10.0, 0.0, 0.0),
);
let mut d = DimensionLinear::new(Vector3::new(0.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0));
d.definition_point = Vector3::new(5.0, 3.0, 0.0);
d.base.text_middle_point = Vector3::new(5.0, 5.0, 0.0);
let pieces = explode_entity(&EntityType::Dimension(Dimension::Linear(d)), &doc);
@@ -135,12 +136,19 @@ fn explode_dimension_linear_golden_segments() {
// `linear_dim_bakes_filled_arrowheads`), which is why the Lines count is
// 3 — the plan's pre-execution "≥4 Lines" research claim was wrong.
assert_eq!(
pieces.iter().filter(|p| matches!(p, EntityType::Line(_))).count(),
pieces
.iter()
.filter(|p| matches!(p, EntityType::Line(_)))
.count(),
3,
"linear dim must explode to extension + dimension lines, got {pieces:?}"
);
assert!(
pieces.iter().filter(|p| matches!(p, EntityType::Solid(_))).count() >= 2,
pieces
.iter()
.filter(|p| matches!(p, EntityType::Solid(_)))
.count()
>= 2,
"linear dim must bake filled arrowheads (SOLIDs), got {pieces:?}"
);
// The measurement label is baked as a Text entity when the style path
@@ -219,7 +227,10 @@ fn lwpolyline_explode_preserves_length_and_closed_counts() {
_ => 0.0,
})
.sum();
assert!((total - 7.0).abs() < 1e-9, "total length must be preserved, got {total}");
assert!(
(total - 7.0).abs() < 1e-9,
"total length must be preserved, got {total}"
);
let mut sq = LwPolyline::new();
sq.is_closed = true;
@@ -329,8 +340,10 @@ fn build_differential_scene() -> (Scene, FixtureHandles) {
Vector3::new(30.0, 5.0, 0.0),
])));
let insert =
scene.add_entity(EntityType::Insert(Insert::new(BLOCK_NAME, Vector3::new(0.0, 20.0, 0.0))));
let insert = scene.add_entity(EntityType::Insert(Insert::new(
BLOCK_NAME,
Vector3::new(0.0, 20.0, 0.0),
)));
let mut line = Line::new();
line.start = Vector3::new(0.0, 50.0, 0.0);
@@ -357,7 +370,14 @@ fn build_differential_scene() -> (Scene, FixtureHandles) {
(
scene,
FixtureHandles { poly, mline, insert, survivor, leader1, leader2 },
FixtureHandles {
poly,
mline,
insert,
survivor,
leader1,
leader2,
},
)
}
@@ -382,7 +402,10 @@ fn apply_explode_replacements_matches_legacy_loop() {
assert_eq!(handles_prior.survivor, handles.survivor);
let replacements = plan_from_selection(&scene_legacy);
assert!(replacements.len() >= 3, "poly, mline, insert must be planned");
assert!(
replacements.len() >= 3,
"poly, mline, insert must be planned"
);
assert_eq!(
replacements.iter().map(|(h, _)| *h).collect::<Vec<_>>(),
vec![handles.poly, handles.mline, handles.insert],
@@ -407,11 +430,26 @@ fn apply_explode_replacements_matches_legacy_loop() {
);
for scene in [&scene_legacy, &scene_batched] {
assert!(scene.document.get_entity(handles.poly).is_none(), "poly erased");
assert!(scene.document.get_entity(handles.mline).is_none(), "mline erased");
assert!(scene.document.get_entity(handles.insert).is_none(), "insert erased");
assert!(scene.document.get_entity(handles.leader1).is_none(), "leader coupling L1");
assert!(scene.document.get_entity(handles.leader2).is_none(), "leader coupling L2");
assert!(
scene.document.get_entity(handles.poly).is_none(),
"poly erased"
);
assert!(
scene.document.get_entity(handles.mline).is_none(),
"mline erased"
);
assert!(
scene.document.get_entity(handles.insert).is_none(),
"insert erased"
);
assert!(
scene.document.get_entity(handles.leader1).is_none(),
"leader coupling L1"
);
assert!(
scene.document.get_entity(handles.leader2).is_none(),
"leader coupling L2"
);
assert!(
scene.document.get_entity(handles.survivor).is_some(),
"non-explodable survivor must survive"
@@ -423,8 +461,9 @@ fn apply_explode_replacements_matches_legacy_loop() {
}
// explode_batch keeps empty entries so both sides see the same inputs.
let items: Vec<(Handle, &EntityType)> = vec![
(handles.survivor, scene_batched.document.get_entity(handles.survivor).unwrap()),
];
let items: Vec<(Handle, &EntityType)> = vec![(
handles.survivor,
scene_batched.document.get_entity(handles.survivor).unwrap(),
)];
assert_eq!(explode_batch(&items, &scene_batched.document), vec![vec![]]);
}
+55 -14
View File
@@ -21,7 +21,9 @@ fn line_with_z(z0: f64, z1: f64) -> EntityType {
#[test]
fn golden_line_projects_and_resets_thickness_normal() {
let flat = flatten_entity_z(&line_with_z(3.0, 5.0)).expect("moves");
let EntityType::Line(l) = flat else { panic!("variant") };
let EntityType::Line(l) = flat else {
panic!("variant")
};
assert_eq!((l.start.z, l.end.z), (0.0, 0.0));
assert_eq!((l.start.x, l.start.y), (0.0, 0.0));
assert_eq!((l.end.x, l.end.y), (3.0, 4.0));
@@ -34,18 +36,27 @@ fn golden_tilted_circle_becomes_ellipse() {
let mut c = codec::entities::Circle::from_center_radius(Vector3::new(1.0, 2.0, 4.0), 5.0);
c.normal = Vector3::new(0.0, 0.6, 0.8);
let flat = flatten_entity_z(&EntityType::Circle(c)).expect("moves");
assert!(matches!(flat, EntityType::Ellipse(_)), "tilted circle → ellipse");
assert!(
matches!(flat, EntityType::Ellipse(_)),
"tilted circle → ellipse"
);
}
#[test]
fn golden_polyline3d_vertices_and_elevation_zeroed() {
let mut pl = Polyline3D::new();
pl.elevation = 5.0;
pl.vertices.push(Vertex3DPolyline::new(Vector3::new(1.0, 2.0, 7.5)));
pl.vertices
.push(Vertex3DPolyline::new(Vector3::new(1.0, 2.0, 7.5)));
let flat = flatten_entity_z(&EntityType::Polyline3D(pl)).expect("moves");
let EntityType::Polyline3D(pl) = flat else { panic!("variant") };
let EntityType::Polyline3D(pl) = flat else {
panic!("variant")
};
assert_eq!(pl.elevation, 0.0);
assert!(pl.vertices.iter().all(|v| v.position.z == 0.0 && v.position.x == 1.0));
assert!(pl
.vertices
.iter()
.all(|v| v.position.z == 0.0 && v.position.x == 1.0));
}
#[test]
@@ -54,7 +65,9 @@ fn golden_polyline2d_elevation_zeroed() {
pl.elevation = 9.0;
pl.vertices.push(Vertex2D::new(Vector3::new(3.0, 4.0, 0.0)));
let flat = flatten_entity_z(&EntityType::Polyline2D(pl)).expect("moves");
let EntityType::Polyline2D(pl) = flat else { panic!("variant") };
let EntityType::Polyline2D(pl) = flat else {
panic!("variant")
};
assert_eq!(pl.elevation, 0.0);
assert_eq!(pl.vertices[0].location.z, 0.0);
}
@@ -65,7 +78,9 @@ fn golden_text_insertion_and_normal_zeroed() {
t.insertion_point = Vector3::new(1.0, 2.0, 3.0);
t.thickness = 1.0;
let flat = flatten_entity_z(&EntityType::Text(t)).expect("moves");
let EntityType::Text(t) = flat else { panic!("variant") };
let EntityType::Text(t) = flat else {
panic!("variant")
};
assert_eq!(t.insertion_point.z, 0.0);
assert_eq!(t.thickness, 0.0);
assert_eq!(t.normal, Vector3::UNIT_Z);
@@ -80,9 +95,16 @@ fn golden_solid_corners_zeroed() {
Vector3::new(0.0, 1.0, 4.0),
);
let flat = flatten_entity_z(&EntityType::Solid(s)).expect("moves");
let EntityType::Solid(s) = flat else { panic!("variant") };
let EntityType::Solid(s) = flat else {
panic!("variant")
};
assert_eq!(
[s.first_corner.z, s.second_corner.z, s.third_corner.z, s.fourth_corner.z],
[
s.first_corner.z,
s.second_corner.z,
s.third_corner.z,
s.fourth_corner.z
],
[0.0; 4]
);
}
@@ -132,12 +154,28 @@ fn collect_excludes_entities_outside_active_space() {
// Enter block editor scoping on block b: entities whose owner block
// differs from the active space must be excluded.
scene.block_edit_block = Some(b);
scene.document.get_entity_mut(a).unwrap().common_mut().owner_handle = c; // wrong owner
scene.document.get_entity_mut(c).unwrap().common_mut().owner_handle = b; // active block
scene
.document
.get_entity_mut(a)
.unwrap()
.common_mut()
.owner_handle = c; // wrong owner
scene
.document
.get_entity_mut(c)
.unwrap()
.common_mut()
.owner_handle = b; // active block
let candidates = collect_flatten_handles(&scene);
assert!(!candidates.contains(&a), "wrong-owner entity must be excluded");
assert!(candidates.contains(&c), "owner of the active block must be included");
assert!(
!candidates.contains(&a),
"wrong-owner entity must be excluded"
);
assert!(
candidates.contains(&c),
"owner of the active block must be included"
);
}
#[test]
@@ -165,7 +203,10 @@ fn plan_moves_only_movable_candidates() {
assert_eq!((l.start.z, l.end.z), (0.0, 0.0));
}
// Ray untouched.
assert!(matches!(scene.document.get_entity(ray).unwrap(), EntityType::Ray(_)));
assert!(matches!(
scene.document.get_entity(ray).unwrap(),
EntityType::Ray(_)
));
}
#[test]
+4 -1
View File
@@ -133,7 +133,10 @@ fn ctab_is_created_against_a_repaired_root() {
panic!("variable dictionary must resolve");
};
assert!(
variable_dictionary.entries.iter().any(|(key, _)| key == "CTAB"),
variable_dictionary
.entries
.iter()
.any(|(key, _)| key == "CTAB"),
"CTAB must be registered in the variable dictionary"
);
}
+3 -3
View File
@@ -737,9 +737,9 @@ fn midpoint_constraint_pulls_a_point_onto_a_lines_midpoint() {
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(codec::entities::Point::at(
Vector3::new(20.0, 20.0, 0.0),
)));
let point = scene.add_entity(EntityType::Point(codec::entities::Point::at(Vector3::new(
20.0, 20.0, 0.0,
))));
scene
.parametric_constraint_set_mut(ParametricScope::ModelSpace)
.add(
+37 -16
View File
@@ -18,7 +18,10 @@ fn puck() -> kernel::brep::Body {
));
let body = presspull_model::extrusion_body(&entity, [0.0, 0.0, 10.0])
.expect("a closed circular profile extrudes");
assert!(body.validate().is_empty(), "extrusion produced a valid body");
assert!(
body.validate().is_empty(),
"extrusion produced a valid body"
);
body
}
@@ -29,10 +32,10 @@ fn puck() -> kernel::brep::Body {
fn opposite_faces_report_opposite_outward_normals() {
let body = puck();
let top = solid_model::nearest_planar_face(&body, [0.0, 0.0, 10.0])
.expect("the top cap is planar");
let bottom = solid_model::nearest_planar_face(&body, [0.0, 0.0, 0.0])
.expect("the bottom cap is planar");
let top =
solid_model::nearest_planar_face(&body, [0.0, 0.0, 10.0]).expect("the top cap is planar");
let bottom =
solid_model::nearest_planar_face(&body, [0.0, 0.0, 0.0]).expect("the bottom cap is planar");
assert_ne!(top, bottom, "the caps are distinct faces");
let top_normal = solid_model::planar_face_normal(&body, top).expect("top cap has a normal");
@@ -81,18 +84,30 @@ fn the_curved_side_is_not_offered_as_a_plane() {
#[test]
fn ucs_pick_command_enables_solid_face_picking() {
use OpenCADStudio::command::{CadCommand, CmdResult, UcsPickCommand};
use glam::DVec3;
use OpenCADStudio::command::{CadCommand, CmdResult, UcsPickCommand};
let cmd_face = UcsPickCommand { face: true };
assert!(cmd_face.needs_entity_pick(), "UCS FACE needs entity pick");
assert!(cmd_face.entity_pick_includes_fills(), "UCS FACE must include fills to raycast solid faces");
assert!(cmd_face.entity_pick_uses_surface_point(), "UCS FACE must use 3D surface point");
assert!(cmd_face.entity_pick_highlights_hover(), "UCS FACE must highlight hovered solid face");
assert!(
cmd_face.entity_pick_includes_fills(),
"UCS FACE must include fills to raycast solid faces"
);
assert!(
cmd_face.entity_pick_uses_surface_point(),
"UCS FACE must use 3D surface point"
);
assert!(
cmd_face.entity_pick_highlights_hover(),
"UCS FACE must highlight hovered solid face"
);
let cmd_obj = UcsPickCommand { face: false };
assert!(cmd_obj.needs_entity_pick());
assert!(!cmd_obj.entity_pick_includes_fills(), "UCS OBJECT operates on curves, not fills");
assert!(
!cmd_obj.entity_pick_includes_fills(),
"UCS OBJECT operates on curves, not fills"
);
assert!(!cmd_obj.entity_pick_uses_surface_point());
let handle = codec::Handle::new(0x2A);
@@ -113,8 +128,8 @@ fn face_ucs_snaps_origin_and_aligns_axes_on_box() {
// Top face is at Z = +15, bounds X in [-5, 5], Y in [-10, 10].
// Pick near corner (5, 10, 15), closer to the edge along -X than the edge along -Y.
let face = solid_model::nearest_planar_face(&box_body, [3.0, 9.0, 15.0])
.expect("top face found");
let face =
solid_model::nearest_planar_face(&box_body, [3.0, 9.0, 15.0]).expect("top face found");
let ucs = solid_model::planar_face_ucs(&box_body, face, [3.0, 9.0, 15.0])
.expect("planar_face_ucs constructed");
@@ -124,8 +139,11 @@ fn face_ucs_snaps_origin_and_aligns_axes_on_box() {
assert!((ucs.origin.z - 15.0).abs() < 1e-6);
// Z axis must be outward normal (0, 0, 1).
let z = glam::DVec3::new(ucs.x_axis.x, ucs.x_axis.y, ucs.x_axis.z)
.cross(glam::DVec3::new(ucs.y_axis.x, ucs.y_axis.y, ucs.y_axis.z));
let z = glam::DVec3::new(ucs.x_axis.x, ucs.x_axis.y, ucs.x_axis.z).cross(glam::DVec3::new(
ucs.y_axis.x,
ucs.y_axis.y,
ucs.y_axis.z,
));
assert!((z.z - 1.0).abs() < 1e-6);
// Closer to edge along -X: X axis must point along (-1, 0, 0).
@@ -165,8 +183,11 @@ fn face_ucs_on_puck() {
let ucs = solid_model::planar_face_ucs(&body, top_face, [60.0, -25.0, 10.0]).unwrap();
// Normal Z = (0, 0, 1).
let z = glam::DVec3::new(ucs.x_axis.x, ucs.x_axis.y, ucs.x_axis.z)
.cross(glam::DVec3::new(ucs.y_axis.x, ucs.y_axis.y, ucs.y_axis.z));
let z = glam::DVec3::new(ucs.x_axis.x, ucs.x_axis.y, ucs.x_axis.z).cross(glam::DVec3::new(
ucs.y_axis.x,
ucs.y_axis.y,
ucs.y_axis.z,
));
assert!((z.z - 1.0).abs() < 1e-6);
// Orthonormality of X and Y.
+82 -22
View File
@@ -3,9 +3,7 @@ use codec::objects::{Dictionary, DictionaryWithDefault, ObjectType, SpatialFilte
use codec::tables::BlockRecord;
use codec::types::{Matrix4, Vector2, Vector3};
use codec::EntityType;
use OpenCADStudio::scene::pick::xclip::{
insert_spatial_filter, world_clip_polygon_for_transform,
};
use OpenCADStudio::scene::pick::xclip::{insert_spatial_filter, world_clip_polygon_for_transform};
use OpenCADStudio::scene::Scene;
#[test]
@@ -38,18 +36,27 @@ fn xclip_in_paper_and_viewport() {
let mut xdict1 = Dictionary::new();
xdict1.handle = h_xdict1;
xdict1.add_entry("ACAD_FILTER", h_filter1);
scene.document.objects.insert(h_xdict1, ObjectType::Dictionary(xdict1));
scene
.document
.objects
.insert(h_xdict1, ObjectType::Dictionary(xdict1));
let mut filter_dict1 = Dictionary::new();
filter_dict1.handle = h_filter1;
filter_dict1.add_entry("SPATIAL", h_spatial1);
scene.document.objects.insert(h_filter1, ObjectType::Dictionary(filter_dict1));
scene
.document
.objects
.insert(h_filter1, ObjectType::Dictionary(filter_dict1));
let mut sf1 = SpatialFilter::new();
sf1.handle = h_spatial1;
sf1.display_enabled = true;
sf1.boundary_points = vec![Vector2::new(0.0, 0.0), Vector2::new(50.0, 50.0)];
scene.document.objects.insert(h_spatial1, ObjectType::SpatialFilter(sf1));
scene
.document
.objects
.insert(h_spatial1, ObjectType::SpatialFilter(sf1));
scene.set_current_layout("Layout1".into());
@@ -67,8 +74,16 @@ fn xclip_in_paper_and_viewport() {
// Verify point coordinates do not exceed clip boundary (250.0)
for p in &house_wire.points {
if p[0].is_finite() {
assert!(p[0] <= 250.001, "point X {} exceeds clip boundary 250.0", p[0]);
assert!(p[1] <= 50.001, "point Y {} exceeds clip boundary 50.0", p[1]);
assert!(
p[0] <= 250.001,
"point X {} exceeds clip boundary 250.0",
p[0]
);
assert!(
p[1] <= 50.001,
"point Y {} exceeds clip boundary 50.0",
p[1]
);
}
}
}
@@ -95,8 +110,13 @@ fn test_xclip_extension_dictionary_resolution() {
let h_filter = scene.document.allocate_handle();
// Mixed case and whitespace in key:
xdict.entries.push((" acad_filter ".to_string(), h_filter));
scene.document.objects.insert(h_xdict, ObjectType::DictionaryWithDefault(xdict));
xdict
.entries
.push((" acad_filter ".to_string(), h_filter));
scene
.document
.objects
.insert(h_xdict, ObjectType::DictionaryWithDefault(xdict));
// Sub-dictionary (ACAD_FILTER) also as DictionaryWithDefault:
let mut filter_dict = DictionaryWithDefault::new();
@@ -104,13 +124,19 @@ fn test_xclip_extension_dictionary_resolution() {
filter_dict.owner = h_xdict;
let h_spatial = scene.document.allocate_handle();
filter_dict.entries.push(("spatial".to_string(), h_spatial));
scene.document.objects.insert(h_filter, ObjectType::DictionaryWithDefault(filter_dict));
scene
.document
.objects
.insert(h_filter, ObjectType::DictionaryWithDefault(filter_dict));
let mut sf = SpatialFilter::new();
sf.handle = h_spatial;
sf.display_enabled = true;
sf.boundary_points = vec![Vector2::new(10.0, 10.0), Vector2::new(40.0, 40.0)];
scene.document.objects.insert(h_spatial, ObjectType::SpatialFilter(sf));
scene
.document
.objects
.insert(h_spatial, ObjectType::SpatialFilter(sf));
let resolved = insert_spatial_filter(&scene.document, &ins);
assert!(resolved.is_some(), "SpatialFilter should be resolved through a DictionaryWithDefault with loosely written keys");
@@ -173,16 +199,25 @@ fn test_xclip_multiple_inserts_same_block_different_clips() {
let mut xdict1 = Dictionary::new();
xdict1.handle = h_xdict1;
xdict1.add_entry("ACAD_FILTER", h_filter1);
scene.document.objects.insert(h_xdict1, ObjectType::Dictionary(xdict1));
scene
.document
.objects
.insert(h_xdict1, ObjectType::Dictionary(xdict1));
let mut fdict1 = Dictionary::new();
fdict1.handle = h_filter1;
fdict1.add_entry("SPATIAL", h_spatial1);
scene.document.objects.insert(h_filter1, ObjectType::Dictionary(fdict1));
scene
.document
.objects
.insert(h_filter1, ObjectType::Dictionary(fdict1));
let mut sf1 = SpatialFilter::new();
sf1.handle = h_spatial1;
sf1.display_enabled = true;
sf1.boundary_points = vec![Vector2::new(10.0, 10.0), Vector2::new(30.0, 30.0)];
scene.document.objects.insert(h_spatial1, ObjectType::SpatialFilter(sf1));
scene
.document
.objects
.insert(h_spatial1, ObjectType::SpatialFilter(sf1));
let mut ins1 = Insert::new(block_name, Vector3::new(0.0, 0.0, 0.0));
ins1.common.xdictionary_handle = Some(h_xdict1);
@@ -197,11 +232,17 @@ fn test_xclip_multiple_inserts_same_block_different_clips() {
let mut xdict2 = Dictionary::new();
xdict2.handle = h_xdict2;
xdict2.add_entry("ACAD_FILTER", h_filter2);
scene.document.objects.insert(h_xdict2, ObjectType::Dictionary(xdict2));
scene
.document
.objects
.insert(h_xdict2, ObjectType::Dictionary(xdict2));
let mut fdict2 = Dictionary::new();
fdict2.handle = h_filter2;
fdict2.add_entry("SPATIAL", h_spatial2);
scene.document.objects.insert(h_filter2, ObjectType::Dictionary(fdict2));
scene
.document
.objects
.insert(h_filter2, ObjectType::Dictionary(fdict2));
let mut sf2 = SpatialFilter::new();
sf2.handle = h_spatial2;
sf2.display_enabled = true;
@@ -214,7 +255,10 @@ fn test_xclip_multiple_inserts_same_block_different_clips() {
],
};
sf2.boundary_points = vec![Vector2::new(560.0, 60.0), Vector2::new(580.0, 80.0)];
scene.document.objects.insert(h_spatial2, ObjectType::SpatialFilter(sf2));
scene
.document
.objects
.insert(h_spatial2, ObjectType::SpatialFilter(sf2));
let mut ins2 = Insert::new(block_name, Vector3::new(500.0, 0.0, 0.0));
ins2.common.xdictionary_handle = Some(h_xdict2);
@@ -241,13 +285,29 @@ fn test_xclip_multiple_inserts_same_block_different_clips() {
// All points for Insert 1 must be within [10, 10]..[30, 30]
for p in &ins1_points {
assert!(p[0] >= 9.99 && p[0] <= 30.01, "Insert 1 point X {} outside [10, 30]", p[0]);
assert!(p[1] >= 9.99 && p[1] <= 30.01, "Insert 1 point Y {} outside [10, 30]", p[1]);
assert!(
p[0] >= 9.99 && p[0] <= 30.01,
"Insert 1 point X {} outside [10, 30]",
p[0]
);
assert!(
p[1] >= 9.99 && p[1] <= 30.01,
"Insert 1 point Y {} outside [10, 30]",
p[1]
);
}
// All points for Insert 2 must be within [560, 60]..[580, 80]
for p in &ins2_points {
assert!(p[0] >= 559.99 && p[0] <= 580.01, "Insert 2 point X {} outside [560, 580]", p[0]);
assert!(p[1] >= 59.99 && p[1] <= 80.01, "Insert 2 point Y {} outside [60, 80]", p[1]);
assert!(
p[0] >= 559.99 && p[0] <= 580.01,
"Insert 2 point X {} outside [560, 580]",
p[0]
);
assert!(
p[1] >= 59.99 && p[1] <= 80.01,
"Insert 2 point Y {} outside [60, 80]",
p[1]
);
}
}