Files
OneCAD-Draft/src/app/viewport_dimension_tests.rs
T
Claude bf1c4ddc74 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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013RyGiMXcsf9HTKfqDxaQGt
2026-10-11 18:29:37 +00:00

1498 lines
56 KiB
Rust

use super::OpenCADStudio;
use crate::command::{DimensionAssociationSource, StepInput};
use crate::entities::dim_override;
use crate::scene::viewport_ref::{AcceptedSnap, MeasurementScale, ViewportFrame};
use crate::scene::Scene;
use crate::snap::{SnapResult, SnapType};
use codec::entities::{Circle, Dimension, Line, Viewport};
use codec::types::{Handle, Vector3};
use codec::{CadDocument, EntityType};
use glam::DVec3;
fn fixture() -> (OpenCADStudio, Handle, ViewportFrame) {
let mut app = OpenCADStudio::new_for_test();
app.automation_op(r#"{"op":"new"}"#);
let mut scene = Scene::new();
let line = scene.add_entity(EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::new(100.0, 0.0, 0.0),
)));
scene.document.add_layout("Dimensions").unwrap();
scene.set_current_layout("Dimensions".into());
let mut viewport = Viewport::new();
viewport.id = 2;
viewport.center = Vector3::new(50.0, 50.0, 0.0);
viewport.width = 100.0;
viewport.height = 100.0;
viewport.view_height = 1000.0;
viewport.custom_scale = 0.1;
viewport.view_direction = Vector3::UNIT_Z;
viewport.turn_on();
let viewport = scene.add_entity(EntityType::Viewport(viewport));
let frame = scene.viewport_frame(viewport).unwrap();
scene.document.header.dimension_associativity = 2;
let i = app.active_tab;
app.tabs[i].scene = scene;
(app, line, frame)
}
fn hit(frame: ViewportFrame, source: Handle, model: DVec3) -> SnapResult {
SnapResult {
world: frame.model_to_paper(model),
model_point: Some(model),
screen: iced::Point::ORIGIN,
snap_type: SnapType::Endpoint,
tangent_obj: None,
extension_base: None,
extension_base2: None,
extension_origin: None,
extension_dir: None,
viewport: Some(frame.viewport),
source: Some(DimensionAssociationSource::inferred(source)),
secondary_source: None,
}
}
fn point(app: &mut OpenCADStudio, frame: ViewportFrame, source: Handle, model: DVec3) {
let i = app.active_tab;
let hit = hit(frame, source, model);
let paper = hit.world.with_z(0.0);
assert!(app.record_accepted_snap(i, Some(hit), Some(frame), paper));
let result = app.tabs[i].active_cmd.as_mut().unwrap().on_point(paper);
app.sync_dimension_snaps(i);
let _ = app.apply_cmd_result(result);
}
fn dimension(doc: &CadDocument) -> &Dimension {
doc.entities()
.find_map(|e| match e {
EntityType::Dimension(d) => Some(d),
_ => None,
})
.unwrap()
}
fn displayed(doc: &CadDocument) -> f64 {
let dim = dimension(doc);
if matches!(dim, Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)) {
return dim.measurement();
}
let factor = dim_override::real(&dim.base().common.extended_data, dim_override::DIMLFAC)
.or_else(|| {
doc.dim_styles
.get(&dim.base().style_name)
.map(|s| s.dimlfac)
})
.unwrap_or(1.0);
dim.measurement() * MeasurementScale::user_lfac_for_space(factor, true)
}
pub(super) fn preview_text(app: &OpenCADStudio, entity: &EntityType) -> String {
let EntityType::Dimension(dimension) = entity else {
panic!("expected dimension")
};
let doc = &app.tabs[app.active_tab].scene.document;
let source = doc.dim_styles.get(&dimension.base().style_name).unwrap();
let style = crate::entities::dimension::resolved_dimension_style(source, dimension, doc);
crate::entities::dimension::dimension_text_value(dimension, Some(&style)).unwrap()
}
fn finish_aligned(app: &mut OpenCADStudio, line: Handle, frame: ViewportFrame) {
let _ = app.dispatch_command("DIMALIGNED");
point(app, frame, line, DVec3::ZERO);
point(app, frame, line, DVec3::X * 100.0);
let _ = app.feed_command(StepInput::Point(DVec3::new(250.0, 200.0, 0.0)));
}
#[test]
fn viewport_dimension_navigation_refreshes_after_the_complete_camera_update() {
let (mut app, _, _, frame) =
start_centerline_dimension("DIMALIGNED", true, SnapType::Midpoint, 1.0);
let _ = app.feed_command(StepInput::Point(DVec3::new(60.0, 80.0, 0.0)));
let scene = &mut app.tabs[app.active_tab].scene;
assert!((displayed(&scene.document) - 50.0).abs() < 1e-4);
scene.active_viewport = Some(frame.viewport);
let mut camera = scene.navigation_camera();
camera.target.x += 20.0;
camera.distance *= 0.5;
assert!(scene.apply_navigation_camera(camera));
assert!(
(displayed(&scene.document) - 55.0).abs() < 1e-4,
"{}",
displayed(&scene.document)
);
}
#[test]
fn viewport_dimension_imported_paper_owner_does_not_require_layout_objects() {
let (mut app, line, frame) = fixture();
finish_aligned(&mut app, line, frame);
let doc = &mut app.tabs[app.active_tab].scene.document;
doc.objects
.retain(|_, object| !matches!(object, codec::objects::ObjectType::Layout(_)));
assert!(crate::entities::dimension::dimension_in_paper_space(
dimension(doc),
doc
));
}
#[test]
fn viewport_dimension_curved_clip_includes_the_exact_boundary() {
let (mut app, _, frame) = fixture();
let scene = &mut app.tabs[app.active_tab].scene;
let mut circle = Circle::new();
circle.center = Vector3::new(50.0, 50.0, 0.0);
circle.radius = 10.0;
let boundary = circle.point_at_angle_wcs(0.123456);
let clip = scene.add_entity(EntityType::Circle(circle));
let EntityType::Viewport(vp) = scene.document.get_entity_mut(frame.viewport).unwrap() else {
panic!()
};
vp.clip_boundary_handle = clip;
vp.status.non_rectangular_clipping = true;
assert!(scene
.viewport_displays_paper_point(frame.viewport, glam::DVec2::new(boundary.x, boundary.y)));
assert!(!scene.viewport_displays_paper_point(frame.viewport, glam::DVec2::new(60.01, 50.0)));
assert!(!scene.viewport_displays_paper_point(frame.viewport, glam::DVec2::new(35.0, 60.0)));
}
#[test]
fn viewport_dimension_apparent_intersection_extends_only_the_lines() {
use crate::scene::dimension_assoc_chain::{feature_point, osnap, FeatureContext};
use codec::objects::AssocDimensionReference;
let mut scene = Scene::new();
let first = scene.add_entity(EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::UNIT_X,
)));
let second = scene.add_entity(EntityType::Line(Line::from_points(
Vector3::new(2.0, -1.0, 0.0),
Vector3::new(2.0, -0.5, 0.0),
)));
let mut reference = AssocDimensionReference {
osnap_type: osnap::APPARENT_INT,
xrefs: vec![first],
intersection_objects: vec![second],
..Default::default()
};
let expected = Vector3::new(2.0, 0.0, 0.0);
let context = FeatureContext {
hint: Some(expected),
from: None,
};
let entity = scene.document.get_entity(first).unwrap();
assert_eq!(
feature_point(&scene.document, entity, &reference, context),
Some(expected)
);
reference.osnap_type = osnap::INTERSEC;
assert!(feature_point(&scene.document, entity, &reference, context).is_none());
}
#[test]
fn viewport_dimension_projected_snap_keeps_identity_and_elevation() {
let (_, line, frame) = fixture();
let model = DVec3::new(100.0, 0.0, 35.0);
let hit = hit(frame, line, model);
let accepted =
AcceptedSnap::from_snap(&hit, Some(frame)).with_paper_point(hit.world.with_z(0.0));
assert_eq!(accepted.model_point, model);
assert_eq!(accepted.source.as_ref().unwrap().source.handle, line);
let moved = accepted.with_paper_point(hit.world + DVec3::X);
assert!(moved.source.is_none());
assert!((moved.model_point - frame.paper_to_model(moved.paper_point)).length() < 1e-9);
}
#[test]
fn viewport_dimension_creation_preserves_active_units_and_roundtrips() {
for factor in [25.4, -25.4] {
let (mut app, line, frame) = fixture();
let i = app.active_tab;
let mut style = codec::tables::DimStyle::new("Millimetres");
style.dimlfac = factor;
app.tabs[i].scene.document.dim_styles.add(style).unwrap();
app.tabs[i].scene.document.header.current_dimstyle_name = "Millimetres".into();
finish_aligned(&mut app, line, frame);
let doc = &app.tabs[i].scene.document;
assert_eq!(dimension(doc).base().style_name, "Millimetres");
assert!((displayed(doc) - 2540.0).abs() < 1e-5, "{}", displayed(doc));
let data = MeasurementScale::read(&dimension(doc).base().common.extended_data).unwrap();
assert_eq!(data.user_lfac, 25.4);
assert_eq!(doc.header.dimension_associativity, 2);
for ext in ["dxf", "dwg"] {
let bytes = crate::io::save_to_bytes(doc, ext, doc.version).unwrap();
let loaded = crate::io::load_bytes(&format!("viewport.{ext}"), bytes).unwrap();
assert!(
(displayed(&loaded) - 2540.0).abs() < 1e-4,
"{ext}: {}",
displayed(&loaded)
);
assert_eq!(
MeasurementScale::read(&dimension(&loaded).base().common.extended_data),
Some(data)
);
}
app.undo_steps(1);
assert!(!app.tabs[i]
.scene
.document
.entities()
.any(|e| matches!(e, EntityType::Dimension(_))));
app.redo_steps(1);
assert!((displayed(&app.tabs[i].scene.document) - 2540.0).abs() < 1e-5);
}
}
#[test]
fn viewport_dimension_conflicting_viewports_and_degenerate_picks_allow_retry() {
let (mut app, line, frame) = fixture();
let i = app.active_tab;
let _ = app.dispatch_command("DIMALIGNED");
point(&mut app, frame, line, DVec3::ZERO);
point(&mut app, frame, line, DVec3::ZERO);
assert_eq!(app.accepted_snaps().len(), 1);
let other = ViewportFrame {
viewport: Handle::new(0xFFFF),
..frame
};
assert!(!app.record_accepted_snap(
i,
Some(hit(other, line, DVec3::X * 100.0)),
Some(other),
DVec3::new(60.0, 50.0, 0.0)
));
point(&mut app, frame, line, DVec3::X * 100.0);
let _ = app.feed_command(StepInput::Point(DVec3::new(250.0, 200.0, 0.0)));
assert!((displayed(&app.tabs[i].scene.document) - 100.0).abs() < 1e-5);
}
#[test]
fn viewport_dimension_typed_sheet_points_keep_paper_units() {
let (mut app, _, _) = fixture();
let result = app.automation_op(r#"{"op":"run","cmd":"DIMALIGNED 50,50 60,50 60,55"}"#);
assert_eq!(result["ok"], true);
let doc = &app.tabs[app.active_tab].scene.document;
assert!((displayed(doc) - 10.0).abs() < 1e-9);
assert!(MeasurementScale::read(&dimension(doc).base().common.extended_data).is_none());
}
#[test]
fn viewport_dimension_linear_aligned_and_radial_object_picks() {
for (name, expected) in [
("DIMLINEAR", 100.0),
("DIMALIGNED", 100.0),
("DIMRADIUS", 100.0),
("DIMDIAMETER", 200.0),
] {
let (mut app, _, frame) = fixture();
let i = app.active_tab;
let radial = name == "DIMRADIUS" || name == "DIMDIAMETER";
let model = if radial {
let mut circle = Circle::new();
circle.center = Vector3::new(0.0, 200.0, 0.0);
circle.radius = 100.0;
app.tabs[i].scene.set_current_layout("Model".into());
app.tabs[i].scene.add_entity(EntityType::Circle(circle));
app.tabs[i].scene.set_current_layout("Dimensions".into());
DVec3::new(100.0, 200.0, 0.0)
} else {
DVec3::new(50.0, 0.0, 0.0)
};
let _ = app.dispatch_command(name);
if !radial {
let _ = app.feed_command(StepInput::Enter);
}
let result = app
.try_dimension_viewport_entity_pick(i, frame.model_to_paper(model), 0.5)
.unwrap();
let _ = app.apply_cmd_result(result);
assert_eq!(
app.accepted_snaps().len(),
if radial { 1 } else { 2 },
"{name}"
);
let place = DVec3::new(55.0, 80.0, 0.0);
let preview = app.dimension_preview_entities(i, place).unwrap();
assert_eq!(preview.len(), 1, "{name}");
let value = preview_text(&app, &preview[0]);
assert!(
!app.dimension_preview_wires(i, place).unwrap().is_empty(),
"{name}"
);
let _ = app.feed_command(StepInput::Point(place));
assert_eq!(
preview_text(
&app,
&EntityType::Dimension(dimension(&app.tabs[i].scene.document).clone())
),
value,
"{name}"
);
assert!(
(displayed(&app.tabs[i].scene.document) - expected).abs() < 1e-4,
"{name}: {}",
displayed(&app.tabs[i].scene.document)
);
}
}
#[test]
fn viewport_dimension_angular_preserves_angle_without_length_factor() {
let (mut app, line, frame) = fixture();
let i = app.active_tab;
app.tabs[i].scene.set_current_layout("Model".into());
let vertical = app.tabs[i]
.scene
.add_entity(EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::new(0.0, 100.0, 0.0),
)));
app.tabs[i].scene.set_current_layout("Dimensions".into());
let _ = app.dispatch_command("DIMANGULAR");
let _ = app.feed_command(StepInput::Enter);
point(&mut app, frame, line, DVec3::ZERO);
point(&mut app, frame, line, DVec3::X * 100.0);
point(&mut app, frame, vertical, DVec3::Y * 100.0);
let place = DVec3::new(70.0, 70.0, 0.0);
let preview = app.dimension_preview_entities(i, place).unwrap();
assert_eq!(preview.len(), 1);
assert!(MeasurementScale::read(&preview[0].common().extended_data).is_none());
let value = preview_text(&app, &preview[0]);
let _ = app.feed_command(StepInput::Point(place));
let doc = &app.tabs[app.active_tab].scene.document;
assert_eq!(
preview_text(&app, &EntityType::Dimension(dimension(doc).clone())),
value
);
assert!((dimension(doc).measurement() - 90.0).abs() < 1e-5);
assert!(MeasurementScale::read(&dimension(doc).base().common.extended_data).is_none());
}
#[test]
fn viewport_dimension_preview_uses_candidate_scale_only_before_second_pick() {
for command in ["DIMLINEAR", "DIMALIGNED"] {
let (mut app, model, frame) = fixture();
let i = app.active_tab;
let mut style = codec::tables::DimStyle::new("PreviewUnits");
style.dimlfac = 25.4;
app.tabs[i].scene.document.dim_styles.add(style).unwrap();
app.tabs[i].scene.document.header.current_dimstyle_name = "PreviewUnits".into();
let _ = app.dispatch_command(command);
let _ = app.feed_command(StepInput::Point(DVec3::new(55.0, 50.0, 0.0)));
let target = hit(frame, model, DVec3::X * 100.0);
let sheet = app.dimension_preview_entities(i, target.world).unwrap();
assert_eq!(preview_text(&app, &sheet[0]), "127");
app.vp_snap_frame = Some(frame);
app.tabs[i].snap_result = Some(target);
let model_preview = app.dimension_preview_entities(i, target.world).unwrap();
assert_eq!(preview_text(&app, &model_preview[0]), "1270");
point(&mut app, frame, model, DVec3::X * 100.0);
let other = ViewportFrame {
viewport: Handle::new(0xFFFF),
scale: frame.scale * 2.0,
..frame
};
app.vp_snap_frame = Some(other);
app.tabs[i].snap_result = Some(hit(other, model, DVec3::X * 100.0));
let placement = app
.dimension_preview_entities(i, DVec3::new(60.0, 80.0, 0.0))
.unwrap();
assert_eq!(preview_text(&app, &placement[0]), "1270");
}
}
#[test]
fn viewport_dimension_snap_query_matches_pan_scale_and_twist() {
for (height, twist, target) in [
(1000.0, 0.0, Vector3::ZERO),
(500.0, 0.45, Vector3::new(80.0, 20.0, 0.0)),
] {
let (mut app, line, initial) = fixture();
let i = app.active_tab;
let Some(EntityType::Viewport(vp)) =
app.tabs[i].scene.document.get_entity_mut(initial.viewport)
else {
panic!()
};
vp.view_height = height;
vp.custom_scale = vp.height / height;
vp.twist_angle = twist;
vp.view_target = target;
let frame = app.tabs[i].scene.viewport_frame(initial.viewport).unwrap();
let model = DVec3::X * 100.0;
let paper = frame.model_to_paper(model);
let bounds = iced::Rectangle {
x: 0.0,
y: 0.0,
width: 1000.0,
height: 1000.0,
};
let screen = {
let cam = app.tabs[i].scene.camera.borrow();
let ndc = cam
.view_proj_rte(bounds)
.project_point3((paper - cam.eye()).as_vec3());
iced::Point::new((ndc.x + 1.0) * 500.0, (1.0 - ndc.y) * 500.0)
};
app.snapper.snap_enabled = true;
let (hit, acquired_frame) = app
.paper_viewport_snap(i, screen, (1000.0, 1000.0), paper)
.unwrap();
assert_eq!(hit.source.unwrap().handle, line);
assert!((hit.world - paper).length() < 1e-5, "{hit:?}");
let accepted = AcceptedSnap::from_snap(&hit, Some(acquired_frame));
assert!((accepted.model_point - model).length() < 1e-6);
}
}
#[test]
fn viewport_dimension_eligibility_is_shared_and_uses_clipping() {
use codec::entities::LwPolyline;
use codec::types::Vector2;
let (mut app, _, first) = fixture();
let scene = &mut app.tabs[app.active_tab].scene;
let mut overlay = scene.document.get_entity(first.viewport).unwrap().clone();
if let EntityType::Viewport(vp) = &mut overlay {
vp.id = 3;
vp.width = 200.0;
}
overlay.common_mut().handle = Handle::NULL;
let top = scene.add_entity(overlay);
let paper = DVec3::new(55.0, 50.0, 0.0);
assert_eq!(
scene
.viewport_frames_at_paper_point(paper)
.into_iter()
.next()
.unwrap()
.viewport,
top
);
assert_eq!(
scene
.dimension_pick_through_viewport(paper, 0.2)
.unwrap()
.frame
.viewport,
top
);
let mut clip = LwPolyline::from_points(vec![
Vector2::new(0.0, 0.0),
Vector2::new(100.0, 0.0),
Vector2::new(0.0, 100.0),
]);
clip.is_closed = true;
let clip = scene.add_entity(EntityType::LwPolyline(clip));
if let Some(EntityType::Viewport(vp)) = scene.document.get_entity_mut(top) {
vp.clip_boundary_handle = clip;
vp.status.non_rectangular_clipping = true;
}
assert_eq!(
scene
.viewport_frames_at_paper_point(paper)
.into_iter()
.next()
.unwrap()
.viewport,
first.viewport
);
assert_eq!(
scene
.dimension_pick_through_viewport(paper, 0.2)
.unwrap()
.frame
.viewport,
first.viewport
);
if let Some(EntityType::Viewport(vp)) = scene.document.get_entity_mut(first.viewport) {
vp.turn_off();
}
assert!(scene.viewport_frames_at_paper_point(paper).is_empty());
assert!(scene.dimension_pick_through_viewport(paper, 0.2).is_none());
}
#[test]
fn viewport_dimension_exploded_commit_uses_compensated_text_and_undo() {
let (mut app, line, frame) = fixture();
let i = app.active_tab;
app.tabs[i].scene.document.header.dimension_associativity = 0;
finish_aligned(&mut app, line, frame);
let doc = &app.tabs[i].scene.document;
assert!(!doc
.entities()
.any(|e| matches!(e, EntityType::Dimension(_))));
let texts: Vec<_> = doc
.entities()
.filter_map(|e| match e {
EntityType::Text(t) => Some(t.value.clone()),
EntityType::MText(t) => Some(t.value.clone()),
_ => None,
})
.collect();
assert!(texts.iter().any(|t| t.contains("100")), "{texts:?}");
app.undo_steps(1);
assert_eq!(app.tabs[i].scene.document.header.dimension_associativity, 0);
app.redo_steps(1);
assert!(app.tabs[i]
.scene
.document
.entities()
.any(|e| matches!(e, EntityType::Text(_) | EntityType::MText(_))));
}
#[test]
fn viewport_dimension_association_scale_edit_picture_and_history() {
use crate::scene::ChangeKind;
let (mut app, line, frame) = fixture();
let i = app.active_tab;
finish_aligned(&mut app, line, frame);
let handle = dimension(&app.tabs[i].scene.document).base().common.handle;
assert_eq!(
app.tabs[i].scene.dimension_association_status(handle).len(),
2
);
let Some(EntityType::Dimension(d)) = app.tabs[i].scene.document.get_entity_mut(handle) else {
panic!()
};
d.base_mut().text_user_positioned = true;
d.base_mut().text_middle_point = Vector3::new(250.0, 200.0, 0.0);
d.base_mut().block_name = "*SavedPicture".into();
let pending = app.begin_undo(i, "Viewport scale", 1, true).unwrap();
let before = app.tabs[i].scene.document.get_entity_arc(frame.viewport);
app.tabs[i].scene.record_undo_before(frame.viewport, before);
let Some(EntityType::Viewport(vp)) = app.tabs[i].scene.document.get_entity_mut(frame.viewport)
else {
panic!()
};
vp.view_height = 500.0;
vp.custom_scale = 0.2;
app.tabs[i].scene.notify_viewport_changed(frame.viewport);
app.commit_undo_delta(i, pending);
assert!((displayed(&app.tabs[i].scene.document) - 100.0).abs() < 1e-5);
assert!((dimension(&app.tabs[i].scene.document).measurement() - 20.0).abs() < 1e-5);
assert!(dimension(&app.tabs[i].scene.document)
.base()
.block_name
.is_empty());
assert_eq!(
dimension(&app.tabs[i].scene.document)
.base()
.text_middle_point,
Vector3::new(250.0, 200.0, 0.0)
);
app.undo_steps(1);
assert!((dimension(&app.tabs[i].scene.document).measurement() - 10.0).abs() < 1e-5);
assert_eq!(
dimension(&app.tabs[i].scene.document).base().block_name,
"*SavedPicture"
);
app.redo_steps(1);
assert!((displayed(&app.tabs[i].scene.document) - 100.0).abs() < 1e-5);
let pending = app.begin_undo(i, "Edit source", 1, true).unwrap();
let before = app.tabs[i].scene.document.get_entity_arc(line);
app.tabs[i].scene.record_undo_before(line, before);
let Some(EntityType::Line(source)) = app.tabs[i].scene.document.get_entity_mut(line) else {
panic!()
};
source.end.x = 175.0;
app.tabs[i]
.scene
.bump_entities(&[(line, ChangeKind::Modified)]);
app.commit_undo_delta(i, pending);
assert!((displayed(&app.tabs[i].scene.document) - 175.0).abs() < 1e-5);
let pieces = crate::modules::draw::modify::explode::explode_entity(
app.tabs[i].scene.document.get_entity(handle).unwrap(),
&app.tabs[i].scene.document,
);
assert!(
pieces.iter().any(
|e| matches!(e,EntityType::Text(t) if t.value.contains("175"))
|| matches!(e,EntityType::MText(t) if t.value.contains("175"))
),
"{pieces:?}"
);
for ext in ["dxf", "dwg"] {
let doc = &app.tabs[i].scene.document;
let bytes = crate::io::save_to_bytes(doc, ext, doc.version).unwrap();
let mut loaded = Scene::new();
loaded.document = crate::io::load_bytes(&format!("assoc.{ext}"), bytes).unwrap();
loaded.notify_viewport_changed(frame.viewport);
assert!((displayed(&loaded.document) - 175.0).abs() < 1e-5, "{ext}");
assert_eq!(loaded.dimension_association_status(handle).len(), 2);
}
app.undo_steps(1);
assert!((displayed(&app.tabs[i].scene.document) - 100.0).abs() < 1e-5);
}
#[test]
fn viewport_dimension_unsupported_and_erased_sources_keep_last_picture() {
use crate::scene::dimension_assoc::{resolve_reference_chain, ReferenceStatus};
use codec::objects::AssocDimensionReference;
let (mut app, line, frame) = fixture();
let i = app.active_tab;
finish_aligned(&mut app, line, frame);
let handle = dimension(&app.tabs[i].scene.document).base().common.handle;
let reference = AssocDimensionReference {
xrefs: vec![frame.viewport, line],
osnap_type: 255,
..Default::default()
};
assert!(matches!(
resolve_reference_chain(&app.tabs[i].scene, &reference, None),
Err(ReferenceStatus::Unresolved)
));
let previous = app.tabs[i]
.scene
.document
.get_entity(handle)
.unwrap()
.clone();
let Some(EntityType::Viewport(vp)) = app.tabs[i].scene.document.get_entity_mut(frame.viewport)
else {
panic!()
};
vp.status.perspective = true;
app.tabs[i].scene.notify_viewport_changed(frame.viewport);
assert_eq!(
app.tabs[i].scene.document.get_entity(handle).unwrap(),
&previous
);
assert!(app.tabs[i]
.scene
.dimension_association_status(handle)
.iter()
.all(|(_, s)| *s == ReferenceStatus::Unresolved));
let Some(EntityType::Viewport(vp)) = app.tabs[i].scene.document.get_entity_mut(frame.viewport)
else {
panic!()
};
vp.status.perspective = false;
let pending = app.begin_undo(i, "Erase source", 1, false).unwrap();
app.tabs[i].scene.erase_entities(&[line]);
app.commit_undo_delta(i, pending);
assert_eq!(
app.tabs[i].scene.document.get_entity(handle).unwrap(),
&previous
);
assert!(app.tabs[i]
.scene
.dimension_association_status(handle)
.iter()
.all(|(_, s)| *s == ReferenceStatus::Broken(line)));
app.undo_steps(1);
assert!(app.tabs[i]
.scene
.dimension_association_status(handle)
.iter()
.all(|(_, s)| *s == ReferenceStatus::Resolved));
}
#[test]
fn viewport_dimension_intersection_tracks_both_entities() {
use crate::scene::ChangeKind;
let (mut app, line, frame) = fixture();
let i = app.active_tab;
app.tabs[i].scene.set_current_layout("Model".into());
let other = app.tabs[i]
.scene
.add_entity(EntityType::Line(Line::from_points(
Vector3::new(60.0, -50.0, 0.0),
Vector3::new(60.0, 50.0, 0.0),
)));
app.tabs[i].scene.set_current_layout("Dimensions".into());
let _ = app.dispatch_command("DIMALIGNED");
point(&mut app, frame, line, DVec3::ZERO);
let mut intersection = hit(frame, line, DVec3::X * 60.0);
intersection.snap_type = SnapType::Intersection;
intersection.secondary_source = Some(DimensionAssociationSource::inferred(other));
assert!(app.record_accepted_snap(i, Some(intersection), Some(frame), intersection.world));
let result = app.tabs[i]
.active_cmd
.as_mut()
.unwrap()
.on_point(intersection.world);
app.sync_dimension_snaps(i);
let _ = app.apply_cmd_result(result);
let _ = app.feed_command(StepInput::Point(DVec3::new(80.0, 80.0, 0.0)));
let handle = dimension(&app.tabs[i].scene.document).base().common.handle;
assert!(app.tabs[i]
.scene
.dimension_association_sources(handle)
.contains(&other));
assert!((displayed(&app.tabs[i].scene.document) - 60.0).abs() < 1e-5);
let Some(EntityType::Line(source)) = app.tabs[i].scene.document.get_entity_mut(other) else {
panic!()
};
source.start.x = 80.0;
source.end.x = 80.0;
app.tabs[i]
.scene
.bump_entities(&[(other, ChangeKind::Modified)]);
assert!((displayed(&app.tabs[i].scene.document) - 80.0).abs() < 1e-5);
}
#[test]
fn viewport_dimension_spline_tangency_is_parallel_and_validated() {
use crate::scene::dimension_assoc_chain::{feature_point, osnap, FeatureContext};
use codec::objects::AssocDimensionReference;
let mut spline = codec::entities::Spline::new();
spline.degree = 2;
spline.control_points = vec![
Vector3::ZERO,
Vector3::new(0.5, 0.0, 0.0),
Vector3::new(1.0, 1.0, 0.0),
];
spline.knots = vec![0.0, 0.0, 0.0, 1.0, 1.0, 1.0];
let entity = EntityType::Spline(spline);
let reference = AssocDimensionReference {
osnap_type: osnap::TAN,
osnap_distance: 0.8,
..Default::default()
};
let result = feature_point(
&CadDocument::new(),
&entity,
&reference,
FeatureContext {
hint: Some(Vector3::new(1.0, 1.0, 0.0)),
from: Some(Vector3::new(0.0, -1.0, 0.0)),
},
)
.unwrap();
assert!(
(result.x - 1.0).abs() < 1e-6 && (result.y - 1.0).abs() < 1e-6,
"{result:?}"
);
assert!(feature_point(
&CadDocument::new(),
&entity,
&reference,
FeatureContext {
hint: None,
from: Some(Vector3::new(0.0, 1.0, 0.0))
}
)
.is_none());
}
#[test]
#[ignore = "Set OPENCAD_VIEWPORT_REGRESSION_DXF to an external regression fixture"]
fn viewport_dimension_external_fixture_keeps_unchanged_measurements() {
use crate::scene::ChangeKind;
let path = std::env::var("OPENCAD_VIEWPORT_REGRESSION_DXF").expect("regression drawing path");
let doc = codec::DxfReader::from_file(std::path::Path::new(&path))
.unwrap()
.read()
.unwrap();
let mut scene = Scene::new();
scene.document = doc;
let dimensions: Vec<_> = scene
.document
.entities()
.filter_map(|entity| match entity {
EntityType::Dimension(dim) => {
Some((dim.base().common.handle, dim.base().actual_measurement))
}
_ => None,
})
.collect();
let mut sources: Vec<_> = dimensions
.iter()
.flat_map(|(h, _)| scene.dimension_association_sources(*h))
.collect();
sources.sort();
sources.dedup();
scene.bump_entities(
&sources
.into_iter()
.map(|h| (h, ChangeKind::Modified))
.collect::<Vec<_>>(),
);
for (handle, saved) in dimensions {
if scene.dimension_association_status(handle).is_empty() {
continue;
}
let EntityType::Dimension(dim) = scene.document.get_entity(handle).unwrap() else {
panic!()
};
let factor = dim_override::real(&dim.base().common.extended_data, dim_override::DIMLFAC)
.unwrap_or(1.0)
.abs();
let result = dim.measurement() * factor;
println!(
"{:X}: saved={saved:.9}, current={result:.9}, status={:?}",
handle.value(),
scene.dimension_association_status(handle)
);
assert!(
(result - saved).abs() < 1e-3,
"{:X}: {saved} -> {result}",
handle.value()
);
}
}
#[test]
fn viewport_dimension_fixture_regenerates_and_tracks_an_intersection() {
use crate::scene::ChangeKind;
let bytes = include_bytes!("../../tests/fixtures/dim_assoc/viewport_associations.dxf").to_vec();
let mut scene = Scene::new();
scene.document = crate::io::load_bytes("viewport_associations.dxf", bytes).unwrap();
scene.notify_viewport_changed(Handle::new(0x200));
// The cubic's tangent from (0,300) touches at u=.75, C=(90,283.125).
let EntityType::Spline(spline) = scene.document.get_entity(Handle::new(0x104)).unwrap() else {
panic!()
};
let curve = crate::entities::spline::nurbs3(spline).unwrap();
let c = DVec3::from_array(curve.point_at_knot(0.75));
let tangent = DVec3::from_array(curve.derivative_at_knot(0.75));
assert!((c - DVec3::new(0.0, 300.0, 0.0)).cross(tangent).length() < 1e-8);
let measured = |scene: &Scene, handle| {
let EntityType::Dimension(d) = scene.document.get_entity(Handle::new(handle)).unwrap()
else {
panic!()
};
d.measurement()
* dim_override::real(&d.base().common.extended_data, dim_override::DIMLFAC)
.unwrap_or(1.0)
.abs()
};
for (handle, expected) in [
(0x201, 100.0),
(0x202, 50.0),
(0x203, 35.355339),
(0x204, 141.421356),
(0x205, 90.0_f64.hypot(16.875)),
(0x206, 80.0),
(0x207, 60.0),
] {
assert!(
(measured(&scene, handle) - expected).abs() < 1e-4,
"{handle:X}: {}",
measured(&scene, handle)
);
}
let Some(EntityType::Line(line)) = scene.document.get_entity_mut(Handle::new(0x107)) else {
panic!()
};
line.start.x = 80.0;
line.end.x = 80.0;
scene.bump_entities(&[(Handle::new(0x107), ChangeKind::Modified)]);
assert!((measured(&scene, 0x207) - 80.0).abs() < 1e-5);
let before = scene
.document
.get_entity(Handle::new(0x205))
.unwrap()
.clone();
for types in [(1, 255), (8, 9)] {
let codec::objects::ObjectType::Associative(object) =
scene.document.objects.get_mut(&Handle::new(0x304)).unwrap()
else {
panic!()
};
let codec::objects::AssociativeData::DimensionAssociation(assoc) = &mut object.data else {
panic!()
};
assoc.references[0][0].osnap_type = types.0;
assoc.references[1][0].osnap_type = types.1;
scene.notify_viewport_changed(Handle::new(0x200));
assert_eq!(
scene.document.get_entity(Handle::new(0x205)).unwrap(),
&before
);
assert!(scene
.dimension_association_status(Handle::new(0x205))
.iter()
.any(|(_, s)| *s == crate::scene::ReferenceStatus::Unresolved));
}
}
#[test]
fn viewport_dimension_nearest_parameter_survives_source_stretch() {
use crate::scene::ChangeKind;
let (mut app, line, frame) = fixture();
let i = app.active_tab;
let _ = app.dispatch_command("DIMALIGNED");
point(&mut app, frame, line, DVec3::ZERO);
let mut near = hit(frame, line, DVec3::X * 75.0);
near.snap_type = SnapType::Nearest;
assert!(app.record_accepted_snap(i, Some(near), Some(frame), near.world));
let result = app.tabs[i]
.active_cmd
.as_mut()
.unwrap()
.on_point(near.world);
app.sync_dimension_snaps(i);
let _ = app.apply_cmd_result(result);
let _ = app.feed_command(StepInput::Point(DVec3::new(55.0, 80.0, 0.0)));
assert!((displayed(&app.tabs[i].scene.document) - 75.0).abs() < 1e-5);
let Some(EntityType::Line(source)) = app.tabs[i].scene.document.get_entity_mut(line) else {
panic!()
};
source.end.x = 200.0;
app.tabs[i]
.scene
.bump_entities(&[(line, ChangeKind::Modified)]);
assert!((displayed(&app.tabs[i].scene.document) - 150.0).abs() < 1e-5);
}
#[test]
fn viewport_dimension_nested_block_path_survives_edit_and_copy() {
use crate::scene::ChangeKind;
use codec::entities::Insert;
use codec::tables::BlockRecord;
let (mut app, _, frame) = fixture();
let i = app.active_tab;
let scene = &mut app.tabs[i].scene;
let mut inner = BlockRecord::new("Inner");
inner.handle = scene.document.allocate_handle();
let inner_h = inner.handle;
scene.document.block_records.add(inner).unwrap();
let mut leaf = Line::from_points(Vector3::ZERO, Vector3::new(20.0, 0.0, 0.0));
leaf.common.owner_handle = inner_h;
let leaf = scene.document.add_entity(EntityType::Line(leaf)).unwrap();
let mut outer = BlockRecord::new("Outer");
outer.handle = scene.document.allocate_handle();
let outer_h = outer.handle;
scene.document.block_records.add(outer).unwrap();
let mut nested = Insert::new("Inner", Vector3::new(30.0, 0.0, 0.0));
nested.common.owner_handle = outer_h;
nested.set_x_scale(3.0);
nested.set_y_scale(3.0);
nested.rotation = 0.4;
let nested = scene
.document
.add_entity(EntityType::Insert(nested))
.unwrap();
scene.set_current_layout("Model".into());
let mut root = Insert::new("Outer", Vector3::new(20.0, 50.0, 0.0));
root.set_x_scale(2.0);
root.set_y_scale(2.0);
root.rotation = 0.3;
let root = scene.add_entity(EntityType::Insert(root));
scene.set_current_layout("Dimensions".into());
let transform =
crate::scene::dimension_assoc_chain::walk_chain(&scene.document, &[root, nested, leaf])
.unwrap()
.transform;
let model = |x| {
let v = transform.apply(Vector3::new(x, 0.0, 0.0));
DVec3::new(v.x, v.y, v.z)
};
let _ = app.dispatch_command("DIMALIGNED");
point(&mut app, frame, root, model(0.0));
let accepted = app.accepted_snaps()[0].source.as_ref().unwrap();
assert_eq!(accepted.source.handle, leaf);
assert_eq!(accepted.block_path, vec![root, nested]);
point(&mut app, frame, root, model(20.0));
let _ = app.feed_command(StepInput::Point(DVec3::new(90.0, 90.0, 0.0)));
let handle = dimension(&app.tabs[i].scene.document).base().common.handle;
assert!((displayed(&app.tabs[i].scene.document) - 120.0).abs() < 1e-5);
let Some(EntityType::Line(source)) = app.tabs[i].scene.document.get_entity_mut(leaf) else {
panic!()
};
source.end.x = 40.0;
app.tabs[i]
.scene
.bump_entities(&[(leaf, ChangeKind::Modified)]);
assert!((displayed(&app.tabs[i].scene.document) - 240.0).abs() < 1e-5);
let copied = app.tabs[i].scene.copy_entities(
&[handle],
&crate::command::EntityTransform::Translate(DVec3::new(10.0, 10.0, 0.0)),
);
assert_eq!(copied.len(), 1);
assert!(app.tabs[i].scene.dimension_association(copied[0]).is_none());
assert!(app.tabs[i].scene.dimension_association(handle).is_some());
}
#[test]
fn viewport_dimension_hidden_border_and_lock_keep_content_available() {
let (mut app, _, frame) = fixture();
let scene = &mut app.tabs[app.active_tab].scene;
let mut layer = codec::tables::Layer::new("Viewport frames");
layer.flags.off = true;
scene.document.layers.add(layer).unwrap();
let Some(EntityType::Viewport(vp)) = scene.document.get_entity_mut(frame.viewport) else {
panic!()
};
vp.common.layer = "Viewport frames".into();
vp.status.locked = true;
let pick = scene
.dimension_pick_through_viewport(DVec3::new(55.0, 50.0, 0.0), 0.2)
.unwrap();
assert!(pick.frame.locked);
assert_eq!(pick.frame.viewport, frame.viewport);
}
#[test]
fn viewport_dimension_perpendicular_uses_transformed_geometry() {
use crate::scene::dimension_assoc::{resolve_reference_chain, ReferenceStatus};
use codec::entities::Insert;
use codec::objects::AssocDimensionReference;
use codec::tables::BlockRecord;
let mut scene = Scene::new();
let mut block = BlockRecord::new("Stretched");
block.handle = scene.document.allocate_handle();
let owner = block.handle;
scene.document.block_records.add(block).unwrap();
let mut source = Line::from_points(Vector3::ZERO, Vector3::new(10.0, 10.0, 0.0));
source.common.owner_handle = owner;
let source = scene.document.add_entity(EntityType::Line(source)).unwrap();
let mut insert = Insert::new("Stretched", Vector3::ZERO);
insert.set_x_scale(2.0);
let root = scene.add_entity(EntityType::Insert(insert));
let reference = AssocDimensionReference {
xrefs: vec![root, source],
osnap_type: 8,
osnap_point: Vector3::new(4.0, 2.0, 0.0),
..Default::default()
};
let result =
resolve_reference_chain(&scene, &reference, Some(Vector3::new(0.0, 10.0, 0.0))).unwrap();
assert!(
(result.model.x - 4.0).abs() < 1e-8 && (result.model.y - 2.0).abs() < 1e-8,
"{result:?}"
);
let unrelated = scene.add_entity(EntityType::Line(Line::from_points(
Vector3::ZERO,
Vector3::UNIT_X,
)));
let invalid = AssocDimensionReference {
xrefs: vec![root, unrelated],
..reference
};
assert!(matches!(
resolve_reference_chain(&scene, &invalid, None),
Err(ReferenceStatus::Unresolved)
));
}
#[test]
fn viewport_dimension_refresh_preserves_explicit_overrides() {
use codec::xdata::XDataValue;
let (mut app, line, frame) = fixture();
let i = app.active_tab;
finish_aligned(&mut app, line, frame);
let handle = dimension(&app.tabs[i].scene.document).base().common.handle;
let entity = app.tabs[i].scene.document.get_entity_mut(handle).unwrap();
dim_override::set_on_entity(
entity,
dim_override::DIMLFAC,
Some(XDataValue::Real(-254.0)),
);
dim_override::set_on_entity(entity, dim_override::DIMTOL, Some(XDataValue::Integer16(1)));
dim_override::set_on_entity(entity, dim_override::DIMTP, Some(XDataValue::Real(0.05)));
dim_override::set_on_entity(entity, dim_override::DIMTM, Some(XDataValue::Real(0.02)));
if let EntityType::Dimension(d) = entity {
crate::entities::dimension::set_dimension_text_override(d.base_mut(), Some("L=<>".into()));
}
let expected = displayed(&app.tabs[i].scene.document);
let Some(EntityType::Viewport(vp)) = app.tabs[i].scene.document.get_entity_mut(frame.viewport)
else {
panic!()
};
vp.view_height = 500.0;
vp.custom_scale = 0.2;
app.tabs[i].scene.notify_viewport_changed(frame.viewport);
let d = dimension(&app.tabs[i].scene.document);
assert!(
(displayed(&app.tabs[i].scene.document) - expected).abs() < 1e-5,
"{} vs {expected}",
displayed(&app.tabs[i].scene.document)
);
assert_eq!(
crate::entities::dimension::dimension_text_override(d.base()),
Some("L=<>")
);
assert_eq!(
dim_override::real(&d.base().common.extended_data, dim_override::DIMTP),
Some(0.05)
);
assert_eq!(
dim_override::real(&d.base().common.extended_data, dim_override::DIMTM),
Some(0.02)
);
}
#[test]
fn viewport_dimension_block_center_keeps_the_circle_source() {
use codec::entities::Insert;
use codec::tables::BlockRecord;
let (mut app, _, frame) = fixture();
let i = app.active_tab;
let scene = &mut app.tabs[i].scene;
let mut block = BlockRecord::new("Circle block");
block.handle = scene.document.allocate_handle();
let owner = block.handle;
scene.document.block_records.add(block).unwrap();
let mut circle = Circle::new();
circle.radius = 10.0;
circle.common.owner_handle = owner;
let circle = scene
.document
.add_entity(EntityType::Circle(circle))
.unwrap();
// This line is closer to the circle center than its circumference is.
let mut line = Line::from_points(Vector3::new(-2.0, 1.0, 0.0), Vector3::new(2.0, 1.0, 0.0));
line.common.owner_handle = owner;
scene.document.add_entity(EntityType::Line(line)).unwrap();
scene.set_current_layout("Model".into());
let root = scene.add_entity(EntityType::Insert(Insert::new(
"Circle block",
Vector3::new(40.0, 20.0, 0.0),
)));
scene.set_current_layout("Dimensions".into());
let mut center = hit(frame, root, DVec3::new(40.0, 20.0, 0.0));
center.snap_type = SnapType::Center;
assert!(app.record_accepted_snap(i, Some(center), Some(frame), center.world));
let source = app
.accepted_snaps()
.last()
.unwrap()
.source
.as_ref()
.unwrap();
assert_eq!(source.source.handle, circle);
assert_eq!(source.block_path, vec![root]);
}
#[test]
fn viewport_dimension_angular_object_picks_follow_arc_features() {
use crate::scene::{ChangeKind, ReferenceStatus};
use codec::entities::{Arc, LwPolyline};
use codec::types::Vector2;
for bulged in [false, true] {
let (mut app, _, frame) = fixture();
let i = app.active_tab;
app.tabs[i].scene.set_current_layout("Model".into());
let entity = if bulged {
let mut poly =
LwPolyline::from_points(vec![Vector2::new(100.0, 200.0), Vector2::new(0.0, 300.0)]);
poly.vertices[0].bulge = (std::f64::consts::PI / 8.0).tan();
EntityType::LwPolyline(poly)
} else {
let mut arc = Arc::new();
arc.center = Vector3::new(0.0, 200.0, 0.0);
arc.radius = 100.0;
arc.start_angle = 0.0;
arc.end_angle = std::f64::consts::FRAC_PI_2;
EntityType::Arc(arc)
};
let source = app.tabs[i].scene.add_entity(entity);
app.tabs[i].scene.set_current_layout("Dimensions".into());
let _ = app.dispatch_command("DIMANGULAR");
let model = DVec3::new(100.0 / 2.0_f64.sqrt(), 200.0 + 100.0 / 2.0_f64.sqrt(), 0.0);
let result = app
.try_dimension_viewport_entity_pick(i, frame.model_to_paper(model), 0.5)
.unwrap();
let _ = app.apply_cmd_result(result);
assert_eq!(app.accepted_snaps().len(), 3);
let _ = app.feed_command(StepInput::Point(DVec3::new(65.0, 85.0, 0.0)));
assert!((displayed(&app.tabs[i].scene.document) - 90.0).abs() < 1e-5);
let dim = dimension(&app.tabs[i].scene.document).base().common.handle;
app.tabs[i].scene.notify_viewport_changed(frame.viewport);
assert!((displayed(&app.tabs[i].scene.document) - 90.0).abs() < 1e-5);
let status = app.tabs[i].scene.dimension_association_status(dim);
assert_eq!(status.len(), 3);
assert!(
status.iter().all(|(_, s)| *s == ReferenceStatus::Resolved),
"{bulged}: {status:?}"
);
match app.tabs[i].scene.document.get_entity_mut(source).unwrap() {
EntityType::Arc(arc) => arc.end_angle = std::f64::consts::PI / 3.0,
EntityType::LwPolyline(poly) => {
poly.vertices[0].bulge = (std::f64::consts::PI / 12.0).tan()
}
_ => unreachable!(),
}
app.tabs[i]
.scene
.bump_entities(&[(source, ChangeKind::Modified)]);
assert!(
(displayed(&app.tabs[i].scene.document) - 60.0).abs() < 1e-5,
"{bulged}: {}",
displayed(&app.tabs[i].scene.document)
);
}
}
#[test]
fn viewport_dimension_quadrant_keeps_its_feature_after_translation() {
use crate::scene::ChangeKind;
let (mut app, line, frame) = fixture();
let i = app.active_tab;
app.tabs[i].scene.set_current_layout("Model".into());
let mut circle = Circle::new();
circle.radius = 100.0;
let source = app.tabs[i].scene.add_entity(EntityType::Circle(circle));
app.tabs[i].scene.set_current_layout("Dimensions".into());
let _ = app.dispatch_command("DIMALIGNED");
point(&mut app, frame, line, DVec3::ZERO);
let mut quadrant = hit(frame, source, DVec3::X * 100.0);
quadrant.snap_type = SnapType::Quadrant;
assert!(app.record_accepted_snap(i, Some(quadrant), Some(frame), quadrant.world));
let result = app.tabs[i]
.active_cmd
.as_mut()
.unwrap()
.on_point(quadrant.world);
app.sync_dimension_snaps(i);
let _ = app.apply_cmd_result(result);
let _ = app.feed_command(StepInput::Point(DVec3::new(60.0, 80.0, 0.0)));
let EntityType::Circle(circle) = app.tabs[i].scene.document.get_entity_mut(source).unwrap()
else {
panic!()
};
circle.center.x = 300.0;
app.tabs[i]
.scene
.bump_entities(&[(source, ChangeKind::Modified)]);
assert!((displayed(&app.tabs[i].scene.document) - 400.0).abs() < 1e-5);
}
fn start_centerline_dimension(
command: &str,
paper_first: bool,
paper_kind: SnapType,
factor: f64,
) -> (OpenCADStudio, Handle, Handle, ViewportFrame) {
let (mut app, model, frame) = fixture();
let i = app.active_tab;
let paper = app.tabs[i]
.scene
.add_entity(EntityType::Line(Line::from_points(
Vector3::new(55.0, 40.0, 0.0),
Vector3::new(55.0, 60.0, 0.0),
)));
let mut style = codec::tables::DimStyle::new("CenterlineUnits");
style.dimlfac = factor;
app.tabs[i].scene.document.dim_styles.add(style).unwrap();
app.tabs[i].scene.document.header.current_dimstyle_name = "CenterlineUnits".into();
let _ = app.dispatch_command(command);
let mut paper_hit = hit(frame, paper, DVec3::new(55.0, 50.0, 0.0));
paper_hit.world = DVec3::new(55.0, 50.0, 0.0);
paper_hit.model_point = None;
paper_hit.viewport = None;
paper_hit.snap_type = paper_kind;
let mut picks = [
(paper_hit, None),
(hit(frame, model, DVec3::X * 100.0), Some(frame)),
];
if !paper_first {
picks.reverse();
}
for (snap, context) in picks {
assert!(app.record_accepted_snap(i, Some(snap), context, snap.world));
let result = app.tabs[i]
.active_cmd
.as_mut()
.unwrap()
.on_point(snap.world);
app.sync_dimension_snaps(i);
let _ = app.apply_cmd_result(result);
}
(app, model, paper, frame)
}
#[test]
fn viewport_dimension_paper_centerline_uses_viewport_units_in_either_order() {
for command in ["DIMLINEAR", "DIMALIGNED"] {
for paper_first in [true, false] {
let (mut app, _, paper, _) =
start_centerline_dimension(command, paper_first, SnapType::Midpoint, 25.4);
let paper_snap = app
.accepted_snaps()
.iter()
.find(|s| s.viewport.is_none())
.unwrap();
assert_eq!(paper_snap.source.as_ref().unwrap().source.handle, paper);
assert_eq!(paper_snap.paper_point, paper_snap.model_point);
let _ = app.feed_command(StepInput::Point(DVec3::new(60.0, 80.0, 0.0)));
let doc = &app.tabs[app.active_tab].scene.document;
assert!(
(displayed(doc) - 1270.0).abs() < 1e-3,
"{command}: {}",
displayed(doc)
);
app.undo_steps(1);
app.redo_steps(1);
assert!((displayed(&app.tabs[app.active_tab].scene.document) - 1270.0).abs() < 1e-3);
}
}
}
#[test]
fn viewport_dimension_paper_centerline_tracks_each_owning_space() {
use crate::scene::{ChangeKind, ReferenceStatus};
for (paper_first, kind) in [(true, SnapType::Midpoint), (false, SnapType::Perpendicular)] {
let (mut app, model, paper, frame) =
start_centerline_dimension("DIMALIGNED", paper_first, kind, 1.0);
let i = app.active_tab;
let _ = app.feed_command(StepInput::Point(DVec3::new(60.0, 80.0, 0.0)));
let dim = dimension(&app.tabs[i].scene.document).base().common.handle;
assert!((displayed(&app.tabs[i].scene.document) - 50.0).abs() < 1e-4);
let assoc = app.tabs[i].scene.dimension_association(dim).unwrap();
assert!(assoc.trans_space);
assert!(assoc
.references
.iter()
.flatten()
.any(|r| r.xrefs == vec![paper]));
assert!(assoc
.references
.iter()
.flatten()
.any(|r| r.xrefs == vec![frame.viewport, model]));
assert!(app.tabs[i]
.scene
.dimension_association_status(dim)
.iter()
.all(|(_, s)| *s == ReferenceStatus::Resolved));
let pending = app.begin_undo(i, "Move paper centerline", 1, true).unwrap();
let before = app.tabs[i].scene.document.get_entity_arc(paper);
app.tabs[i].scene.record_undo_before(paper, before);
let EntityType::Line(line) = app.tabs[i].scene.document.get_entity_mut(paper).unwrap()
else {
panic!()
};
line.start.x = 56.0;
line.end.x = 56.0;
app.tabs[i]
.scene
.bump_entities(&[(paper, ChangeKind::Modified)]);
app.commit_undo_delta(i, pending);
assert!((displayed(&app.tabs[i].scene.document) - 40.0).abs() < 1e-4);
app.undo_steps(1);
assert!((displayed(&app.tabs[i].scene.document) - 50.0).abs() < 1e-4);
app.redo_steps(1);
let EntityType::Viewport(vp) = app.tabs[i]
.scene
.document
.get_entity_mut(frame.viewport)
.unwrap()
else {
panic!()
};
vp.view_height = 500.0;
vp.custom_scale = 0.2;
app.tabs[i].scene.notify_viewport_changed(frame.viewport);
// Model endpoint now appears at paper x=70; paper centerline stays x=56.
assert!((displayed(&app.tabs[i].scene.document) - 70.0).abs() < 1e-4);
let EntityType::Viewport(vp) = app.tabs[i]
.scene
.document
.get_entity_mut(frame.viewport)
.unwrap()
else {
panic!()
};
vp.view_target.x = 20.0;
app.tabs[i].scene.notify_viewport_changed(frame.viewport);
assert!((displayed(&app.tabs[i].scene.document) - 50.0).abs() < 1e-4);
let EntityType::Line(line) = app.tabs[i].scene.document.get_entity_mut(model).unwrap()
else {
panic!()
};
line.end.x = 160.0;
app.tabs[i]
.scene
.bump_entities(&[(model, ChangeKind::Modified)]);
assert!((displayed(&app.tabs[i].scene.document) - 110.0).abs() < 1e-4);
for ext in ["dxf", "dwg"] {
let doc = &app.tabs[i].scene.document;
let bytes = crate::io::save_to_bytes(doc, ext, doc.version).unwrap();
let mut scene = Scene::new();
scene.document = crate::io::load_bytes(&format!("centerline.{ext}"), bytes).unwrap();
scene.notify_viewport_changed(frame.viewport);
assert!((displayed(&scene.document) - 110.0).abs() < 1e-4, "{ext}");
assert!(scene
.dimension_association_status(dim)
.iter()
.all(|(_, s)| *s == ReferenceStatus::Resolved));
}
let before = app.tabs[i].scene.document.get_entity(dim).unwrap().clone();
let model_owner = app.tabs[i]
.scene
.document
.get_entity(model)
.unwrap()
.common()
.owner_handle;
app.tabs[i]
.scene
.document
.get_entity_mut(paper)
.unwrap()
.common_mut()
.owner_handle = model_owner;
app.tabs[i].scene.notify_viewport_changed(frame.viewport);
assert_eq!(app.tabs[i].scene.document.get_entity(dim).unwrap(), &before);
assert!(app.tabs[i]
.scene
.dimension_association_status(dim)
.iter()
.any(|(_, s)| *s == ReferenceStatus::Unresolved));
}
}
#[test]
fn viewport_dimension_invalid_paths_leave_no_partial_association() {
use crate::scene::viewport_ref::SnapSourceRef;
use codec::entities::{DimensionAligned, Insert};
use codec::tables::BlockRecord;
let (mut app, model, frame) = fixture();
let scene = &mut app.tabs[app.active_tab].scene;
let mut block = BlockRecord::new("Collapsed");
block.handle = scene.document.allocate_handle();
let owner = block.handle;
scene.document.block_records.add(block).unwrap();
let mut leaf = Line::from_points(Vector3::ZERO, Vector3::UNIT_X);
leaf.common.owner_handle = owner;
let leaf = scene.document.add_entity(EntityType::Line(leaf)).unwrap();
scene.set_current_layout("Model".into());
let mut insert = Insert::new("Collapsed", Vector3::ZERO);
insert.set_x_scale(0.0);
insert.set_y_scale(0.0);
let root = scene.add_entity(EntityType::Insert(insert));
scene.set_current_layout("Dimensions".into());
let paper = scene.add_entity(EntityType::Line(Line::from_points(
Vector3::new(55.0, 50.0, 0.0),
Vector3::new(55.0, 60.0, 0.0),
)));
for (source, path) in [(model, vec![Handle::new(u64::MAX)]), (leaf, vec![root])] {
let dim = scene.add_entity(EntityType::Dimension(Dimension::Aligned(
DimensionAligned::new(Vector3::new(50.0, 50.0, 0.0), Vector3::new(55.0, 50.0, 0.0)),
)));
let invalid = AcceptedSnap {
paper_point: DVec3::new(50.0, 50.0, 0.0),
model_point: DVec3::ZERO,
viewport: Some(frame.viewport),
frame: Some(frame),
source: Some(SnapSourceRef {
source: DimensionAssociationSource::inferred(source),
block_path: path,
snap_type: SnapType::Endpoint,
intersection: None,
}),
};
let mut valid = AcceptedSnap::free(DVec3::new(55.0, 50.0, 0.0));
valid.source = Some(SnapSourceRef {
source: DimensionAssociationSource::inferred(paper),
block_path: Vec::new(),
snap_type: SnapType::Endpoint,
intersection: None,
});
scene.attach_viewport_dimension_association(dim, &[Some(invalid), Some(valid)]);
let association = scene.dimension_association(dim).unwrap();
assert!(!association.trans_space);
assert!(association.references[0].is_empty());
assert_eq!(association.references[1][0].xrefs, vec![paper]);
assert_eq!(scene.dimension_association_sources(dim), vec![paper]);
assert_eq!(
scene.dimension_association_status(dim),
vec![(1, crate::scene::ReferenceStatus::Resolved)]
);
}
}