forked from 1b-agent/opencadstudio-upstream
Resolve viewport dimension features and preserve valid measurements on refresh
This commit is contained in:
1 parent
7f018ab0f1
commit
64ce0b2e8c
11 files changed
+1680
-344
No files matched your search
+14
-17
@@ -208,7 +208,9 @@ impl OpenCADStudio {
|
||||
i: usize,
|
||||
dimension: Handle,
|
||||
) -> Vec<Option<AcceptedSnap>> {
|
||||
let slots = self.tabs[i].scene.dimension_association_slot_points(dimension);
|
||||
let slots = self.tabs[i]
|
||||
.scene
|
||||
.dimension_association_slot_points(dimension);
|
||||
if slots.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
@@ -220,7 +222,12 @@ impl OpenCADStudio {
|
||||
let snap = measuring.get(index)?.clone();
|
||||
let frame = snap.frame?;
|
||||
let slot = DVec3::new(slot.x, slot.y, slot.z);
|
||||
if snap.paper_point.truncate().distance_squared(slot.truncate()) <= 1e-12 {
|
||||
if snap
|
||||
.paper_point
|
||||
.truncate()
|
||||
.distance_squared(slot.truncate())
|
||||
<= 1e-12
|
||||
{
|
||||
return Some(snap);
|
||||
}
|
||||
Some(AcceptedSnap {
|
||||
@@ -235,9 +242,8 @@ impl OpenCADStudio {
|
||||
/// Record the viewport association for a dimension that has just been
|
||||
/// committed while measuring through a layout viewport.
|
||||
///
|
||||
/// Announces the whole chain — the viewport, every INSERT on the block
|
||||
/// path and the source entity — so the dependency index picks the new
|
||||
/// association up immediately.
|
||||
/// The stored reference chains establish dependencies. Only the new
|
||||
/// dimension needs a render update; its sources have not changed.
|
||||
pub(crate) fn attach_viewport_dimension_association(&mut self, i: usize, dimension: Handle) {
|
||||
let snaps = self.viewport_dimension_snaps(i, dimension);
|
||||
if snaps.iter().flatten().all(|snap| snap.source.is_none()) {
|
||||
@@ -246,17 +252,8 @@ impl OpenCADStudio {
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.attach_viewport_dimension_association(dimension, &snaps);
|
||||
let mut changes = vec![(dimension, crate::scene::ChangeKind::Modified)];
|
||||
changes.extend(
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.dimension_association_sources(dimension)
|
||||
.into_iter()
|
||||
.map(|handle| (handle, crate::scene::ChangeKind::Modified)),
|
||||
);
|
||||
changes.sort_by_key(|(handle, _)| handle.value());
|
||||
changes.dedup_by_key(|(handle, _)| handle.value());
|
||||
self.tabs[i].scene.bump_entities(&changes);
|
||||
self.tabs[i]
|
||||
.scene
|
||||
.bump_entities(&[(dimension, crate::scene::ChangeKind::Modified)]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1277,6 +1277,24 @@ impl OpenCADStudio {
|
||||
&self.tabs[i].scene.document,
|
||||
d.base().common.handle,
|
||||
);
|
||||
let statuses = self.tabs[i].scene.dimension_association_status(d.base().common.handle);
|
||||
let slots = self.tabs[i].scene.dimension_association_slot_points(d.base().common.handle).len();
|
||||
let status = if statuses.is_empty() {
|
||||
t!("Nonassociative")
|
||||
} else if statuses.iter().any(|(_, status)| matches!(status, crate::scene::ReferenceStatus::Broken(_))) {
|
||||
t!("Broken reference")
|
||||
} else if statuses.iter().any(|(_, status)| matches!(status, crate::scene::ReferenceStatus::Unresolved)) {
|
||||
t!("Unresolved reference")
|
||||
} else if statuses.len() < slots {
|
||||
t!("Partially associated")
|
||||
} else {
|
||||
t!("Associated")
|
||||
};
|
||||
general.props.push(crate::scene::model::object::Property {
|
||||
label: t!("Association status").into_owned(),
|
||||
field: "association_status",
|
||||
value: crate::scene::model::object::PropValue::ReadOnly(status.into_owned()),
|
||||
});
|
||||
general.props.push(crate::scene::model::object::Property {
|
||||
label: t!("Associative").into_owned(),
|
||||
field: "associative",
|
||||
|
||||
@@ -137,35 +137,73 @@ impl OpenCADStudio {
|
||||
true
|
||||
}
|
||||
|
||||
/// Replace a snap source that is a block INSTANCE with the entity inside
|
||||
/// it that actually owns the snapped feature, plus the INSERT path.
|
||||
///
|
||||
/// The snap engine can only report what the wire carries, and the
|
||||
/// renderer stamps every wire of an expanded block with the *top-level
|
||||
/// INSERT's* handle — nested inserts included. `SnapSourceRef` promises
|
||||
/// the innermost entity in `source.handle` and the instance chain in
|
||||
/// `block_path`, so the descent happens here, once, at accept time (not
|
||||
/// per pointer move).
|
||||
///
|
||||
/// When the descent cannot identify the inner entity the source stays the
|
||||
/// INSERT with an empty path. See
|
||||
/// [`crate::scene::Scene::resolve_block_snap_source`] for exactly when
|
||||
/// that happens; the resulting association is coarser (it tracks the whole
|
||||
/// block instance) but never wrong.
|
||||
/// Resolve the instance path once, when a feature is accepted. Failure
|
||||
/// drops source identity so a failed descent cannot attach to the insert's
|
||||
/// origin. Ambiguous intersections within one batched block remain unlinked.
|
||||
fn resolve_source_block_path(&self, tab: usize, accepted: &mut AcceptedSnap) {
|
||||
let resolve = |source: &mut crate::scene::viewport_ref::SnapSourceRef| {
|
||||
if !source.block_path.is_empty() {
|
||||
return true;
|
||||
}
|
||||
if !matches!(
|
||||
self.tabs[tab]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(source.source.handle),
|
||||
Some(acadrust::EntityType::Insert(_))
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if source.snap_type == SnapType::Insertion {
|
||||
let Some(acadrust::EntityType::Insert(insert)) = self.tabs[tab]
|
||||
.scene
|
||||
.document
|
||||
.get_entity(source.source.handle)
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
let origin = glam::DVec3::new(
|
||||
insert.insert_point.x,
|
||||
insert.insert_point.y,
|
||||
insert.insert_point.z,
|
||||
);
|
||||
return origin.distance(accepted.model_point) < 1e-5;
|
||||
}
|
||||
let Some((entity, path)) = self.tabs[tab].scene.resolve_measurable_feature(
|
||||
source.source.handle,
|
||||
accepted.model_point,
|
||||
accepted.viewport,
|
||||
Some(source.snap_type),
|
||||
) else {
|
||||
return false;
|
||||
};
|
||||
let distance = crate::scene::viewport_dim_seam::feature_pick_distance(
|
||||
&entity,
|
||||
accepted.model_point,
|
||||
Some(source.snap_type),
|
||||
);
|
||||
if !distance.is_some_and(|distance| distance < 1e-5) {
|
||||
return false;
|
||||
}
|
||||
source.source =
|
||||
crate::command::DimensionAssociationSource::inferred(entity.common().handle);
|
||||
source.block_path = path;
|
||||
true
|
||||
};
|
||||
let Some(source) = accepted.source.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if !source.block_path.is_empty() {
|
||||
if !resolve(source) {
|
||||
accepted.source = None;
|
||||
return;
|
||||
}
|
||||
if let Some((entity, path)) = self.tabs[tab]
|
||||
.scene
|
||||
.resolve_block_snap_source(source.source.handle, accepted.model_point)
|
||||
{
|
||||
source.source =
|
||||
crate::command::DimensionAssociationSource::inferred(entity);
|
||||
source.block_path = path;
|
||||
if let Some(other) = source.intersection.as_mut() {
|
||||
if !resolve(other)
|
||||
|| (other.source.handle == source.source.handle
|
||||
&& other.block_path == source.block_path)
|
||||
{
|
||||
source.intersection = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -239,11 +239,20 @@ fn viewport_dimension_linear_aligned_and_radial_object_picks() {
|
||||
#[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, line, DVec3::Y * 100.0);
|
||||
point(&mut app, frame, vertical, DVec3::Y * 100.0);
|
||||
let _ = app.feed_command(StepInput::Point(DVec3::new(70.0, 70.0, 0.0)));
|
||||
let doc = &app.tabs[app.active_tab].scene.document;
|
||||
assert!((dimension(doc).measurement() - 90.0).abs() < 1e-5);
|
||||
@@ -377,3 +386,712 @@ fn viewport_dimension_exploded_commit_uses_compensated_text_and_undo() {
|
||||
.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 acadrust::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 acadrust::objects::AssocDimensionReference;
|
||||
let mut spline = acadrust::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 the original local regression drawing"]
|
||||
fn viewport_dimension_original_drawing_keeps_unchanged_measurements() {
|
||||
use crate::scene::ChangeKind;
|
||||
let path = std::env::var("OPENCAD_VIEWPORT_REGRESSION_DXF").expect("regression drawing path");
|
||||
let doc = acadrust::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 acadrust::objects::ObjectType::Associative(object) =
|
||||
scene.document.objects.get_mut(&Handle::new(0x304)).unwrap()
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
let acadrust::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 acadrust::entities::Insert;
|
||||
use acadrust::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::block_transform(&scene.document, &[root, nested]);
|
||||
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 = acadrust::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 acadrust::entities::Insert;
|
||||
use acadrust::objects::AssocDimensionReference;
|
||||
use acadrust::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 acadrust::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 acadrust::entities::Insert;
|
||||
use acadrust::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 acadrust::entities::{Arc, LwPolyline};
|
||||
use acadrust::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);
|
||||
}
|
||||
+351
-78
@@ -14,10 +14,8 @@ use std::f64::consts::TAU;
|
||||
|
||||
use crate::command::DimensionAssociationSource;
|
||||
|
||||
use super::dimension_assoc_chain::{
|
||||
self as chain, ChainError, FeatureContext, ReferenceChain,
|
||||
};
|
||||
use super::viewport_ref::{AcceptedSnap, ViewportFrame};
|
||||
use super::dimension_assoc_chain::{self as chain, ChainError, FeatureContext, ReferenceChain};
|
||||
use super::viewport_ref::{AcceptedSnap, MeasurementScale, ViewportFrame};
|
||||
use super::{ChangeKind, Scene};
|
||||
|
||||
pub(crate) const POLYLINE_ARC_CENTER_MARKER: i32 = -4;
|
||||
@@ -404,8 +402,7 @@ impl ChainMap {
|
||||
pub(crate) fn model_to_space(&self, point: Vector3) -> Vector3 {
|
||||
match &self.frame {
|
||||
Some(frame) => {
|
||||
let mapped =
|
||||
frame.model_to_paper(glam::DVec3::new(point.x, point.y, point.z));
|
||||
let mapped = frame.model_to_paper(glam::DVec3::new(point.x, point.y, point.z));
|
||||
Vector3::new(mapped.x, mapped.y, mapped.z)
|
||||
}
|
||||
None => point,
|
||||
@@ -416,8 +413,7 @@ impl ChainMap {
|
||||
pub(crate) fn space_to_model(&self, point: Vector3) -> Vector3 {
|
||||
match &self.frame {
|
||||
Some(frame) => {
|
||||
let mapped =
|
||||
frame.paper_to_model(glam::DVec3::new(point.x, point.y, point.z));
|
||||
let mapped = frame.paper_to_model(glam::DVec3::new(point.x, point.y, point.z));
|
||||
Vector3::new(mapped.x, mapped.y, mapped.z)
|
||||
}
|
||||
None => point,
|
||||
@@ -462,11 +458,18 @@ impl ChainMap {
|
||||
/// Only the plane moves: the centre, radius and angles are coordinates
|
||||
/// *within* the plane, so they stay as they are and every point the
|
||||
/// geometry produces comes out in the dimension's space already.
|
||||
fn map_radial(&self, radial: RadialSourceGeometry) -> RadialSourceGeometry {
|
||||
RadialSourceGeometry {
|
||||
plane: self.map_plane(radial.plane),
|
||||
..radial
|
||||
fn map_radial(&self, radial: RadialSourceGeometry) -> Option<RadialSourceGeometry> {
|
||||
let plane = self.map_plane(radial.plane);
|
||||
let x = glam::DVec3::from_array(plane.x_axis);
|
||||
let y = glam::DVec3::from_array(plane.y_axis);
|
||||
let magnitude = x.length_squared().max(y.length_squared());
|
||||
if magnitude < 1e-24
|
||||
|| (x.length_squared() - y.length_squared()).abs() > magnitude * 1e-9
|
||||
|| x.dot(y).abs() > magnitude * 1e-9
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(RadialSourceGeometry { plane, ..radial })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -500,7 +503,9 @@ pub enum ReferenceStatus {
|
||||
fn chain_map(scene: &Scene, walked: &ReferenceChain) -> ChainMap {
|
||||
ChainMap {
|
||||
transform: walked.transform,
|
||||
frame: walked.viewport.and_then(|handle| scene.viewport_frame(handle)),
|
||||
frame: walked
|
||||
.viewport
|
||||
.and_then(|handle| scene.viewport_frame(handle)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,7 +536,12 @@ fn hint_error(entity: &EntityType, point: Vector3) -> f64 {
|
||||
/// the same drawing. Rather than guess, both readings are mapped down to the
|
||||
/// entity and the one that actually lands on the geometry wins. The point is
|
||||
/// only ever a *hint* — it disambiguates candidates, it is never the answer.
|
||||
fn local_hint(scene: &Scene, map: &ChainMap, entity: &EntityType, stored: Vector3) -> Option<Vector3> {
|
||||
fn local_hint(
|
||||
scene: &Scene,
|
||||
map: &ChainMap,
|
||||
entity: &EntityType,
|
||||
stored: Vector3,
|
||||
) -> Option<Vector3> {
|
||||
let _ = scene;
|
||||
let inverse = chain::invert(&map.transform)?;
|
||||
let mut candidates = vec![inverse.apply(stored)];
|
||||
@@ -558,34 +568,66 @@ pub fn resolve_reference_chain(
|
||||
let walked = match chain::walk_chain(&scene.document, &reference.xrefs) {
|
||||
Ok(walked) => walked,
|
||||
Err(ChainError::Missing(handle)) => return Err(ReferenceStatus::Broken(handle)),
|
||||
Err(ChainError::Empty) => return Err(ReferenceStatus::Unresolved),
|
||||
Err(ChainError::Empty | ChainError::Invalid) => return Err(ReferenceStatus::Unresolved),
|
||||
};
|
||||
let Some(entity) = scene.document.get_entity(walked.entity) else {
|
||||
return Err(ReferenceStatus::Broken(walked.entity));
|
||||
};
|
||||
let map = chain_map(scene, &walked);
|
||||
let inverse = chain::invert(&map.transform);
|
||||
|
||||
let mut local = None;
|
||||
if chain::evaluates(reference.osnap_type) {
|
||||
let hint = local_hint(scene, &map, entity, reference.osnap_point);
|
||||
let from = from_space
|
||||
.zip(inverse)
|
||||
.map(|(point, inverse)| inverse.apply(map.space_to_model(point)));
|
||||
local = chain::feature_point(
|
||||
if walked.viewport.is_some() && map.frame.is_none() {
|
||||
return Err(ReferenceStatus::Unresolved);
|
||||
}
|
||||
let inverse = chain::invert(&map.transform).ok_or(ReferenceStatus::Unresolved)?;
|
||||
if matches!(
|
||||
reference.osnap_type,
|
||||
chain::osnap::INTERSEC | chain::osnap::APPARENT_INT
|
||||
) {
|
||||
match chain::walk_chain(&scene.document, &reference.intersection_objects) {
|
||||
Err(ChainError::Missing(handle)) => return Err(ReferenceStatus::Broken(handle)),
|
||||
Err(_) => return Err(ReferenceStatus::Unresolved),
|
||||
Ok(_) => {}
|
||||
}
|
||||
}
|
||||
let legacy = reference.main_subent_type != 2
|
||||
&& matches!(
|
||||
reference.osnap_type,
|
||||
chain::osnap::NONE | chain::osnap::END | chain::osnap::START_POINT
|
||||
);
|
||||
let local = if reference.osnap_type == chain::osnap::PERP && !walked.block_path.is_empty() {
|
||||
from_space
|
||||
.and_then(|from| {
|
||||
chain::perpendicular_in_model(entity, &map.transform, map.space_to_model(from))
|
||||
})
|
||||
.map(|point| inverse.apply(point))
|
||||
} else if legacy {
|
||||
resolve_local_by_marker(entity, reference)
|
||||
} else if chain::evaluates(reference.osnap_type) {
|
||||
let hint = if reference
|
||||
.osnap_point
|
||||
.x
|
||||
.abs()
|
||||
.max(reference.osnap_point.y.abs())
|
||||
.max(reference.osnap_point.z.abs())
|
||||
< 1e40
|
||||
{
|
||||
local_hint(scene, &map, entity, reference.osnap_point)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let from = from_space.map(|point| inverse.apply(map.space_to_model(point)));
|
||||
chain::feature_point(
|
||||
&scene.document,
|
||||
entity,
|
||||
reference,
|
||||
FeatureContext { hint, from },
|
||||
);
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.ok_or(ReferenceStatus::Unresolved)?;
|
||||
if !local.x.is_finite() || !local.y.is_finite() || !local.z.is_finite() {
|
||||
return Err(ReferenceStatus::Unresolved);
|
||||
}
|
||||
// Every reference — including one whose osnap type we do not evaluate —
|
||||
// falls back to the marker/parameter convention, which is what the
|
||||
// pre-PR3 writer and plain END references use.
|
||||
let local = match local.or_else(|| resolve_local_by_marker(entity, reference)) {
|
||||
Some(local) => local,
|
||||
None => return Err(ReferenceStatus::Unresolved),
|
||||
};
|
||||
let model = map.to_model(local);
|
||||
Ok(ResolvedReference {
|
||||
model,
|
||||
@@ -798,9 +840,11 @@ pub(crate) fn dimension_is_associative(
|
||||
};
|
||||
association.dimension == dimension
|
||||
&& association.associativity != 0
|
||||
&& association.references.iter().flatten().any(|reference| {
|
||||
chain::walk_chain(document, &reference.xrefs).is_ok()
|
||||
})
|
||||
&& association
|
||||
.references
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|reference| chain::walk_chain(document, &reference.xrefs).is_ok())
|
||||
})
|
||||
}
|
||||
|
||||
@@ -827,6 +871,15 @@ pub(crate) fn constraint_from_associative_dimension(
|
||||
};
|
||||
(association.dimension == handle && association.associativity != 0).then_some(association)
|
||||
})?;
|
||||
if association.trans_space
|
||||
|| association
|
||||
.references
|
||||
.iter()
|
||||
.flatten()
|
||||
.any(|r| r.xrefs.len() != 1)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let point = |index: usize| {
|
||||
let reference = association.references.get(index)?.first()?;
|
||||
Some(ParametricRef::point(
|
||||
@@ -1207,8 +1260,7 @@ impl Scene {
|
||||
self.attach_viewport_radial_association(dimension, snaps);
|
||||
return;
|
||||
}
|
||||
let mut references: [Vec<AssocDimensionReference>; 4] =
|
||||
std::array::from_fn(|_| Vec::new());
|
||||
let mut references: [Vec<AssocDimensionReference>; 4] = std::array::from_fn(|_| Vec::new());
|
||||
let mut associativity = 0;
|
||||
let mut trans_space = false;
|
||||
let mut reactor_targets = Vec::new();
|
||||
@@ -1233,25 +1285,41 @@ impl Scene {
|
||||
reactor_targets.extend(xrefs.iter().copied());
|
||||
|
||||
let osnap_type = chain::osnap_type_for(source.snap_type);
|
||||
// An explicitly-acquired sub-entity wins; otherwise fall back to
|
||||
// the same marker inference model-space creation already uses, in
|
||||
// the entity's own coordinates.
|
||||
let (marker, parameter) = match source.source.marker {
|
||||
Some(marker) => (marker, source.source.parameter),
|
||||
None => {
|
||||
let local = Self::point_in_entity_space(
|
||||
&self.document,
|
||||
&source.block_path,
|
||||
dvec3(snap.model_point),
|
||||
);
|
||||
source_reference(entity, local).unwrap_or((0, 0.0))
|
||||
}
|
||||
let local = Self::point_in_entity_space(
|
||||
&self.document,
|
||||
&source.block_path,
|
||||
dvec3(snap.model_point),
|
||||
);
|
||||
let marker_parameter = source
|
||||
.source
|
||||
.marker
|
||||
.map(|marker| (marker, source.source.parameter))
|
||||
.or_else(|| feature_reference(entity, local, source.snap_type));
|
||||
let Some((marker, parameter)) = marker_parameter else {
|
||||
continue;
|
||||
};
|
||||
let mut intersection_objects = Vec::new();
|
||||
if matches!(
|
||||
source.snap_type,
|
||||
crate::snap::SnapType::Intersection | crate::snap::SnapType::ApparentIntersection
|
||||
) {
|
||||
let Some(other) = source.intersection.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
intersection_objects.extend(snap.viewport);
|
||||
intersection_objects.extend(other.block_path.iter().copied());
|
||||
intersection_objects.push(other.source.handle);
|
||||
if chain::walk_chain(&self.document, &intersection_objects).is_err() {
|
||||
continue;
|
||||
}
|
||||
reactor_targets.extend(intersection_objects.iter().copied());
|
||||
}
|
||||
associativity |= 1 << index;
|
||||
references[index].push(AssocDimensionReference {
|
||||
class_name: "AcDbOsnapPointRef".to_string(),
|
||||
osnap_type,
|
||||
xrefs,
|
||||
intersection_objects,
|
||||
main_subent_type: 1,
|
||||
main_gs_marker: marker,
|
||||
osnap_distance: parameter,
|
||||
@@ -1385,13 +1453,41 @@ impl Scene {
|
||||
/// the erased source comes back and the same query starts answering
|
||||
/// [`ReferenceStatus::Resolved`] again, with the reference record itself
|
||||
/// never having been rewritten.
|
||||
pub fn dimension_association_status(
|
||||
&self,
|
||||
dimension: Handle,
|
||||
) -> Vec<(usize, ReferenceStatus)> {
|
||||
pub fn dimension_association_status(&self, dimension: Handle) -> Vec<(usize, ReferenceStatus)> {
|
||||
let Some(association) = self.dimension_association(dimension) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let measurement_supported = !association.trans_space
|
||||
|| self.document.get_entity(dimension).is_some_and(|entity| {
|
||||
matches!(
|
||||
entity,
|
||||
EntityType::Dimension(Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_))
|
||||
) || persisted_measurement_scale(&self.document, entity).is_some()
|
||||
});
|
||||
let measurement_supported = measurement_supported
|
||||
&& self.document.get_entity(dimension).is_some_and(|entity| {
|
||||
if !matches!(
|
||||
entity,
|
||||
EntityType::Dimension(
|
||||
Dimension::Radius(_) | Dimension::Diameter(_) | Dimension::LargeRadial(_)
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
association.references[0]
|
||||
.first()
|
||||
.and_then(|reference| {
|
||||
let walked = chain::walk_chain(&self.document, &reference.xrefs).ok()?;
|
||||
let source = self.document.get_entity(walked.entity)?;
|
||||
chain_map(self, &walked)
|
||||
.map_radial(radial_source_for_marker(source, reference.main_gs_marker)?)
|
||||
})
|
||||
.is_some()
|
||||
});
|
||||
let measurement_supported = measurement_supported
|
||||
&& association.references.iter().flatten()
|
||||
.filter(|r| chain::needs_from_point(r.osnap_type)).count() <= 1;
|
||||
let points = self.dimension_association_slot_points(dimension);
|
||||
association
|
||||
.references
|
||||
.iter()
|
||||
@@ -1400,8 +1496,16 @@ impl Scene {
|
||||
let reference = slot.first()?;
|
||||
Some((
|
||||
index,
|
||||
match resolve_reference_chain(self, reference, None) {
|
||||
Ok(_) => ReferenceStatus::Resolved,
|
||||
match resolve_reference_chain(
|
||||
self,
|
||||
reference,
|
||||
points
|
||||
.iter()
|
||||
.enumerate()
|
||||
.find_map(|(other, point)| (other != index).then_some(*point)),
|
||||
) {
|
||||
Ok(_) if measurement_supported => ReferenceStatus::Resolved,
|
||||
Ok(_) => ReferenceStatus::Unresolved,
|
||||
Err(status) => status,
|
||||
},
|
||||
))
|
||||
@@ -1437,10 +1541,7 @@ impl Scene {
|
||||
}
|
||||
|
||||
/// The DIMASSOC payload attached to `dimension`, if there is one.
|
||||
pub fn dimension_association(
|
||||
&self,
|
||||
dimension: Handle,
|
||||
) -> Option<&AssocDimensionAssociation> {
|
||||
pub fn dimension_association(&self, dimension: Handle) -> Option<&AssocDimensionAssociation> {
|
||||
self.document.objects.values().find_map(|object| {
|
||||
let ObjectType::Associative(object) = object else {
|
||||
return None;
|
||||
@@ -1557,12 +1658,8 @@ impl Scene {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn infer_dimension_sources(
|
||||
&self,
|
||||
dimension: Handle,
|
||||
) -> Vec<Option<Handle>> {
|
||||
let Some(EntityType::Dimension(entity)) = self.document.get_entity(dimension)
|
||||
else {
|
||||
pub(crate) fn infer_dimension_sources(&self, dimension: Handle) -> Vec<Option<Handle>> {
|
||||
let Some(EntityType::Dimension(entity)) = self.document.get_entity(dimension) else {
|
||||
return Vec::new();
|
||||
};
|
||||
let radial_data = match entity {
|
||||
@@ -1659,11 +1756,15 @@ impl Scene {
|
||||
|
||||
let mut refreshed = Vec::new();
|
||||
for association in associations {
|
||||
// Two passes. PERPENDICULAR and TANGENT are measured *from* the
|
||||
// other definition point, so the independent references have to
|
||||
// land first; a dimension with two dependent references keeps its
|
||||
// old points as the from-points, which is stable because they were
|
||||
// valid the last time round.
|
||||
// Dependent snaps need a resolved independent definition point.
|
||||
// Two such features require a coupled solve; old points are not
|
||||
// evidence of a solution after the geometry changes.
|
||||
if association.references.iter().flatten()
|
||||
.filter(|r| chain::needs_from_point(r.osnap_type)).count() > 1
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// Resolve independent references first, then perpendicular/tangent.
|
||||
let mut resolved: [Option<ResolvedReference>; 4] = std::array::from_fn(|index| {
|
||||
let reference = association.references[index].first()?;
|
||||
if chain::needs_from_point(reference.osnap_type) {
|
||||
@@ -1675,9 +1776,7 @@ impl Scene {
|
||||
.document
|
||||
.get_entity(association.dimension)
|
||||
.and_then(|entity| match entity {
|
||||
EntityType::Dimension(dimension) => {
|
||||
Some(dimension_reference_points(dimension))
|
||||
}
|
||||
EntityType::Dimension(dimension) => Some(dimension_reference_points(dimension)),
|
||||
_ => None,
|
||||
})
|
||||
.unwrap_or_default();
|
||||
@@ -1699,6 +1798,19 @@ impl Scene {
|
||||
});
|
||||
resolved[index] = resolve_reference_chain(self, reference, from).ok();
|
||||
}
|
||||
if association
|
||||
.references
|
||||
.iter()
|
||||
.enumerate()
|
||||
.any(|(index, slot)| !slot.is_empty() && resolved[index].is_none())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let viewports: std::collections::HashSet<_> =
|
||||
resolved.iter().flatten().map(|r| r.viewport).collect();
|
||||
if viewports.len() > 1 {
|
||||
continue;
|
||||
}
|
||||
// The map of the first live reference governs how the whole
|
||||
// dimension is placed and measured — all of a dimension's points
|
||||
// are in one space.
|
||||
@@ -1725,7 +1837,7 @@ impl Scene {
|
||||
// Mapped once, here: everything the match arms below build out
|
||||
// of it then lands in the dimension's own space.
|
||||
Some((
|
||||
chain_map(self, &walked).map_radial(radial),
|
||||
chain_map(self, &walked).map_radial(radial)?,
|
||||
reference.osnap_distance,
|
||||
))
|
||||
});
|
||||
@@ -1738,15 +1850,34 @@ impl Scene {
|
||||
_ => return None,
|
||||
};
|
||||
let radial = radial_source_for_marker(entity, segment)?;
|
||||
Some(chain_map(self, &walked).map_radial(radial))
|
||||
chain_map(self, &walked).map_radial(radial)
|
||||
});
|
||||
if radial_source.is_none() && resolved.iter().all(Option::is_none) {
|
||||
continue;
|
||||
}
|
||||
let Some(before) = self.document.get_entity(association.dimension).cloned() else {
|
||||
continue;
|
||||
};
|
||||
let angular = matches!(
|
||||
&before,
|
||||
EntityType::Dimension(Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_))
|
||||
);
|
||||
let measurement_scale = if let Some(frame) = map.frame.filter(|_| !angular) {
|
||||
let Some(mut scale) = persisted_measurement_scale(&self.document, &before) else {
|
||||
continue;
|
||||
};
|
||||
scale.viewport_compensation = frame.paper_to_model_length_factor();
|
||||
Some(scale)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if self.is_recording_undo() {
|
||||
let before = self.document.get_entity_arc(association.dimension);
|
||||
self.record_undo_before(association.dimension, before);
|
||||
}
|
||||
if measurement_scale.is_some() {
|
||||
self.ensure_app_id(MeasurementScale::APP_ID);
|
||||
}
|
||||
let Some(EntityType::Dimension(dimension)) =
|
||||
self.document.get_entity_mut(association.dimension)
|
||||
else {
|
||||
@@ -2005,8 +2136,8 @@ impl Scene {
|
||||
.zip(resolved.iter())
|
||||
.all(|(model, space)| match (model, space, map.frame.as_ref()) {
|
||||
(Some(model), Some(space), Some(frame)) => {
|
||||
let projected =
|
||||
frame.model_to_paper(glam::DVec3::new(model.x, model.y, model.z));
|
||||
let projected = frame
|
||||
.model_to_paper(glam::DVec3::new(model.x, model.y, model.z));
|
||||
(projected.x - space.x).abs() < 1e-6
|
||||
&& (projected.y - space.y).abs() < 1e-6
|
||||
}
|
||||
@@ -2015,6 +2146,30 @@ impl Scene {
|
||||
"resolved paper points must be the model points through the frame",
|
||||
);
|
||||
dimension.base_mut().actual_measurement = measurement;
|
||||
if map.frame.is_some() {
|
||||
if let EntityType::Dimension(old) = &before {
|
||||
if old.base().text_user_positioned {
|
||||
dimension.base_mut().text_middle_point = old.base().text_middle_point;
|
||||
dimension.base_mut().insertion_point = old.base().insertion_point;
|
||||
}
|
||||
}
|
||||
}
|
||||
let entity = self
|
||||
.document
|
||||
.get_entity_mut(association.dimension)
|
||||
.expect("dimension exists");
|
||||
if let Some(scale) = measurement_scale {
|
||||
scale.write_to_entity(entity);
|
||||
}
|
||||
if *entity == before {
|
||||
continue;
|
||||
}
|
||||
// Saved anonymous blocks bypass generated dimension rendering.
|
||||
// Invalidate only after a completely resolved, committed update;
|
||||
// the undo snapshot above retains the previous picture and data.
|
||||
if let EntityType::Dimension(dimension) = entity {
|
||||
dimension.base_mut().block_name.clear();
|
||||
}
|
||||
refreshed.push((association.dimension, ChangeKind::Modified));
|
||||
}
|
||||
refreshed
|
||||
@@ -2053,3 +2208,121 @@ fn linear_measurement(linear: &acadrust::entities::DimensionLinear) -> f64 {
|
||||
let axis = [linear.rotation.cos(), linear.rotation.sin()];
|
||||
((second[0] - first[0]) * axis[0] + (second[1] - first[1]) * axis[1]).abs()
|
||||
}
|
||||
|
||||
/// Encode the feature actually acquired, in source-local coordinates.
|
||||
fn feature_reference(
|
||||
entity: &EntityType,
|
||||
point: Vector3,
|
||||
kind: crate::snap::SnapType,
|
||||
) -> Option<(i32, f64)> {
|
||||
use crate::snap::SnapType as S;
|
||||
match kind {
|
||||
S::Node | S::Insertion => Some((0, 0.0)),
|
||||
S::Center if matches!(entity, EntityType::Ellipse(_)) => Some((-3, 0.0)),
|
||||
S::Endpoint | S::ObjectPick | S::Center => {
|
||||
if kind != S::Center {
|
||||
if let Some(index) = source_points(entity)
|
||||
.iter()
|
||||
.position(|candidate| point_distance_squared(*candidate, point) < 1e-12)
|
||||
{
|
||||
return Some((index as i32, 0.0));
|
||||
}
|
||||
}
|
||||
if matches!(entity, EntityType::Circle(_) | EntityType::Arc(_)) {
|
||||
return source_reference(entity, point);
|
||||
}
|
||||
let planar = crate::entities::curve::entity_curve(entity)?;
|
||||
if let KernelCurve::Polyline(polyline) = &planar.curve {
|
||||
for segment in 0..polyline.vertices.len() {
|
||||
if let Some(arc) = polyline.segment_arc(segment) {
|
||||
let center = vector3(planar.plane.point_at(arc.center));
|
||||
if point_distance_squared(center, point) < 1e-12 {
|
||||
return Some((POLYLINE_ARC_CENTER_MARKER, segment as f64));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
S::Nearest | S::Extension => {
|
||||
if matches!(entity, EntityType::Circle(_) | EntityType::Arc(_)) {
|
||||
return source_reference(entity, point);
|
||||
}
|
||||
let planar = crate::entities::curve::entity_curve(entity)?;
|
||||
let near = closest_point(&planar.curve, planar.plane.project(dpoint(point))?);
|
||||
Some((0, near.t))
|
||||
}
|
||||
S::Midpoint => {
|
||||
let planar = crate::entities::curve::entity_curve(entity)?;
|
||||
if let KernelCurve::Polyline(_) = &planar.curve {
|
||||
let uv = planar.plane.project(dpoint(point))?;
|
||||
let segments = planar.curve.segments();
|
||||
let (index, _) = segments.iter().enumerate().min_by(|(_, a), (_, b)| {
|
||||
let error = |c: &KernelCurve| {
|
||||
let p = closest_point(c, uv).point;
|
||||
(p[0] - uv[0]).powi(2) + (p[1] - uv[1]).powi(2)
|
||||
};
|
||||
error(a).total_cmp(&error(b))
|
||||
})?;
|
||||
Some((index as i32, 0.5))
|
||||
} else {
|
||||
Some((0, 0.5))
|
||||
}
|
||||
}
|
||||
S::Tangent => {
|
||||
if let EntityType::Spline(spline) = entity {
|
||||
return Some((
|
||||
0,
|
||||
crate::entities::spline::nurbs3(spline)?.parameter_at(dpoint(point)),
|
||||
));
|
||||
}
|
||||
source_reference(entity, point)
|
||||
}
|
||||
S::Intersection | S::ApparentIntersection => Some((0, 0.0)),
|
||||
_ => source_reference(entity, point),
|
||||
}
|
||||
}
|
||||
|
||||
fn persisted_measurement_scale(
|
||||
doc: &acadrust::CadDocument,
|
||||
entity: &EntityType,
|
||||
) -> Option<MeasurementScale> {
|
||||
use crate::entities::dim_override;
|
||||
use acadrust::xdata::XDataValue;
|
||||
let EntityType::Dimension(dimension) = entity else {
|
||||
return None;
|
||||
};
|
||||
let data = &dimension.base().common.extended_data;
|
||||
let effective = dim_override::real(data, dim_override::DIMLFAC)
|
||||
.or_else(|| {
|
||||
doc.dim_styles
|
||||
.get(&dimension.base().style_name)
|
||||
.map(|s| s.dimlfac)
|
||||
})?
|
||||
.abs();
|
||||
let effective = if effective == 0.0 { 1.0 } else { effective };
|
||||
if !effective.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if let Some(mut scale) = MeasurementScale::read(data) {
|
||||
// A Properties edit writes the persisted total DIMLFAC. Preserve that
|
||||
// new user choice when the next source/viewport update recomposes it.
|
||||
scale.user_lfac = effective / scale.viewport_compensation;
|
||||
return Some(scale);
|
||||
}
|
||||
let calculated = data
|
||||
.get_record("ACAD_DIMASSOC_CALC_DIMLFAC")?
|
||||
.values
|
||||
.iter()
|
||||
.find_map(|v| match v {
|
||||
XDataValue::Real(v) => Some(v.abs()),
|
||||
_ => None,
|
||||
})?;
|
||||
if !calculated.is_finite() || calculated <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
Some(MeasurementScale {
|
||||
user_lfac: effective / calculated,
|
||||
viewport_compensation: calculated,
|
||||
})
|
||||
}
|
||||
+263
-178
@@ -37,7 +37,7 @@
|
||||
use acadrust::objects::AssocDimensionReference;
|
||||
use acadrust::types::{Handle, Matrix4, Transform, Vector3};
|
||||
use acadrust::{CadDocument, EntityType};
|
||||
use cadkernel::geom2d::{closest_point, Curve as KernelCurve, DEFAULT_SEGMENTS_PER_RADIAN};
|
||||
use cadkernel::geom2d::{closest_point, Curve as KernelCurve};
|
||||
use cadkernel::space::Plane;
|
||||
|
||||
use crate::entities::curve::entity_curve;
|
||||
@@ -50,7 +50,7 @@ use crate::entities::curve::entity_curve;
|
||||
/// and `NEAR` for a point taken at a parameter on a circle, so those two keep
|
||||
/// meaning exactly what they used to.
|
||||
#[allow(dead_code)] // The full code table is kept so a reader can see what a
|
||||
// stored `osnap_type` means, including the modes we pass through untouched.
|
||||
// stored `osnap_type` means, including the modes we pass through untouched.
|
||||
pub(crate) mod osnap {
|
||||
pub const NONE: u8 = 0;
|
||||
pub const END: u8 = 1;
|
||||
@@ -115,6 +115,7 @@ pub(crate) struct ReferenceChain {
|
||||
pub(crate) enum ChainError {
|
||||
/// `xrefs` was empty — nothing to resolve.
|
||||
Empty,
|
||||
Invalid,
|
||||
/// A handle in the chain names an object that is not in the document
|
||||
/// (erased source; restored by an undo).
|
||||
Missing(Handle),
|
||||
@@ -131,55 +132,54 @@ pub(crate) fn walk_chain(
|
||||
document: &CadDocument,
|
||||
xrefs: &[Handle],
|
||||
) -> Result<ReferenceChain, ChainError> {
|
||||
if xrefs.is_empty() {
|
||||
return Err(ChainError::Empty);
|
||||
}
|
||||
let mut viewport = None;
|
||||
let mut block_path: Vec<Handle> = Vec::new();
|
||||
let mut entity = None;
|
||||
let mut block_path = Vec::new();
|
||||
let mut transform = Transform::identity();
|
||||
|
||||
for (index, handle) in xrefs.iter().copied().enumerate() {
|
||||
let mut owner = None;
|
||||
for (index, &handle) in xrefs.iter().enumerate() {
|
||||
if handle.is_null() {
|
||||
return Err(ChainError::Invalid);
|
||||
}
|
||||
let node = document
|
||||
.get_entity(handle)
|
||||
.ok_or(ChainError::Missing(handle))?;
|
||||
if owner.is_some_and(|owner| node.common().owner_handle != owner) {
|
||||
return Err(ChainError::Invalid);
|
||||
}
|
||||
if index == 0 && matches!(node, EntityType::Viewport(_)) {
|
||||
viewport = Some(handle);
|
||||
owner = document.block_records.get("*Model_Space").map(|b| b.handle);
|
||||
continue;
|
||||
}
|
||||
let Some(node) = document.get_entity(handle) else {
|
||||
return Err(ChainError::Missing(handle));
|
||||
};
|
||||
match node {
|
||||
// Only a *leading* viewport is the trans-space hop. A viewport
|
||||
// deeper in the chain would be geometry being dimensioned (the
|
||||
// paper-space rectangle itself), so it falls through to `entity`.
|
||||
EntityType::Viewport(_) if index == 0 && viewport.is_none() => {
|
||||
viewport = Some(handle);
|
||||
}
|
||||
EntityType::Insert(insert) => {
|
||||
block_path.push(handle);
|
||||
transform =
|
||||
transform.compose(&crate::scene::render_graph::insert_transform(document, insert));
|
||||
}
|
||||
_ => {
|
||||
entity = Some(handle);
|
||||
break;
|
||||
}
|
||||
if index == xrefs.len() - 1 {
|
||||
return Ok(ReferenceChain {
|
||||
viewport,
|
||||
block_path,
|
||||
entity: handle,
|
||||
transform,
|
||||
});
|
||||
}
|
||||
let EntityType::Insert(insert) = node else {
|
||||
return Err(ChainError::Invalid);
|
||||
};
|
||||
// Arrays need an instance index that this reference format does not carry.
|
||||
if insert.row_count > 1 || insert.column_count > 1 || block_path.contains(&handle) {
|
||||
return Err(ChainError::Invalid);
|
||||
}
|
||||
let block = document
|
||||
.block_records
|
||||
.get(&insert.block_name)
|
||||
.ok_or(ChainError::Invalid)?;
|
||||
owner = Some(block.handle);
|
||||
block_path.push(handle);
|
||||
transform = transform.compose(&crate::scene::render_graph::insert_transform(
|
||||
document, insert,
|
||||
));
|
||||
}
|
||||
|
||||
let entity = match entity {
|
||||
Some(entity) => entity,
|
||||
// Nothing but INSERTs: the innermost one is the geometry, and it is
|
||||
// no longer part of the path leading to it.
|
||||
None => match block_path.pop() {
|
||||
Some(innermost) => {
|
||||
transform = block_transform(document, &block_path);
|
||||
innermost
|
||||
}
|
||||
None => return Err(ChainError::Empty),
|
||||
},
|
||||
};
|
||||
Ok(ReferenceChain {
|
||||
viewport,
|
||||
block_path,
|
||||
entity,
|
||||
transform,
|
||||
})
|
||||
Err(ChainError::Empty)
|
||||
}
|
||||
|
||||
/// Accumulated entity-local -> model transform for an ordered INSERT path.
|
||||
@@ -187,8 +187,9 @@ pub(crate) fn block_transform(document: &CadDocument, block_path: &[Handle]) ->
|
||||
let mut transform = Transform::identity();
|
||||
for handle in block_path {
|
||||
if let Some(EntityType::Insert(insert)) = document.get_entity(*handle) {
|
||||
transform =
|
||||
transform.compose(&crate::scene::render_graph::insert_transform(document, insert));
|
||||
transform = transform.compose(&crate::scene::render_graph::insert_transform(
|
||||
document, insert,
|
||||
));
|
||||
}
|
||||
}
|
||||
transform
|
||||
@@ -270,7 +271,7 @@ pub(crate) struct FeatureContext {
|
||||
/// The point the feature is measured *from*, in entity-local coordinates.
|
||||
/// PERPENDICULAR and TANGENT are two-point features: the foot / touch
|
||||
/// point only exists relative to an external point, which for a dimension
|
||||
/// is the other definition point. `None` falls back to `hint`.
|
||||
/// is the other definition point. Tangency requires this point.
|
||||
pub from: Option<Vector3>,
|
||||
}
|
||||
|
||||
@@ -287,9 +288,12 @@ pub(crate) fn feature_point(
|
||||
context: FeatureContext,
|
||||
) -> Option<Vector3> {
|
||||
match reference.osnap_type {
|
||||
osnap::END | osnap::START_POINT if reference.main_subent_type == 2 => {
|
||||
imported_endpoint(entity, reference, context.hint)
|
||||
}
|
||||
osnap::CEN => center_point(entity, reference),
|
||||
osnap::MID => mid_point(entity, reference),
|
||||
osnap::QUAD => quadrant_point(entity, context.hint?),
|
||||
osnap::QUAD => quadrant_point(entity, reference, context.hint?),
|
||||
osnap::NODE | osnap::INS => node_point(entity),
|
||||
osnap::INTERSEC | osnap::APPARENT_INT => intersection_point(
|
||||
document,
|
||||
@@ -313,7 +317,9 @@ pub(crate) fn feature_point(
|
||||
pub(crate) fn evaluates(osnap_type: u8) -> bool {
|
||||
matches!(
|
||||
osnap_type,
|
||||
osnap::CEN
|
||||
osnap::END
|
||||
| osnap::START_POINT
|
||||
| osnap::CEN
|
||||
| osnap::MID
|
||||
| osnap::QUAD
|
||||
| osnap::NODE
|
||||
@@ -342,7 +348,13 @@ fn center_point(entity: &EntityType, reference: &AssocDimensionReference) -> Opt
|
||||
let KernelCurve::Polyline(polyline) = &planar.curve else {
|
||||
return None;
|
||||
};
|
||||
let segment = reference.main_gs_marker.max(0) as usize;
|
||||
let segment = if reference.main_subent_type == 1
|
||||
&& reference.main_gs_marker == super::dimension_assoc::POLYLINE_ARC_CENTER_MARKER
|
||||
{
|
||||
reference.osnap_distance.round().max(0.0) as usize
|
||||
} else {
|
||||
segment_index(reference)?
|
||||
};
|
||||
let arc = polyline.segment_arc(segment)?;
|
||||
Some(lift(&planar.plane, arc.center))
|
||||
}
|
||||
@@ -356,36 +368,52 @@ fn mid_point(entity: &EntityType, reference: &AssocDimensionReference) -> Option
|
||||
// without one, the midpoint of the whole run.
|
||||
KernelCurve::Polyline(_) if reference.main_gs_marker >= 0 => {
|
||||
let segments = planar.curve.segments();
|
||||
let segment = segments.get(reference.main_gs_marker as usize)?;
|
||||
let segment = segments.get(segment_index(reference)?)?;
|
||||
Some(lift(&planar.plane, segment.point_at(0.5)))
|
||||
}
|
||||
_ => Some(lift(&planar.plane, planar.curve.point_at(0.5))),
|
||||
}
|
||||
}
|
||||
|
||||
fn quadrant_point(entity: &EntityType, hint: Vector3) -> Option<Vector3> {
|
||||
fn quadrant_point(
|
||||
entity: &EntityType,
|
||||
reference: &AssocDimensionReference,
|
||||
hint: Vector3,
|
||||
) -> Option<Vector3> {
|
||||
let planar = entity_curve(entity)?;
|
||||
let (centre, radius) = match &planar.curve {
|
||||
KernelCurve::Circle(circle) => (circle.centre, circle.radius),
|
||||
KernelCurve::Arc(arc) => (arc.centre, arc.radius),
|
||||
_ => return None,
|
||||
};
|
||||
// Four candidates on the curve's own plane; the stored point picks one, so
|
||||
// a rotated source keeps the quadrant the user actually snapped to.
|
||||
let valid_angle = |angle: f64| match &planar.curve {
|
||||
KernelCurve::Arc(arc) => {
|
||||
(angle - arc.start_angle).rem_euclid(std::f64::consts::TAU) <= arc.sweep() + 1e-9
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
let at_angle = |angle: f64| {
|
||||
lift(
|
||||
&planar.plane,
|
||||
[
|
||||
centre[0] + radius * angle.cos(),
|
||||
centre[1] + radius * angle.sin(),
|
||||
],
|
||||
)
|
||||
};
|
||||
// New references retain the chosen source-local angle, so translation
|
||||
// cannot silently select another quadrant using the old world point.
|
||||
if reference.main_subent_type == 1 && reference.main_gs_marker == -2 {
|
||||
let angle = reference.osnap_distance;
|
||||
return (angle.is_finite() && valid_angle(angle)).then(|| at_angle(angle));
|
||||
}
|
||||
// An imported point can identify an unchanged quadrant, but cannot prove
|
||||
// which quadrant was intended after it moves away from the stored hint.
|
||||
(0..4)
|
||||
.map(|index| {
|
||||
let angle = std::f64::consts::FRAC_PI_2 * index as f64;
|
||||
lift(
|
||||
&planar.plane,
|
||||
[
|
||||
centre[0] + radius * angle.cos(),
|
||||
centre[1] + radius * angle.sin(),
|
||||
],
|
||||
)
|
||||
})
|
||||
.min_by(|first, second| {
|
||||
distance_squared(*first, hint).total_cmp(&distance_squared(*second, hint))
|
||||
})
|
||||
.map(|index| std::f64::consts::FRAC_PI_2 * index as f64)
|
||||
.filter(|angle| valid_angle(*angle))
|
||||
.map(at_angle)
|
||||
.find(|candidate| distance_squared(*candidate, hint) < 1e-12)
|
||||
}
|
||||
|
||||
fn node_point(entity: &EntityType) -> Option<Vector3> {
|
||||
@@ -402,7 +430,50 @@ fn node_point(entity: &EntityType) -> Option<Vector3> {
|
||||
fn perpendicular_point(entity: &EntityType, from: Vector3) -> Option<Vector3> {
|
||||
let planar = entity_curve(entity)?;
|
||||
let uv = planar.plane.project(dpoint(from))?;
|
||||
Some(lift(&planar.plane, closest_point(&planar.curve, uv).point))
|
||||
let near = closest_point(&planar.curve, uv);
|
||||
let direction = planar.curve.tangent_at(near.t);
|
||||
let delta = [near.point[0] - uv[0], near.point[1] - uv[1]];
|
||||
let residual = delta[0] * direction[0] + delta[1] * direction[1];
|
||||
if residual.abs() > 1e-8 * delta[0].hypot(delta[1]).max(1.0) {
|
||||
return None;
|
||||
}
|
||||
Some(lift(&planar.plane, near.point))
|
||||
}
|
||||
|
||||
/// Perpendicularity must be evaluated after a block's nonuniform scale.
|
||||
pub(crate) fn perpendicular_in_model(
|
||||
entity: &EntityType,
|
||||
transform: &Transform,
|
||||
from: Vector3,
|
||||
) -> Option<Vector3> {
|
||||
use glam::DVec3;
|
||||
let source = entity_curve(entity)?;
|
||||
let origin = transform.apply(vector3(source.plane.origin));
|
||||
let x = DVec3::from_array(dpoint(
|
||||
transform.apply_rotation(vector3(source.plane.x_axis)),
|
||||
));
|
||||
let y = DVec3::from_array(dpoint(
|
||||
transform.apply_rotation(vector3(source.plane.y_axis)),
|
||||
));
|
||||
let axis_x = x.try_normalize()?;
|
||||
let normal = x.cross(y).try_normalize()?;
|
||||
let axis_y = normal.cross(axis_x);
|
||||
let plane = Plane::from_axes(dpoint(origin), axis_x.to_array(), axis_y.to_array());
|
||||
let curve = source.curve.transformed(&cadkernel::geom2d::Transform {
|
||||
origin: [0.0, 0.0].into(),
|
||||
x_axis: [x.dot(axis_x), x.dot(axis_y)].into(),
|
||||
y_axis: [y.dot(axis_x), y.dot(axis_y)].into(),
|
||||
})?;
|
||||
let uv = plane.project(dpoint(from))?;
|
||||
let near = closest_point(&curve, uv);
|
||||
let tangent = curve.tangent_at(near.t);
|
||||
let delta = [near.point[0] - uv[0], near.point[1] - uv[1]];
|
||||
if (delta[0] * tangent[0] + delta[1] * tangent[1]).abs()
|
||||
> 1e-8 * delta[0].hypot(delta[1]).max(1.0)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(lift(&plane, near.point))
|
||||
}
|
||||
|
||||
fn nearest_point(
|
||||
@@ -416,9 +487,7 @@ fn nearest_point(
|
||||
// Every other curve carries the kernel's normalised 0..1 parameter.
|
||||
match &planar.curve {
|
||||
KernelCurve::Circle(_) | KernelCurve::Arc(_) => match entity {
|
||||
EntityType::Circle(circle) => {
|
||||
Some(circle.point_at_angle_wcs(reference.osnap_distance))
|
||||
}
|
||||
EntityType::Circle(circle) => Some(circle.point_at_angle_wcs(reference.osnap_distance)),
|
||||
EntityType::Arc(arc) => Some(arc.point_at_angle_wcs(reference.osnap_distance)),
|
||||
_ => None,
|
||||
},
|
||||
@@ -454,39 +523,14 @@ fn tangent_point(
|
||||
return spline_tangent_point(spline, reference, context);
|
||||
}
|
||||
let planar = entity_curve(entity)?;
|
||||
let (centre, radius) = match &planar.curve {
|
||||
KernelCurve::Circle(circle) => (circle.centre, circle.radius),
|
||||
KernelCurve::Arc(arc) => (arc.centre, arc.radius),
|
||||
_ => {
|
||||
// Generic curve: no closed form and no spline evaluator. Hold the
|
||||
// stored parameter if there is one, else leave it unresolved.
|
||||
return nearest_point(entity, reference, context.hint);
|
||||
}
|
||||
};
|
||||
let from = context.from.or(context.hint)?;
|
||||
let from = context.from?;
|
||||
let uv = planar.plane.project(dpoint(from))?;
|
||||
let (dx, dy) = (uv[0] - centre[0], uv[1] - centre[1]);
|
||||
let distance = (dx * dx + dy * dy).sqrt();
|
||||
if !distance.is_finite() || distance <= radius + 1e-12 || radius <= 1e-12 {
|
||||
return None;
|
||||
}
|
||||
// Two touch points, symmetric about the centre-to-`from` line.
|
||||
let base = dy.atan2(dx);
|
||||
let offset = (radius / distance).clamp(-1.0, 1.0).acos();
|
||||
let candidates = [base + offset, base - offset].map(|angle| {
|
||||
lift(
|
||||
&planar.plane,
|
||||
[
|
||||
centre[0] + radius * angle.cos(),
|
||||
centre[1] + radius * angle.sin(),
|
||||
],
|
||||
)
|
||||
});
|
||||
let hint = context.hint.unwrap_or(candidates[0]);
|
||||
candidates
|
||||
cadkernel::geom2d::snap::tangent_from(&planar.curve, uv)
|
||||
.into_iter()
|
||||
.min_by(|first, second| {
|
||||
distance_squared(*first, hint).total_cmp(&distance_squared(*second, hint))
|
||||
.map(|candidate| lift(&planar.plane, candidate.point))
|
||||
.min_by(|a, b| {
|
||||
distance_squared(*a, context.hint.unwrap_or(from))
|
||||
.total_cmp(&distance_squared(*b, context.hint.unwrap_or(from)))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -504,50 +548,52 @@ fn spline_tangent_point(
|
||||
let hint = context.hint.or(context.from)?;
|
||||
curve.parameter_at(dpoint(hint)).clamp(start, end)
|
||||
};
|
||||
let Some(from) = context.from.or(context.hint) else {
|
||||
// No external point: the stored parameter is the whole answer.
|
||||
return Some(vector3(curve.point_at_knot(u)));
|
||||
};
|
||||
let p = dpoint(from);
|
||||
// Newton on f(u) = (C(u) - P) · C'(u). f'(u) is approximated by a central
|
||||
// difference of f, which converges in a handful of steps from a seed that
|
||||
// is already on the right lobe and never leaves the domain.
|
||||
let from = glam::DVec3::from_array(dpoint(context.from?));
|
||||
// A tangent is parallel to C(u)-P. Minimize the vector cross product,
|
||||
// which also detects nonplanar curves that have no tangent from P.
|
||||
let f = |u: f64| {
|
||||
let c = curve.point_at_knot(u);
|
||||
let d = curve.derivative_at_knot(u);
|
||||
(c[0] - p[0]) * d[0] + (c[1] - p[1]) * d[1] + (c[2] - p[2]) * d[2]
|
||||
let c = glam::DVec3::from_array(curve.point_at_knot(u));
|
||||
let d = glam::DVec3::from_array(curve.derivative_at_knot(u));
|
||||
(c - from).cross(d)
|
||||
};
|
||||
let span = (end - start).abs().max(1e-12);
|
||||
let step = span * 1e-6;
|
||||
for _ in 0..24 {
|
||||
let span = (end - start).abs();
|
||||
if !span.is_finite() || span <= 1e-15 {
|
||||
return None;
|
||||
}
|
||||
for _ in 0..48 {
|
||||
let value = f(u);
|
||||
if value.abs() <= span * 1e-12 {
|
||||
let lo = (u - span * 1e-6).max(start);
|
||||
let hi = (u + span * 1e-6).min(end);
|
||||
let derivative = (f(hi) - f(lo)) / (hi - lo);
|
||||
if !value.is_finite() || !derivative.is_finite() {
|
||||
return None;
|
||||
}
|
||||
if derivative.length_squared() <= 1e-30 {
|
||||
break;
|
||||
}
|
||||
let slope = (f((u + step).min(end)) - f((u - step).max(start))) / (2.0 * step);
|
||||
if !slope.is_finite() || slope.abs() < 1e-18 {
|
||||
break;
|
||||
}
|
||||
let next = (u - value / slope).clamp(start, end);
|
||||
if !next.is_finite() || (next - u).abs() <= span * 1e-14 {
|
||||
let next = (u - value.dot(derivative) / derivative.length_squared()).clamp(start, end);
|
||||
if (next - u).abs() < span * 1e-14 {
|
||||
u = next;
|
||||
break;
|
||||
}
|
||||
u = next;
|
||||
}
|
||||
Some(vector3(curve.point_at_knot(u)))
|
||||
let c = glam::DVec3::from_array(curve.point_at_knot(u));
|
||||
let d = glam::DVec3::from_array(curve.derivative_at_knot(u));
|
||||
let scale = (c - from).length() * d.length();
|
||||
if !c.is_finite() || scale <= 1e-20 || f(u).length() > 1e-8 * scale {
|
||||
return None;
|
||||
}
|
||||
Some(vector3(c.to_array()))
|
||||
}
|
||||
|
||||
// ── Intersection ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Intersection of `entity` with the reference's second object chain.
|
||||
///
|
||||
/// Both curves are tessellated on the first curve's plane and crossed
|
||||
/// pairwise; the crossing nearest the stored osnap point wins, so a pair of
|
||||
/// curves that meet more than once keeps the corner the user picked. With
|
||||
/// `apparent` the segment parameters are not clamped, which recovers the
|
||||
/// crossing of the two *extended* nearest segments — the best-effort AutoCAD
|
||||
/// calls an apparent intersection in a plan view.
|
||||
/// Transform both curves into the first entity's local plane and use the
|
||||
/// kernel's exact/converged intersection solver. Apparent intersections may
|
||||
/// project noncoplanar curves; only straight lines support an extended crossing.
|
||||
fn intersection_point(
|
||||
document: &CadDocument,
|
||||
entity: &EntityType,
|
||||
@@ -555,59 +601,53 @@ fn intersection_point(
|
||||
hint: Vector3,
|
||||
apparent: bool,
|
||||
) -> Option<Vector3> {
|
||||
let main = walk_chain(document, &reference.xrefs).ok()?;
|
||||
let other = walk_chain(document, &reference.intersection_objects).ok()?;
|
||||
let other_entity = document.get_entity(other.entity)?;
|
||||
if main.viewport != other.viewport {
|
||||
return None;
|
||||
}
|
||||
let inverse = invert(&main.transform)?;
|
||||
let first = entity_curve(entity)?;
|
||||
let second = entity_curve(other_entity)?;
|
||||
// The first curve's plane is the working frame. The second curve is
|
||||
// evaluated through its *own* block path, so each of its points is lifted
|
||||
// to model space and then projected back onto that frame before crossing.
|
||||
let a = first.curve.tessellate(DEFAULT_SEGMENTS_PER_RADIAN);
|
||||
let b = second
|
||||
.curve
|
||||
.tessellate(DEFAULT_SEGMENTS_PER_RADIAN)
|
||||
.into_iter()
|
||||
.map(|uv| {
|
||||
let world = other.transform.apply(lift(&second.plane, uv));
|
||||
first.plane.project(dpoint(world)).unwrap_or(uv)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let second = entity_curve(document.get_entity(other.entity)?)?;
|
||||
let map = |uv| {
|
||||
let local = inverse.apply(other.transform.apply(lift(&second.plane, uv)));
|
||||
let projected = first.plane.project(dpoint(local))?;
|
||||
if !apparent && distance_squared(local, lift(&first.plane, projected)) > 1e-12 {
|
||||
return None;
|
||||
}
|
||||
Some(projected)
|
||||
};
|
||||
let origin = glam::DVec2::from_array(map([0.0, 0.0])?);
|
||||
let x = glam::DVec2::from_array(map([1.0, 0.0])?) - origin;
|
||||
let y = glam::DVec2::from_array(map([0.0, 1.0])?) - origin;
|
||||
let second = second.curve.transformed(&cadkernel::geom2d::Transform {
|
||||
origin: origin.to_array().into(),
|
||||
x_axis: x.to_array().into(),
|
||||
y_axis: y.to_array().into(),
|
||||
})?;
|
||||
let hint_uv = first.plane.project(dpoint(hint))?;
|
||||
let mut best: Option<([f64; 2], f64)> = None;
|
||||
for pair_a in a.windows(2) {
|
||||
for pair_b in b.windows(2) {
|
||||
let Some(point) = segment_cross(pair_a[0], pair_a[1], pair_b[0], pair_b[1], false)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let error = (point[0] - hint_uv[0]).powi(2) + (point[1] - hint_uv[1]).powi(2);
|
||||
if best.is_none_or(|(_, previous)| error < previous) {
|
||||
best = Some((point, error));
|
||||
}
|
||||
let mut points: Vec<_> = cadkernel::geom2d::intersect(
|
||||
&first.curve,
|
||||
&second,
|
||||
cadkernel::geom2d::Tolerance::new(1e-9),
|
||||
)
|
||||
.into_iter()
|
||||
.map(|hit| hit.point)
|
||||
.collect();
|
||||
if apparent && points.is_empty() {
|
||||
// Extended apparent intersections are meaningful for straight lines.
|
||||
// Curved geometry must actually cross after projection.
|
||||
if let (KernelCurve::Line(a), KernelCurve::Line(b)) = (&first.curve, &second) {
|
||||
points.extend(segment_cross(a.start, a.end, b.start, b.end, true));
|
||||
}
|
||||
}
|
||||
if best.is_none() && apparent {
|
||||
// Nothing actually crosses: take the two segments nearest the stored
|
||||
// point and cross their infinite extensions.
|
||||
let nearest = |run: &[[f64; 2]]| -> Option<([f64; 2], [f64; 2])> {
|
||||
run.windows(2)
|
||||
.map(|pair| {
|
||||
let mid = [
|
||||
(pair[0][0] + pair[1][0]) * 0.5,
|
||||
(pair[0][1] + pair[1][1]) * 0.5,
|
||||
];
|
||||
let error =
|
||||
(mid[0] - hint_uv[0]).powi(2) + (mid[1] - hint_uv[1]).powi(2);
|
||||
((pair[0], pair[1]), error)
|
||||
})
|
||||
.min_by(|first, second| first.1.total_cmp(&second.1))
|
||||
.map(|(pair, _)| pair)
|
||||
};
|
||||
let (a0, a1) = nearest(&a)?;
|
||||
let (b0, b1) = nearest(&b)?;
|
||||
best = segment_cross(a0, a1, b0, b1, true).map(|point| (point, 0.0));
|
||||
}
|
||||
best.map(|(point, _)| lift(&first.plane, point))
|
||||
points
|
||||
.into_iter()
|
||||
.min_by(|a, b| {
|
||||
let error = |p: &[f64; 2]| (p[0] - hint_uv[0]).powi(2) + (p[1] - hint_uv[1]).powi(2);
|
||||
error(a).total_cmp(&error(b))
|
||||
})
|
||||
.map(|point| lift(&first.plane, point))
|
||||
}
|
||||
|
||||
/// Crossing of segments `a0->a1` and `b0->b1`. With `extend` the parameters
|
||||
@@ -633,3 +673,48 @@ fn segment_cross(
|
||||
}
|
||||
Some([a0[0] + d[0] * t, a0[1] + d[1] * t])
|
||||
}
|
||||
|
||||
/// Internal markers are zero-based; imported edge markers are one-based.
|
||||
fn segment_index(reference: &AssocDimensionReference) -> Option<usize> {
|
||||
let marker = if reference.main_subent_type == 2 {
|
||||
reference.main_gs_marker.checked_sub(1)?
|
||||
} else {
|
||||
reference.main_gs_marker
|
||||
};
|
||||
usize::try_from(marker).ok()
|
||||
}
|
||||
|
||||
fn imported_endpoint(
|
||||
entity: &EntityType,
|
||||
reference: &AssocDimensionReference,
|
||||
hint: Option<Vector3>,
|
||||
) -> Option<Vector3> {
|
||||
let planar = entity_curve(entity)?;
|
||||
let t = match reference.osnap_distance {
|
||||
v if v.abs() < 1e-9 => 0.0,
|
||||
v if (v - 1.0).abs() < 1e-9 => 1.0,
|
||||
_ => return None,
|
||||
};
|
||||
let point = match &planar.curve {
|
||||
KernelCurve::Polyline(_) => {
|
||||
let segment = segment_index(reference)?;
|
||||
let point = lift(
|
||||
&planar.plane,
|
||||
planar.curve.segments().get(segment)?.point_at(t),
|
||||
);
|
||||
// Exporters can leave stale GS markers after editing a polyline.
|
||||
// Beyond the unambiguous first-edge convention, require a real
|
||||
// stored snap point to validate this imported endpoint. Zero and
|
||||
// 2e50 sentinel points do not establish feature identity.
|
||||
if segment > 0 && !hint.is_some_and(|hint| distance_squared(point, hint) < 1e-10) {
|
||||
return None;
|
||||
}
|
||||
point
|
||||
}
|
||||
KernelCurve::Line(_) | KernelCurve::Arc(_) | KernelCurve::Nurbs(_) => {
|
||||
lift(&planar.plane, planar.curve.point_at(t))
|
||||
}
|
||||
_ => return None,
|
||||
};
|
||||
Some(point)
|
||||
}
|
||||
+1
-1
@@ -134,7 +134,7 @@ impl Scene {
|
||||
let _ = self.document.layers.add(layer);
|
||||
}
|
||||
|
||||
fn ensure_app_id(&mut self, name: &str) {
|
||||
pub(super) fn ensure_app_id(&mut self, name: &str) {
|
||||
if name.trim().is_empty() || self.document.app_ids.contains(name) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -84,6 +84,9 @@ impl Scene {
|
||||
continue;
|
||||
};
|
||||
|
||||
if !planar_pick_distance(&entity, model_point).is_some_and(|d| d <= aperture_model) {
|
||||
continue;
|
||||
}
|
||||
let mut paper_entity = entity;
|
||||
crate::scene::view::dispatch::apply_transform(
|
||||
&mut paper_entity,
|
||||
@@ -106,39 +109,6 @@ impl Scene {
|
||||
None
|
||||
}
|
||||
|
||||
/// Resolve a snapped INSERT handle into the innermost entity that owns the
|
||||
/// geometry, plus the INSERT path down to it.
|
||||
///
|
||||
/// The renderer names every wire of an expanded block with the **top-level
|
||||
/// INSERT's** handle (see `block_cache`), including wires belonging to
|
||||
/// nested inserts, so a snap inside a block reports the INSERT and nothing
|
||||
/// finer. This walks the block definition from that INSERT looking for the
|
||||
/// nearest measurable entity to `model_point`, which recovers the real
|
||||
/// source handle and the instance path for the association chain.
|
||||
///
|
||||
/// Returns `None` — and the caller then keeps the INSERT itself as the
|
||||
/// source, which still gives a correct, if coarse, association — when:
|
||||
/// * `top` is not an INSERT (the common case: `Some((top, vec![]))`);
|
||||
/// * the geometry is a type [`planar_pick_distance`] does not handle
|
||||
/// (text, hatch, spline, solid, 3-D geometry);
|
||||
/// * the nesting is deeper than eight levels.
|
||||
///
|
||||
/// Depth is not limited to one level: [`Scene::descend_block_instance`]
|
||||
/// recurses, and the returned path lists every INSERT from the outermost
|
||||
/// down to the one that directly contains the entity.
|
||||
pub fn resolve_block_snap_source(
|
||||
&self,
|
||||
top: Handle,
|
||||
model_point: DVec3,
|
||||
) -> Option<(Handle, Vec<Handle>)> {
|
||||
if !matches!(self.document.get_entity(top), Some(EntityType::Insert(_))) {
|
||||
return None;
|
||||
}
|
||||
let (entity, path) = self.resolve_measurable_entity(top, model_point, None)?;
|
||||
let handle = entity.as_entity().handle();
|
||||
handle.is_valid().then_some((handle, path))
|
||||
}
|
||||
|
||||
/// Nearest resident model wire of `viewport` to `model_point`, within
|
||||
/// `aperture` model units. Wires are the same tessellation the viewport
|
||||
/// draws, so this matches what the user sees.
|
||||
@@ -205,6 +175,16 @@ impl Scene {
|
||||
handle: Handle,
|
||||
model_point: DVec3,
|
||||
viewport: Option<Handle>,
|
||||
) -> Option<(EntityType, Vec<Handle>)> {
|
||||
self.resolve_measurable_feature(handle, model_point, viewport, None)
|
||||
}
|
||||
|
||||
pub(crate) fn resolve_measurable_feature(
|
||||
&self,
|
||||
handle: Handle,
|
||||
model_point: DVec3,
|
||||
viewport: Option<Handle>,
|
||||
kind: Option<crate::snap::SnapType>,
|
||||
) -> Option<(EntityType, Vec<Handle>)> {
|
||||
let entity = self.document.get_entity(handle)?;
|
||||
match entity {
|
||||
@@ -217,11 +197,12 @@ impl Scene {
|
||||
&mut path,
|
||||
0,
|
||||
viewport,
|
||||
kind,
|
||||
)?;
|
||||
Some((found.0, found.1))
|
||||
}
|
||||
other => {
|
||||
planar_pick_distance(other, model_point)?;
|
||||
feature_pick_distance(other, model_point, kind)?;
|
||||
Some((other.clone(), Vec::new()))
|
||||
}
|
||||
}
|
||||
@@ -237,6 +218,7 @@ impl Scene {
|
||||
path: &mut Vec<Handle>,
|
||||
depth: usize,
|
||||
viewport: Option<Handle>,
|
||||
kind: Option<crate::snap::SnapType>,
|
||||
) -> Option<(EntityType, Vec<Handle>)> {
|
||||
const MAX_DEPTH: usize = 8;
|
||||
if depth > MAX_DEPTH {
|
||||
@@ -245,6 +227,9 @@ impl Scene {
|
||||
let EntityType::Insert(insert) = &insert_entity else {
|
||||
return None;
|
||||
};
|
||||
if insert.row_count > 1 || insert.column_count > 1 {
|
||||
return None;
|
||||
}
|
||||
let insert_handle = insert.common.handle;
|
||||
let local = crate::scene::render_graph::insert_transform(&self.document, insert);
|
||||
let combined = local.then(&outer);
|
||||
@@ -273,8 +258,9 @@ impl Scene {
|
||||
&mut nested,
|
||||
depth + 1,
|
||||
viewport,
|
||||
kind,
|
||||
) {
|
||||
if let Some(distance) = planar_pick_distance(&entity, model_point) {
|
||||
if let Some(distance) = feature_pick_distance(&entity, model_point, kind) {
|
||||
if best.as_ref().is_none_or(|(d, _, _)| distance < *d) {
|
||||
let mut full = vec![insert_handle];
|
||||
full.append(&mut nested_path);
|
||||
@@ -294,6 +280,7 @@ impl Scene {
|
||||
let y = DVec3::new(m[0][1], m[1][1], m[2][1]);
|
||||
let magnitude = x.length_squared().max(y.length_squared());
|
||||
if curved
|
||||
&& kind != Some(crate::snap::SnapType::Center)
|
||||
&& (magnitude < 1e-24
|
||||
|| (x.length_squared() - y.length_squared()).abs() > magnitude * 1e-10
|
||||
|| x.dot(y).abs() > magnitude * 1e-10)
|
||||
@@ -305,7 +292,7 @@ impl Scene {
|
||||
&mut placed,
|
||||
&EntityTransform::Affine(combined),
|
||||
);
|
||||
let Some(distance) = planar_pick_distance(&placed, model_point) else {
|
||||
let Some(distance) = feature_pick_distance(&placed, model_point, kind) else {
|
||||
continue;
|
||||
};
|
||||
if best.as_ref().is_none_or(|(d, _, _)| distance < *d) {
|
||||
@@ -438,3 +425,45 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Distance to the particular feature accepted by a snap, rather than to the
|
||||
/// nearest curve. A circle's center is deliberately not on its circumference.
|
||||
pub(crate) fn feature_pick_distance(
|
||||
entity: &EntityType,
|
||||
point: DVec3,
|
||||
kind: Option<crate::snap::SnapType>,
|
||||
) -> Option<f64> {
|
||||
use crate::snap::SnapType as S;
|
||||
let points: Vec<acadrust::types::Vector3> = match kind {
|
||||
Some(S::Center) => match entity {
|
||||
EntityType::Circle(c) => vec![c.center_wcs()],
|
||||
EntityType::Arc(a) => vec![a.center_wcs()],
|
||||
EntityType::Ellipse(e) => vec![e.center],
|
||||
_ => return None,
|
||||
},
|
||||
Some(S::Endpoint) => crate::scene::dimension_assoc::source_points(entity),
|
||||
Some(S::Node) => match entity {
|
||||
EntityType::Point(p) => vec![p.location],
|
||||
_ => return None,
|
||||
},
|
||||
Some(S::Midpoint | S::Quadrant) => {
|
||||
let planar = crate::entities::curve::entity_curve(entity)?;
|
||||
cadkernel::geom2d::snap::characteristic_points(&planar.curve)
|
||||
.into_iter()
|
||||
.filter(|p| match kind {
|
||||
Some(S::Midpoint) => p.kind == cadkernel::geom2d::snap::SnapKind::Midpoint,
|
||||
_ => p.kind == cadkernel::geom2d::snap::SnapKind::Quadrant,
|
||||
})
|
||||
.map(|p| {
|
||||
let p = planar.plane.point_at(p.point);
|
||||
acadrust::types::Vector3::new(p[0], p[1], p[2])
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
_ => return planar_pick_distance(entity, point),
|
||||
};
|
||||
points
|
||||
.into_iter()
|
||||
.map(|p| (DVec3::new(p.x, p.y, p.z) - point).truncate().length())
|
||||
.min_by(f64::total_cmp)
|
||||
}
|
||||
Reference in new issue
Block a user