forked from 1b-agent/opencadstudio-upstream
Add shared viewport frame, accepted snap, and measurement scale interfaces
Contract module for the viewport snapping, viewport dimension measurement, and dimension association work that builds on it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
37cfa06b02
commit
49fe48f89c
3 files changed
+269
-1
No files matched your search
+1
-1
@@ -179,7 +179,7 @@ pub struct SelectionEntity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Association source with an optional sub-entity marker.
|
/// Association source with an optional sub-entity marker.
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct DimensionAssociationSource {
|
pub struct DimensionAssociationSource {
|
||||||
pub handle: Handle,
|
pub handle: Handle,
|
||||||
pub marker: Option<i32>,
|
pub marker: Option<i32>,
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ mod modify;
|
|||||||
pub mod named_parameters;
|
pub mod named_parameters;
|
||||||
mod named_parameters_persist;
|
mod named_parameters_persist;
|
||||||
mod mspace;
|
mod mspace;
|
||||||
|
pub mod viewport_ref;
|
||||||
mod page_setup;
|
mod page_setup;
|
||||||
mod paper;
|
mod paper;
|
||||||
mod preview;
|
mod preview;
|
||||||
|
|||||||
@@ -0,0 +1,267 @@
|
|||||||
|
//! 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.
|
||||||
|
|
||||||
|
use acadrust::types::Handle;
|
||||||
|
use glam::{DMat3, DVec2, DVec3};
|
||||||
|
|
||||||
|
use crate::command::DimensionAssociationSource;
|
||||||
|
use crate::snap::SnapType;
|
||||||
|
|
||||||
|
/// Planar orthographic mapping between a layout viewport's paper rectangle and
|
||||||
|
/// the model plane it displays. Handles pan (`model_target` / `paper_center`),
|
||||||
|
/// 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`.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct ViewportFrame {
|
||||||
|
/// Viewport entity handle.
|
||||||
|
pub viewport: Handle,
|
||||||
|
/// Paper-space centre of the viewport rectangle.
|
||||||
|
pub paper_center: DVec2,
|
||||||
|
/// Model-space point displayed at `paper_center`.
|
||||||
|
pub model_target: DVec2,
|
||||||
|
/// Paper units per model unit (e.g. 0.1 for a 1:10 viewport).
|
||||||
|
pub scale: f64,
|
||||||
|
/// In-plane rotation (DXF `twist_angle`), radians.
|
||||||
|
pub twist: f64,
|
||||||
|
/// `true` when the viewport is display-locked (snapping still allowed).
|
||||||
|
pub locked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ViewportFrame {
|
||||||
|
/// Model -> paper, as a 2-D affine matrix (homogeneous, column vectors).
|
||||||
|
pub fn model_to_paper_matrix(&self) -> DMat3 {
|
||||||
|
let (s, c) = self.twist.sin_cos();
|
||||||
|
let rs = DMat3::from_cols(
|
||||||
|
DVec3::new(c * self.scale, s * self.scale, 0.0),
|
||||||
|
DVec3::new(-s * self.scale, c * self.scale, 0.0),
|
||||||
|
DVec3::new(0.0, 0.0, 1.0),
|
||||||
|
);
|
||||||
|
let t_model = DMat3::from_translation(-self.model_target);
|
||||||
|
let t_paper = DMat3::from_translation(self.paper_center);
|
||||||
|
t_paper * rs * t_model
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Paper -> model (inverse of [`Self::model_to_paper_matrix`]).
|
||||||
|
pub fn paper_to_model_matrix(&self) -> DMat3 {
|
||||||
|
self.model_to_paper_matrix().inverse()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Project a model-space point onto the paper sheet. `z` passes through.
|
||||||
|
pub fn model_to_paper(&self, p: DVec3) -> DVec3 {
|
||||||
|
let q = self.model_to_paper_matrix().transform_point2(p.truncate());
|
||||||
|
DVec3::new(q.x, q.y, p.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Lift a paper-sheet point into model space. `z` passes through.
|
||||||
|
pub fn paper_to_model(&self, p: DVec3) -> DVec3 {
|
||||||
|
let q = self.paper_to_model_matrix().transform_point2(p.truncate());
|
||||||
|
DVec3::new(q.x, q.y, p.z)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Model -> paper for a direction (rotation + scale, no translation).
|
||||||
|
pub fn model_to_paper_dir(&self, d: DVec2) -> DVec2 {
|
||||||
|
self.model_to_paper_matrix().transform_vector2(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Factor a paper-space length must be multiplied by to recover the model
|
||||||
|
/// length it displays (`1 / scale`). This is the *viewport compensation*
|
||||||
|
/// used by [`MeasurementScale`].
|
||||||
|
pub fn paper_to_model_length_factor(&self) -> f64 {
|
||||||
|
if self.scale.abs() < 1e-12 {
|
||||||
|
1.0
|
||||||
|
} else {
|
||||||
|
1.0 / self.scale
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Identity of the geometry a snap landed on, in model space, including the
|
||||||
|
/// block-instance path when the entity lives inside a block reference.
|
||||||
|
///
|
||||||
|
/// `source.handle` is the innermost entity (the one owning the feature).
|
||||||
|
/// `block_path` is the ordered list of INSERT handles from the outermost
|
||||||
|
/// instance in the space down to the instance that directly contains the
|
||||||
|
/// entity; empty for top-level geometry.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct SnapSourceRef {
|
||||||
|
pub source: DimensionAssociationSource,
|
||||||
|
pub block_path: Vec<Handle>,
|
||||||
|
pub snap_type: SnapType,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A snap accepted by a point-input command, with enough information for
|
||||||
|
/// dimension commands to distinguish *placement* (paper) from *measurement*
|
||||||
|
/// (model) and for association creation to record where it came from.
|
||||||
|
///
|
||||||
|
/// Invariant: when `viewport` is `Some`, `paper_point` is the point on the
|
||||||
|
/// sheet and `model_point` is that same point in model space through the
|
||||||
|
/// viewport frame. When `viewport` is `None` the snap was on paper-space (or
|
||||||
|
/// model-space) geometry directly and `paper_point == model_point`.
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct AcceptedSnap {
|
||||||
|
/// Point in the coordinate system of the current space (paper when a
|
||||||
|
/// layout is active).
|
||||||
|
pub paper_point: DVec3,
|
||||||
|
/// Point in model space (identical to `paper_point` when `viewport` is
|
||||||
|
/// `None`).
|
||||||
|
pub model_point: DVec3,
|
||||||
|
/// The viewport the snap looked through, if any.
|
||||||
|
pub viewport: Option<Handle>,
|
||||||
|
/// The frame in effect when the snap was accepted (so a later measurement
|
||||||
|
/// uses the same scale even if the viewport is changed afterwards).
|
||||||
|
pub frame: Option<ViewportFrame>,
|
||||||
|
/// Geometry identity, when the snap landed on an entity feature (not a
|
||||||
|
/// grid/free point).
|
||||||
|
pub source: Option<SnapSourceRef>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AcceptedSnap {
|
||||||
|
/// A plain point in the current space with no viewport and no source.
|
||||||
|
pub fn free(point: DVec3) -> Self {
|
||||||
|
Self {
|
||||||
|
paper_point: point,
|
||||||
|
model_point: point,
|
||||||
|
viewport: None,
|
||||||
|
frame: None,
|
||||||
|
source: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn through_viewport(&self) -> bool {
|
||||||
|
self.viewport.is_some()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The two independent factors that turn a raw measured distance into the
|
||||||
|
/// number a linear/aligned/radial dimension displays.
|
||||||
|
///
|
||||||
|
/// * `user_lfac` is the drawing/style's DIMLFAC as the user intends it
|
||||||
|
/// (always positive here; the sign convention of DXF DIMLFAC is resolved by
|
||||||
|
/// [`MeasurementScale::from_dimlfac`]).
|
||||||
|
/// * `viewport_compensation` undoes the viewport's paper-per-model scale for
|
||||||
|
/// a dimension placed in paper space but measuring model geometry. It is
|
||||||
|
/// `1.0` for model-space dimensions and for paper-only dimensions.
|
||||||
|
///
|
||||||
|
/// Angular dimensions never use either factor.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
pub struct MeasurementScale {
|
||||||
|
pub user_lfac: f64,
|
||||||
|
pub viewport_compensation: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl MeasurementScale {
|
||||||
|
pub const IDENTITY: Self = Self {
|
||||||
|
user_lfac: 1.0,
|
||||||
|
viewport_compensation: 1.0,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Resolve the DXF DIMLFAC convention:
|
||||||
|
/// * `dimlfac > 0`: applies to every dimension.
|
||||||
|
/// * `dimlfac < 0`: `|dimlfac|` applies only to dimensions created in
|
||||||
|
/// paper space that measure model geometry through a viewport; model
|
||||||
|
/// space dimensions use `1.0`.
|
||||||
|
/// * `dimlfac == 0`: treated as `1.0`.
|
||||||
|
///
|
||||||
|
/// `viewport` supplies the compensation when the dimension measures
|
||||||
|
/// through a viewport; pass `None` for model-space or paper-only
|
||||||
|
/// dimensions.
|
||||||
|
pub fn from_dimlfac(dimlfac: f64, viewport: Option<&ViewportFrame>) -> Self {
|
||||||
|
let through_viewport = viewport.is_some();
|
||||||
|
let user_lfac = if dimlfac == 0.0 {
|
||||||
|
1.0
|
||||||
|
} else if dimlfac > 0.0 {
|
||||||
|
dimlfac
|
||||||
|
} else if through_viewport {
|
||||||
|
-dimlfac
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
|
let viewport_compensation = viewport
|
||||||
|
.map(ViewportFrame::paper_to_model_length_factor)
|
||||||
|
.unwrap_or(1.0);
|
||||||
|
Self {
|
||||||
|
user_lfac,
|
||||||
|
viewport_compensation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Combined factor applied to a distance measured on the paper sheet.
|
||||||
|
pub fn paper_factor(&self) -> f64 {
|
||||||
|
self.user_lfac * self.viewport_compensation
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Factor applied to a distance already measured in model space (the
|
||||||
|
/// viewport compensation must not be applied again).
|
||||||
|
pub fn model_factor(&self) -> f64 {
|
||||||
|
self.user_lfac
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn frame(scale: f64, twist: f64) -> ViewportFrame {
|
||||||
|
ViewportFrame {
|
||||||
|
viewport: Handle::NULL,
|
||||||
|
paper_center: DVec2::new(100.0, 50.0),
|
||||||
|
model_target: DVec2::new(1000.0, 2000.0),
|
||||||
|
scale,
|
||||||
|
twist,
|
||||||
|
locked: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn round_trips_with_scale_and_twist() {
|
||||||
|
let f = frame(0.1, 0.3);
|
||||||
|
let m = DVec3::new(1234.5, 1987.25, 0.0);
|
||||||
|
let p = f.model_to_paper(m);
|
||||||
|
let back = f.paper_to_model(p);
|
||||||
|
assert!((back - m).length() < 1e-9);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn scale_maps_lengths() {
|
||||||
|
let f = frame(0.1, 0.0);
|
||||||
|
let a = f.model_to_paper(DVec3::new(0.0, 0.0, 0.0));
|
||||||
|
let b = f.model_to_paper(DVec3::new(100.0, 0.0, 0.0));
|
||||||
|
assert!(((b - a).length() - 10.0).abs() < 1e-9);
|
||||||
|
assert!((f.paper_to_model_length_factor() - 10.0).abs() < 1e-12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn negative_dimlfac_only_applies_through_viewports() {
|
||||||
|
let f = frame(0.5, 0.0);
|
||||||
|
let through = MeasurementScale::from_dimlfac(-2.0, Some(&f));
|
||||||
|
assert_eq!(through.user_lfac, 2.0);
|
||||||
|
assert!((through.viewport_compensation - 2.0).abs() < 1e-12);
|
||||||
|
assert!((through.paper_factor() - 4.0).abs() < 1e-12);
|
||||||
|
assert!((through.model_factor() - 2.0).abs() < 1e-12);
|
||||||
|
|
||||||
|
let model = MeasurementScale::from_dimlfac(-2.0, None);
|
||||||
|
assert_eq!(model, MeasurementScale::IDENTITY);
|
||||||
|
|
||||||
|
let positive = MeasurementScale::from_dimlfac(3.0, None);
|
||||||
|
assert_eq!(positive.user_lfac, 3.0);
|
||||||
|
assert_eq!(positive.viewport_compensation, 1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user