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.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct DimensionAssociationSource {
|
||||
pub handle: Handle,
|
||||
pub marker: Option<i32>,
|
||||
|
||||
@@ -40,6 +40,7 @@ mod modify;
|
||||
pub mod named_parameters;
|
||||
mod named_parameters_persist;
|
||||
mod mspace;
|
||||
pub mod viewport_ref;
|
||||
mod page_setup;
|
||||
mod paper;
|
||||
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