forked from 1b-agent/opencadstudio-upstream
Fix viewport dimension acquisition and preserve measurement scaling
This commit is contained in:
1 parent
369e0c9fc5
commit
2ee3782850
18 files changed
+957
-577
No files matched your search
+14
-3
@@ -160,6 +160,18 @@ impl Scene {
|
||||
})
|
||||
}
|
||||
|
||||
/// Visible, supported content viewports at a sheet point, topmost first.
|
||||
/// Shared by snapping and explicit dimension object picking.
|
||||
pub fn viewport_frames_at_paper_point(&self, paper: glam::DVec3) -> Vec<crate::scene::viewport_ref::ViewportFrame> {
|
||||
if self.current_layout == "Model" || self.active_viewport.is_some() { return Vec::new(); }
|
||||
self.layout_content_viewports().iter().rev().filter_map(|&handle| {
|
||||
if !self.viewport_displays_content(handle) || !self.viewport_displays_paper_point(handle, paper.truncate()) {
|
||||
return None;
|
||||
}
|
||||
self.viewport_frame(handle)
|
||||
}).collect()
|
||||
}
|
||||
|
||||
/// Does `paper` lie inside the viewport's displayed area?
|
||||
///
|
||||
/// Always bounded by the viewport's paper rectangle; a viewport with a
|
||||
@@ -184,9 +196,8 @@ impl Scene {
|
||||
}
|
||||
let poly = self.clip_boundary_polygon(vp.clip_boundary_handle, vp.center.z as f32);
|
||||
if poly.len() < 3 {
|
||||
// A clip entity we cannot tessellate: fall back to the rectangle
|
||||
// rather than refusing every snap in the viewport.
|
||||
return true;
|
||||
// An unsupported boundary cannot establish snap eligibility.
|
||||
return false;
|
||||
}
|
||||
point_in_polygon_xy(paper, &poly)
|
||||
}
|
||||
|
||||
+128
-114
@@ -21,7 +21,9 @@ use super::*;
|
||||
use crate::command::EntityTransform;
|
||||
use crate::scene::viewport_ref::ViewportFrame;
|
||||
use acadrust::types::{Matrix4, Transform};
|
||||
use glam::{DVec2, DVec3};
|
||||
#[cfg(test)]
|
||||
use glam::DVec2;
|
||||
use glam::DVec3;
|
||||
|
||||
/// A model entity resolved by clicking inside a paper-space content viewport.
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -44,36 +46,16 @@ pub struct ViewportDimPick {
|
||||
}
|
||||
|
||||
impl Scene {
|
||||
|
||||
/// The smallest *on* content viewport of the current layout whose paper
|
||||
/// rectangle contains `paper`. `None` on the Model tab, while editing
|
||||
/// inside a viewport (MSPACE), or when the point is on bare sheet.
|
||||
pub fn content_viewport_at_paper_point(&self, paper: DVec3) -> Option<Handle> {
|
||||
if self.current_layout == "Model" || self.active_viewport.is_some() {
|
||||
return None;
|
||||
}
|
||||
let (_, _, handles) = self.paper_viewport_handles();
|
||||
handles
|
||||
.iter()
|
||||
.filter_map(|handle| {
|
||||
let Some(EntityType::Viewport(vp)) = self.document.get_entity(*handle) else {
|
||||
return None;
|
||||
};
|
||||
if !vp.status.is_on || vp.width <= 0.0 || vp.height <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
let dx = (paper.x - vp.center.x).abs();
|
||||
let dy = (paper.y - vp.center.y).abs();
|
||||
(dx <= vp.width * 0.5 && dy <= vp.height * 0.5)
|
||||
.then(|| (*handle, vp.width * vp.height))
|
||||
})
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
.map(|(handle, _)| handle)
|
||||
self.viewport_frames_at_paper_point(paper)
|
||||
.first()
|
||||
.map(|f| f.viewport)
|
||||
}
|
||||
|
||||
/// [`Scene::content_viewport_at_paper_point`] resolved to a frame.
|
||||
pub fn viewport_frame_at_paper_point(&self, paper: DVec3) -> Option<ViewportFrame> {
|
||||
self.viewport_frame(self.content_viewport_at_paper_point(paper)?)
|
||||
self.viewport_frames_at_paper_point(paper)
|
||||
.into_iter()
|
||||
.next()
|
||||
}
|
||||
|
||||
/// Resolve an explicit dimension object pick made on the sheet but landing
|
||||
@@ -86,28 +68,42 @@ impl Scene {
|
||||
paper: DVec3,
|
||||
aperture_paper: f64,
|
||||
) -> Option<ViewportDimPick> {
|
||||
let frame = self.viewport_frame_at_paper_point(paper)?;
|
||||
let model_point = frame.paper_to_model(paper);
|
||||
let aperture_model =
|
||||
(aperture_paper.max(0.0) * frame.paper_to_model_length_factor()).max(1e-9);
|
||||
for frame in self.viewport_frames_at_paper_point(paper) {
|
||||
let model_point = frame.paper_to_model(paper);
|
||||
let aperture_model =
|
||||
(aperture_paper.max(0.0) * frame.paper_to_model_length_factor()).max(1e-9);
|
||||
|
||||
let top = self.nearest_model_wire_handle(frame.viewport, model_point, aperture_model)?;
|
||||
let (entity, block_path) = self.resolve_measurable_entity(top, model_point)?;
|
||||
let Some(top) =
|
||||
self.nearest_model_wire_handle(frame.viewport, model_point, aperture_model)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let Some((entity, block_path)) =
|
||||
self.resolve_measurable_entity(top, model_point, Some(frame.viewport))
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let mut paper_entity = entity;
|
||||
crate::scene::view::dispatch::apply_transform(
|
||||
&mut paper_entity,
|
||||
&EntityTransform::Affine(viewport_model_to_paper_transform(&frame)),
|
||||
);
|
||||
let entity_handle = paper_entity.as_entity().handle();
|
||||
Some(ViewportDimPick {
|
||||
frame,
|
||||
entity_handle: if entity_handle.is_valid() { entity_handle } else { top },
|
||||
block_path,
|
||||
paper_entity,
|
||||
paper_point: paper,
|
||||
model_point,
|
||||
})
|
||||
let mut paper_entity = entity;
|
||||
crate::scene::view::dispatch::apply_transform(
|
||||
&mut paper_entity,
|
||||
&EntityTransform::Affine(viewport_model_to_paper_transform(&frame)),
|
||||
);
|
||||
let entity_handle = paper_entity.as_entity().handle();
|
||||
return Some(ViewportDimPick {
|
||||
frame,
|
||||
entity_handle: if entity_handle.is_valid() {
|
||||
entity_handle
|
||||
} else {
|
||||
top
|
||||
},
|
||||
block_path,
|
||||
paper_entity,
|
||||
paper_point: paper,
|
||||
model_point,
|
||||
});
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Nearest resident model wire of `viewport` to `model_point`, within
|
||||
@@ -119,7 +115,23 @@ impl Scene {
|
||||
model_point: DVec3,
|
||||
aperture: f64,
|
||||
) -> Option<Handle> {
|
||||
let wires = self.model_wires_for_viewport_arc(viewport, 0.0);
|
||||
let camera = self.camera_for_viewport(viewport)?;
|
||||
let bounds = iced::Rectangle {
|
||||
x: 0.0,
|
||||
y: 0.0,
|
||||
width: 1000.0,
|
||||
height: 1000.0,
|
||||
};
|
||||
let radius_px =
|
||||
(aperture / (2.0 * camera.ortho_size() as f64) * bounds.height as f64) as f32;
|
||||
let wires = self.interaction_pick_candidates_near(
|
||||
self.model_wires_for_viewport_arc(viewport, 0.0),
|
||||
model_point,
|
||||
camera.view_proj_rte(bounds),
|
||||
camera.eye(),
|
||||
bounds,
|
||||
radius_px.max(1.0),
|
||||
);
|
||||
let mut best: Option<(f64, Handle)> = None;
|
||||
for wire in wires.iter() {
|
||||
if !wire.display_visible {
|
||||
@@ -133,8 +145,17 @@ impl Scene {
|
||||
.as_ref()
|
||||
.map(|inst| DVec3::from_array(inst.translation))
|
||||
.unwrap_or(DVec3::ZERO);
|
||||
let distance = wire_polyline_distance(wire, offset, model_point);
|
||||
if let Some(distance) = distance {
|
||||
let distance = wire_polyline_nearest(wire, offset, model_point);
|
||||
if let Some((distance, nearest)) = distance {
|
||||
let Some(frame) = self.viewport_frame(viewport) else {
|
||||
continue;
|
||||
};
|
||||
if !self.viewport_displays_paper_point(
|
||||
viewport,
|
||||
frame.model_to_paper(nearest).truncate(),
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if distance <= aperture && best.as_ref().is_none_or(|(d, _)| distance < *d) {
|
||||
best = Some((distance, handle));
|
||||
}
|
||||
@@ -146,10 +167,11 @@ impl Scene {
|
||||
/// Turn a picked handle into a measurable planar entity in model (WCS)
|
||||
/// coordinates, descending through block instances and baking the
|
||||
/// instance transform into the returned clone.
|
||||
fn resolve_measurable_entity(
|
||||
pub(crate) fn resolve_measurable_entity(
|
||||
&self,
|
||||
handle: Handle,
|
||||
model_point: DVec3,
|
||||
viewport: Option<Handle>,
|
||||
) -> Option<(EntityType, Vec<Handle>)> {
|
||||
let entity = self.document.get_entity(handle)?;
|
||||
match entity {
|
||||
@@ -161,6 +183,7 @@ impl Scene {
|
||||
model_point,
|
||||
&mut path,
|
||||
0,
|
||||
viewport,
|
||||
)?;
|
||||
Some((found.0, found.1))
|
||||
}
|
||||
@@ -180,6 +203,7 @@ impl Scene {
|
||||
model_point: DVec3,
|
||||
path: &mut Vec<Handle>,
|
||||
depth: usize,
|
||||
viewport: Option<Handle>,
|
||||
) -> Option<(EntityType, Vec<Handle>)> {
|
||||
const MAX_DEPTH: usize = 8;
|
||||
if depth > MAX_DEPTH {
|
||||
@@ -199,6 +223,14 @@ impl Scene {
|
||||
let Some(child_entity) = self.document.get_entity(child) else {
|
||||
continue;
|
||||
};
|
||||
let common = child_entity.common();
|
||||
let layer = self.document.layers.get(&common.layer);
|
||||
let frozen = viewport.and_then(|h| self.document.get_entity(h)).is_some_and(|entity| {
|
||||
matches!(entity, EntityType::Viewport(vp) if layer.is_some_and(|l| vp.frozen_layers.contains(&l.handle)))
|
||||
});
|
||||
if common.invisible || layer.is_some_and(|l| l.flags.off || l.flags.frozen) || frozen {
|
||||
continue;
|
||||
}
|
||||
if matches!(child_entity, EntityType::Insert(_)) {
|
||||
let mut nested = Vec::new();
|
||||
if let Some((entity, mut nested_path)) = self.descend_block_instance(
|
||||
@@ -207,6 +239,7 @@ impl Scene {
|
||||
model_point,
|
||||
&mut nested,
|
||||
depth + 1,
|
||||
viewport,
|
||||
) {
|
||||
if let Some(distance) = planar_pick_distance(&entity, model_point) {
|
||||
if best.as_ref().is_none_or(|(d, _, _)| distance < *d) {
|
||||
@@ -218,6 +251,22 @@ impl Scene {
|
||||
}
|
||||
continue;
|
||||
}
|
||||
// A nonuniform block transform turns circular geometry into an
|
||||
// ellipse. The entity transform API retains the circle type, so
|
||||
// decline this object pick instead of manufacturing a radius.
|
||||
let curved = matches!(child_entity, EntityType::Circle(_) | EntityType::Arc(_))
|
||||
|| matches!(child_entity, EntityType::LwPolyline(p) if p.vertices.iter().any(|v| v.bulge != 0.0));
|
||||
let m = &combined.matrix.m;
|
||||
let x = DVec3::new(m[0][0], m[1][0], m[2][0]);
|
||||
let y = DVec3::new(m[0][1], m[1][1], m[2][1]);
|
||||
let magnitude = x.length_squared().max(y.length_squared());
|
||||
if curved
|
||||
&& (magnitude < 1e-24
|
||||
|| (x.length_squared() - y.length_squared()).abs() > magnitude * 1e-10
|
||||
|| x.dot(y).abs() > magnitude * 1e-10)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let mut placed = child_entity.clone();
|
||||
crate::scene::view::dispatch::apply_transform(
|
||||
&mut placed,
|
||||
@@ -259,11 +308,11 @@ pub fn viewport_model_to_paper_transform(frame: &ViewportFrame) -> Transform {
|
||||
|
||||
/// Distance from `point` to a wire's polyline, in XY. `None` when the wire has
|
||||
/// no usable segment.
|
||||
fn wire_polyline_distance(
|
||||
fn wire_polyline_nearest(
|
||||
wire: &crate::scene::WireModel,
|
||||
offset: DVec3,
|
||||
point: DVec3,
|
||||
) -> Option<f64> {
|
||||
) -> Option<(f64, DVec3)> {
|
||||
let count = wire.points.len();
|
||||
if count == 0 {
|
||||
return None;
|
||||
@@ -278,6 +327,7 @@ fn wire_polyline_distance(
|
||||
) + offset
|
||||
};
|
||||
let mut best = f64::INFINITY;
|
||||
let mut best_point = DVec3::ZERO;
|
||||
let mut previous: Option<DVec3> = None;
|
||||
for i in 0..count {
|
||||
let current = at(i);
|
||||
@@ -285,77 +335,41 @@ fn wire_polyline_distance(
|
||||
previous = None;
|
||||
continue;
|
||||
}
|
||||
match previous {
|
||||
Some(start) => best = best.min(segment_distance_xy(start, current, point)),
|
||||
None => best = best.min((current.truncate() - point.truncate()).length()),
|
||||
let nearest = previous.map_or(current, |start| {
|
||||
let delta = current - start;
|
||||
let len2 = delta.truncate().length_squared();
|
||||
let t = if len2 > 1e-24 {
|
||||
((point - start).truncate().dot(delta.truncate()) / len2).clamp(0.0, 1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
start + delta * t
|
||||
});
|
||||
let distance = (nearest - point).truncate().length();
|
||||
if distance < best {
|
||||
best = distance;
|
||||
best_point = nearest;
|
||||
}
|
||||
previous = Some(current);
|
||||
}
|
||||
best.is_finite().then_some(best)
|
||||
}
|
||||
|
||||
fn segment_distance_xy(a: DVec3, b: DVec3, p: DVec3) -> f64 {
|
||||
let ab = b.truncate() - a.truncate();
|
||||
let ap = p.truncate() - a.truncate();
|
||||
let len2 = ab.length_squared();
|
||||
if len2 < 1e-24 {
|
||||
return ap.length();
|
||||
}
|
||||
let t = (ap.dot(ab) / len2).clamp(0.0, 1.0);
|
||||
(ap - ab * t).length()
|
||||
best.is_finite().then_some((best, best_point))
|
||||
}
|
||||
|
||||
/// Distance from `point` to the planar geometry object-pick dimensioning
|
||||
/// supports. `None` for entity types that cannot be dimensioned by picking.
|
||||
pub fn planar_pick_distance(entity: &EntityType, point: DVec3) -> Option<f64> {
|
||||
let p = point.truncate();
|
||||
match entity {
|
||||
EntityType::Line(line) => Some(segment_distance_xy(
|
||||
DVec3::new(line.start.x, line.start.y, line.start.z),
|
||||
DVec3::new(line.end.x, line.end.y, line.end.z),
|
||||
point,
|
||||
)),
|
||||
EntityType::Circle(circle) => {
|
||||
let c = DVec2::new(circle.center.x, circle.center.y);
|
||||
Some(((p - c).length() - circle.radius).abs())
|
||||
}
|
||||
EntityType::Arc(arc) => {
|
||||
let c = DVec2::new(arc.center.x, arc.center.y);
|
||||
Some(((p - c).length() - arc.radius).abs())
|
||||
}
|
||||
EntityType::LwPolyline(polyline) => polyline_distance(
|
||||
polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|v| DVec3::new(v.location.x, v.location.y, polyline.elevation)),
|
||||
polyline.is_closed,
|
||||
point,
|
||||
),
|
||||
EntityType::Polyline2D(polyline) => polyline_distance(
|
||||
polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|v| DVec3::new(v.location.x, v.location.y, polyline.elevation)),
|
||||
polyline.is_closed(),
|
||||
point,
|
||||
),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn polyline_distance(
|
||||
points: impl IntoIterator<Item = DVec3>,
|
||||
closed: bool,
|
||||
point: DVec3,
|
||||
) -> Option<f64> {
|
||||
let points: Vec<DVec3> = points.into_iter().collect();
|
||||
if points.len() < 2 {
|
||||
let planar = crate::entities::curve::entity_curve(entity)?;
|
||||
let normal = DVec3::from_array(planar.plane.normal()?);
|
||||
if normal.cross(DVec3::Z).length_squared() > 1e-12 {
|
||||
return None;
|
||||
}
|
||||
let count = if closed { points.len() } else { points.len() - 1 };
|
||||
(0..count)
|
||||
.map(|i| segment_distance_xy(points[i], points[(i + 1) % points.len()], point))
|
||||
.reduce(f64::min)
|
||||
let uv = planar.plane.project(point.to_array())?;
|
||||
let nearest = cadkernel::geom2d::closest_point(&planar.curve, uv);
|
||||
Some(
|
||||
(DVec3::from_array(planar.plane.point_at(nearest.point)) - point)
|
||||
.truncate()
|
||||
.length(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
+69
-104
@@ -1,20 +1,4 @@
|
||||
//! Shared interfaces for paper-space snapping through layout viewports and
|
||||
//! for viewport-aware dimension measurement.
|
||||
//!
|
||||
//! Three PRs build on this module concurrently:
|
||||
//! * PR1 (viewport snap) constructs [`ViewportFrame`] from the live viewport
|
||||
//! camera and fills [`SnapSourceRef`] / [`AcceptedSnap`] when a snap is
|
||||
//! accepted in paper space.
|
||||
//! * PR2 (dimension measurement) consumes [`AcceptedSnap`] and uses
|
||||
//! [`MeasurementScale`] to split the user's DIMLFAC from viewport
|
||||
//! compensation so the compensation is applied exactly once.
|
||||
//! * PR3 (associations) turns [`SnapSourceRef`] into persistent
|
||||
//! `AssocDimensionReference` chains (dimension -> viewport -> block path ->
|
||||
//! entity -> feature) and resolves them back.
|
||||
//!
|
||||
//! Anything here is a contract between those branches. Extend by adding
|
||||
//! fields/methods; do not rename or remove what exists without updating the
|
||||
//! sibling branches.
|
||||
//! Coordinates and feature identity shared by viewport acquisition and dimensions.
|
||||
|
||||
use acadrust::types::Handle;
|
||||
use glam::{DMat3, DVec2, DVec3};
|
||||
@@ -27,8 +11,7 @@ use crate::snap::SnapType;
|
||||
/// scale (paper units per model unit) and in-plane rotation (`twist`,
|
||||
/// radians, counter-clockwise, applied to model geometry before scaling).
|
||||
///
|
||||
/// PR1 owns construction (`Scene::viewport_frame`); everyone else only reads
|
||||
/// it. All arithmetic is `f64`.
|
||||
/// Constructed by `Scene::viewport_frame`. All arithmetic is `f64`.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ViewportFrame {
|
||||
/// Viewport entity handle.
|
||||
@@ -105,6 +88,8 @@ pub struct SnapSourceRef {
|
||||
pub source: DimensionAssociationSource,
|
||||
pub block_path: Vec<Handle>,
|
||||
pub snap_type: SnapType,
|
||||
/// The other entity/instance path for a true intersection.
|
||||
pub intersection: Option<Box<SnapSourceRef>>,
|
||||
}
|
||||
|
||||
/// A snap accepted by a point-input command, with enough information for
|
||||
@@ -149,31 +134,29 @@ impl AcceptedSnap {
|
||||
self.viewport.is_some()
|
||||
}
|
||||
|
||||
/// Build an accepted snap from a [`crate::snap::SnapResult`].
|
||||
///
|
||||
/// * `frame` `Some`: the snap ran against MODEL geometry through that
|
||||
/// viewport, so `snap.world` is a model point and the paper point is its
|
||||
/// projection onto the sheet.
|
||||
/// * `frame` `None`: the snap ran in the current space directly, so paper
|
||||
/// and model point coincide.
|
||||
///
|
||||
/// PR1 fills `source` from `SnapResult::source` (the owning entity handle
|
||||
/// the snap engine attributed the feature to); `block_path` stays empty
|
||||
/// until PR3 resolves block instance chains.
|
||||
/// `snap.world` is always in the command's current space. A projected
|
||||
/// viewport hit also retains its original model coordinate explicitly.
|
||||
pub fn from_snap(snap: &crate::snap::SnapResult, frame: Option<ViewportFrame>) -> Self {
|
||||
let (paper_point, model_point) = match &frame {
|
||||
Some(frame) => (frame.model_to_paper(snap.world), snap.world),
|
||||
None => (snap.world, snap.world),
|
||||
let frame = frame.filter(|frame| snap.viewport == Some(frame.viewport));
|
||||
let source_ref = |source| SnapSourceRef {
|
||||
source,
|
||||
block_path: Vec::new(),
|
||||
snap_type: snap.snap_type,
|
||||
intersection: None,
|
||||
};
|
||||
Self {
|
||||
paper_point,
|
||||
model_point,
|
||||
paper_point: snap.world,
|
||||
model_point: frame.map_or(snap.world, |frame| {
|
||||
snap.model_point
|
||||
.unwrap_or_else(|| frame.paper_to_model(snap.world))
|
||||
}),
|
||||
viewport: frame.map(|f| f.viewport),
|
||||
frame,
|
||||
source: snap.source.map(|source| SnapSourceRef {
|
||||
source,
|
||||
block_path: Vec::new(),
|
||||
snap_type: snap.snap_type,
|
||||
intersection: snap
|
||||
.secondary_source
|
||||
.map(|source| Box::new(source_ref(source))),
|
||||
..source_ref(source)
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -222,6 +205,53 @@ pub struct MeasurementScale {
|
||||
}
|
||||
|
||||
impl MeasurementScale {
|
||||
/// OCS bookkeeping keeps user scaling separate from the changing viewport
|
||||
/// scale. Standard DIMLFAC still carries the full factor for other readers.
|
||||
pub const APP_ID: &'static str = "OCS_VIEWPORT_MEASUREMENT";
|
||||
|
||||
pub fn read(data: &acadrust::xdata::ExtendedData) -> Option<Self> {
|
||||
use acadrust::xdata::XDataValue;
|
||||
let record = data.get_record(Self::APP_ID)?;
|
||||
let [XDataValue::Integer16(1), XDataValue::Real(user), XDataValue::Real(compensation)] =
|
||||
record.values.as_slice()
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
if !user.is_finite() || *user <= 0.0 || !compensation.is_finite() || *compensation <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
Some(Self {
|
||||
user_lfac: *user,
|
||||
viewport_compensation: *compensation,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write_to_entity(self, entity: &mut acadrust::EntityType) {
|
||||
use acadrust::xdata::{ExtendedData, ExtendedDataRecord, XDataValue};
|
||||
crate::entities::dim_override::set_on_entity(
|
||||
entity,
|
||||
crate::entities::dim_override::DIMLFAC,
|
||||
Some(XDataValue::Real(-self.paper_factor())),
|
||||
);
|
||||
let common = entity.common_mut();
|
||||
let mut data = ExtendedData::new();
|
||||
for record in common
|
||||
.extended_data
|
||||
.records()
|
||||
.iter()
|
||||
.filter(|r| r.application_name != Self::APP_ID)
|
||||
{
|
||||
data.add_record(record.clone());
|
||||
}
|
||||
let mut record = ExtendedDataRecord::new(Self::APP_ID);
|
||||
record.add_value(XDataValue::Integer16(1));
|
||||
record.add_value(XDataValue::Real(self.user_lfac));
|
||||
record.add_value(XDataValue::Real(self.viewport_compensation));
|
||||
data.add_record(record);
|
||||
// Parsed records are authoritative after changing measurement data.
|
||||
common.extended_data = data;
|
||||
}
|
||||
|
||||
pub const IDENTITY: Self = Self {
|
||||
user_lfac: 1.0,
|
||||
viewport_compensation: 1.0,
|
||||
@@ -272,8 +302,7 @@ impl MeasurementScale {
|
||||
///
|
||||
/// Rendering an *existing* dimension only knows whether the entity lives
|
||||
/// in a paper-space layout; the viewport compensation is already baked
|
||||
/// into the persisted magnitude (see
|
||||
/// [`MeasurementScale::viewport_dimlfac_override`]). This is the same rule
|
||||
/// into the persisted magnitude. This is the same rule
|
||||
/// as [`MeasurementScale::from_dimlfac`] with `viewport.is_some()`
|
||||
/// replaced by `paper_space`.
|
||||
pub fn user_lfac_for_space(dimlfac: f64, paper_space: bool) -> f64 {
|
||||
@@ -287,45 +316,6 @@ impl MeasurementScale {
|
||||
1.0
|
||||
}
|
||||
}
|
||||
|
||||
/// The persisted representation of viewport compensation, PR2's single
|
||||
/// source of truth.
|
||||
///
|
||||
/// A paper-space dimension that measures model geometry through a viewport
|
||||
/// keeps its definition points in **paper** coordinates and carries a
|
||||
/// **negative DIMLFAC override** whose magnitude is
|
||||
/// `user_lfac * viewport_compensation`. Because a viewport frame is a
|
||||
/// similarity (uniform scale + in-plane twist), the raw paper measurement
|
||||
/// times that magnitude is exactly the model measurement times the user's
|
||||
/// linear factor — so the compensation is applied exactly once, at
|
||||
/// formatting time, and round-trips through DXF/DWG unchanged (AutoCAD
|
||||
/// applies |DIMLFAC| to paper-space dimensions by the same rule).
|
||||
///
|
||||
/// Returns `None` when no override is needed:
|
||||
/// * the compensation is 1:1, or
|
||||
/// * the drawing/style DIMLFAC is already negative with exactly this
|
||||
/// magnitude (the drawing-wide DIMLFAC is set for this purpose).
|
||||
///
|
||||
/// `style_dimlfac` is the effective DIMLFAC the dimension would inherit
|
||||
/// *without* an override.
|
||||
pub fn viewport_dimlfac_override(style_dimlfac: f64, compensation: f64) -> Option<f64> {
|
||||
if !compensation.is_finite() || compensation.abs() < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
if (compensation - 1.0).abs() < 1e-12 {
|
||||
return None;
|
||||
}
|
||||
if style_dimlfac < 0.0
|
||||
&& ((-style_dimlfac) - compensation).abs() <= 1e-9 * compensation.abs().max(1.0)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
// A positive style DIMLFAC is the user's own multiplier and must be
|
||||
// preserved; a negative one of a *different* magnitude was meant for
|
||||
// another viewport scale, so the user multiplier there is 1.0.
|
||||
let user = if style_dimlfac > 0.0 { style_dimlfac } else { 1.0 };
|
||||
Some(-(user * compensation))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -392,29 +382,4 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn override_encodes_compensation_once() {
|
||||
// 1:10 viewport, no user factor -> -10, and a 10-unit paper span then
|
||||
// formats as 100.
|
||||
let ovr = MeasurementScale::viewport_dimlfac_override(1.0, 10.0).unwrap();
|
||||
assert!((ovr + 10.0).abs() < 1e-12);
|
||||
let lfac = MeasurementScale::user_lfac_for_space(ovr, true);
|
||||
assert!((10.0 * lfac - 100.0).abs() < 1e-9);
|
||||
// Same dimension read back in model space must not scale.
|
||||
assert_eq!(MeasurementScale::user_lfac_for_space(ovr, false), 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn override_preserves_user_multiplier_and_skips_redundant_cases() {
|
||||
assert_eq!(MeasurementScale::viewport_dimlfac_override(1.0, 1.0), None);
|
||||
// Drawing-wide negative DIMLFAC already carries this compensation.
|
||||
assert_eq!(MeasurementScale::viewport_dimlfac_override(-10.0, 10.0), None);
|
||||
// Positive user factor is folded in.
|
||||
let ovr = MeasurementScale::viewport_dimlfac_override(25.4, 10.0).unwrap();
|
||||
assert!((ovr + 254.0).abs() < 1e-9);
|
||||
// Negative but for a different scale -> user multiplier is 1.0.
|
||||
let ovr = MeasurementScale::viewport_dimlfac_override(-2.0, 10.0).unwrap();
|
||||
assert!((ovr + 10.0).abs() < 1e-12);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user