forked from 1b-agent/opencadstudio-upstream
Track mixed paper and viewport dimension references
This commit is contained in:
1 parent
bd00d679a9
commit
5424a19482
28 files changed
+823
-846
No files matched your search
@@ -0,0 +1,19 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="350" viewBox="0 0 900 350">
|
||||||
|
<style>text{font-family:Arial,sans-serif;fill:#263238} .title{font-size:22px;font-weight:600} .label{font-size:16px} .small{font-size:13px;fill:#54616a} .model{stroke:#1479b8;stroke-width:4;fill:none} .paper{stroke:#a94610;stroke-width:2;stroke-dasharray:9 5} .dim{stroke:#263238;stroke-width:1.5;fill:none}</style>
|
||||||
|
<defs><marker id="arrow" viewBox="0 0 8 8" refX="4" refY="4" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M8 0L0 4L8 8" fill="#263238"/></marker></defs>
|
||||||
|
<rect width="900" height="350" rx="10" fill="white"/>
|
||||||
|
<text class="title" x="30" y="38">The dimension follows its paper-space centerline</text>
|
||||||
|
<rect x="30" y="65" width="400" height="220" fill="#f5f8fa" stroke="#c3ced5"/>
|
||||||
|
<rect x="470" y="65" width="400" height="220" fill="#f5f8fa" stroke="#c3ced5"/>
|
||||||
|
<text class="label" x="48" y="93">Before</text><text class="label" x="488" y="93">Centerline moved 1 paper unit right</text>
|
||||||
|
<path class="model" d="M85 160H385M525 160H825"/>
|
||||||
|
<circle cx="385" cy="160" r="5" fill="#1479b8"/><circle cx="825" cy="160" r="5" fill="#1479b8"/>
|
||||||
|
<path class="paper" d="M235 113V257M705 113V257"/>
|
||||||
|
<path class="dim" d="M385 171V244M825 171V244"/>
|
||||||
|
<path class="dim" d="M235 232H385" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
|
||||||
|
<path class="dim" d="M705 232H825" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
|
||||||
|
<rect x="291" y="217" width="38" height="28" fill="#f5f8fa"/><rect x="746" y="217" width="38" height="28" fill="#f5f8fa"/>
|
||||||
|
<text class="label" x="310" y="237" text-anchor="middle">50</text><text class="label" x="765" y="237" text-anchor="middle">40</text>
|
||||||
|
<text class="label" x="30" y="316">The model endpoint stays fixed. The dimension updates from 50 to 40.</text>
|
||||||
|
<text class="small" x="30" y="340">Association schematic; viewport 1:10, dimension style unit factor = 1, DIMASSOC = 2.</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.9 KiB |
@@ -0,0 +1,22 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="900" height="350" viewBox="0 0 900 350">
|
||||||
|
<style>text{font-family:Arial,sans-serif;fill:#263238} .title{font-size:22px;font-weight:600} .label{font-size:16px} .small{font-size:13px;fill:#54616a} .model{stroke:#1479b8;stroke-width:4;fill:none} .paper{stroke:#a94610;stroke-width:2;stroke-dasharray:9 5} .dim{stroke:#263238;stroke-width:1.5;fill:none}</style>
|
||||||
|
<defs><marker id="arrow" viewBox="0 0 8 8" refX="4" refY="4" markerWidth="6" markerHeight="6" orient="auto-start-reverse"><path d="M8 0L0 4L8 8" fill="#263238"/></marker></defs>
|
||||||
|
<rect width="900" height="350" rx="10" fill="white"/>
|
||||||
|
<text class="title" x="30" y="38">Dimension from a paper centerline to a model endpoint</text>
|
||||||
|
<rect x="30" y="65" width="430" height="245" fill="#f5f8fa" stroke="#c3ced5"/>
|
||||||
|
<text class="small" x="45" y="89">Viewport 1:10</text>
|
||||||
|
<path class="model" d="M100 160H400"/>
|
||||||
|
<circle cx="400" cy="160" r="5" fill="#1479b8"/>
|
||||||
|
<path class="paper" d="M250 110V260"/>
|
||||||
|
<text class="small" x="100" y="144">Model geometry</text>
|
||||||
|
<path class="dim" d="M400 171V244"/>
|
||||||
|
<path class="dim" d="M250 232H400" marker-start="url(#arrow)" marker-end="url(#arrow)"/>
|
||||||
|
<rect x="306" y="217" width="38" height="28" fill="#f5f8fa"/>
|
||||||
|
<text class="label" x="325" y="237" text-anchor="middle">50</text>
|
||||||
|
<text class="small" x="45" y="290">Brown dashed line: paper-space centerline</text>
|
||||||
|
<text class="label" x="500" y="116">5 paper units between the picked points</text>
|
||||||
|
<text class="label" x="500" y="155">5 × 10 = 50 model units</text>
|
||||||
|
<text class="label" x="500" y="206">The viewport supplies the length scale.</text>
|
||||||
|
<text class="label" x="500" y="240">Either pick order gives the same value.</text>
|
||||||
|
<text class="small" x="30" y="336">Measurement schematic; dimension style unit factor = 1.</text>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.8 KiB |
+57
-83
@@ -1,100 +1,74 @@
|
|||||||
# Dimensions through paper-space viewports
|
# Dimensions through layout viewports
|
||||||
|
|
||||||
Paper-space point commands can snap to displayed model geometry through a planar,
|
In paper space, snap to model geometry displayed through a planar, orthographic
|
||||||
orthographic layout viewport. The acquired result retains the original model
|
viewport. `DIMLINEAR`, `DIMALIGNED`, `DIMANGULAR`, `DIMRADIUS`, and `DIMDIAMETER`
|
||||||
coordinate, its paper coordinate, the viewport frame, and feature identity.
|
also support their usual object-pick modes through these viewports.
|
||||||
Ordinary selection continues to operate on the active drawing space.
|
|
||||||
|
|
||||||
`DIMLINEAR`, `DIMALIGNED`, `DIMANGULAR`, `DIMRADIUS`, and `DIMDIAMETER` use this
|
## Measurement
|
||||||
acquisition context. The supported object-pick modes also query viewport geometry.
|
|
||||||
Snapping and object picking share viewport draw order, visibility, and clipping
|
|
||||||
rules. Object picking uses the resident interaction index and applies block
|
|
||||||
instance transforms. Nonuniformly transformed circles and circular arcs are
|
|
||||||
declined as radial object picks because their displayed geometry is elliptical.
|
|
||||||
Array inserts without an addressable instance index are also declined; resolved
|
|
||||||
block geometry must remain within the original pick aperture.
|
|
||||||
|
|
||||||
## Input and measurement rules
|
- Model geometry supplies the viewport scale. A 100-unit model length displayed
|
||||||
|
at 1:10 measures 100, although it occupies 10 paper units.
|
||||||
|
- A paper-space centerline or free sheet point can be combined with model
|
||||||
|
geometry through one viewport, in either pick order. The paper-space span is
|
||||||
|
measured using that viewport's scale.
|
||||||
|
- Sheet-only dimensions measure paper units, even inside a viewport rectangle.
|
||||||
|
- Model picks from a second viewport are rejected without advancing the command.
|
||||||
|
Dimension-line and text placement do not change the measurement scale.
|
||||||
|
- The current dimension style's unit conversion is preserved. Angular dimensions
|
||||||
|
do not use length compensation. `DIMASSOC=0` explodes the compensated dimension.
|
||||||
|
|
||||||
- Free and typed sheet points measure paper units, including points inside a
|
Snapping and object picking respect visibility, clipping, and viewport draw order.
|
||||||
viewport rectangle.
|
Paper geometry has priority in object picks. Ordinary selection uses the active
|
||||||
- Measuring points must all belong to the same space and, when applicable, the
|
drawing space. Object picks reuse the resident interaction index and resolve
|
||||||
same viewport. An incompatible pick leaves the command at its current step.
|
nested block transforms.
|
||||||
- Dimension-line and text placement do not change the measurement's space.
|
|
||||||
- Paper geometry takes priority over viewport geometry at an object pick.
|
|
||||||
- The final dimension style is applied before computing measurement scaling.
|
|
||||||
Length dimensions keep paper-space definition points and a negative `DIMLFAC`
|
|
||||||
override containing the effective user factor multiplied by `1 / viewport scale`.
|
|
||||||
Angular measurements do not use this length factor.
|
|
||||||
- `DIMASSOC=0` uses the same compensated value before exploding the annotation.
|
|
||||||
|
|
||||||
## Persistent measurement data
|

|
||||||
|
|
||||||
`OCS_VIEWPORT_MEASUREMENT` XData stores three values: version `1` (integer16),
|
## Saved scale
|
||||||
the positive user factor (real), and viewport compensation (real). This keeps the
|
|
||||||
user's unit conversion separate from viewport scale. The ordinary `DIMLFAC`
|
|
||||||
override remains sufficient for displaying the saved measurement in other CAD
|
|
||||||
readers. The data survives DXF/DWG saves and undo/redo.
|
|
||||||
|
|
||||||
With `DIMASSOC=2`, acquired features attach a dimension to its viewport, each
|
Length dimensions store paper-space definition points and a negative `DIMLFAC`
|
||||||
block instance, and the source entity. Intersections retain both source chains.
|
override: user unit factor multiplied by `1 / viewport scale`.
|
||||||
Source edits and viewport changes update the definition points and recompute
|
`OCS_VIEWPORT_MEASUREMENT` XData stores version `1` (integer16), the positive user
|
||||||
compensation while preserving explicit user-positioned sheet text and dimension
|
factor (real), and viewport compensation (real). Keeping the factors separate
|
||||||
style overrides. A successful update invalidates the saved picture block so the
|
allows compensation to change without losing the user's unit conversion.
|
||||||
new geometry and number are rendered and exported. Undo restores both the
|
Both the override and XData survive DXF/DWG saves and undo/redo.
|
||||||
reference data and the previous picture.
|
|
||||||
|
|
||||||
Properties reports whether the dimension is associated, partially associated,
|
## Associations
|
||||||
unresolved, broken, or nonassociative. A broken or unsupported required reference
|
|
||||||
preserves the entire dimension's last valid appearance and its original records.
|
|
||||||
Creating another dimension does not announce geometry changes to its sources.
|
|
||||||
|
|
||||||
## Imported references and support limits
|
With `DIMASSOC=2`, acquired features retain their viewport, block-instance path,
|
||||||
|
source entity, and snap feature. Intersections retain both source chains.
|
||||||
|
Source edits and viewport changes update the dimension and its compensation,
|
||||||
|
preserving style overrides and user-positioned text on the sheet.
|
||||||
|
|
||||||
The resolver distinguishes the application's vertex/parameter convention from
|
A sheet centerline stays on the sheet; a model endpoint follows its viewport.
|
||||||
imported edge references. Imported start/end and midpoint modes use their edge
|
Moving the viewport therefore changes the distance between them. Undo/redo and
|
||||||
marker and parameter together. The original regression drawing's first-edge
|
DXF/DWG saves preserve the references.
|
||||||
start/end pair now reproduces its `0.128` measurement. An imported polyline
|
|
||||||
endpoint beyond the first edge requires a usable stored point to validate the
|
|
||||||
marker; zero/sentinel data cannot establish that identity. Ambiguous imports
|
|
||||||
remain unresolved. Imported viewport scale is updated only when existing OCS
|
|
||||||
metadata or `ACAD_DIMASSOC_CALC_DIMLFAC` separates compensation from user scaling.
|
|
||||||
|
|
||||||
Unsupported cases retain their saved data and appearance:
|
Properties shows association status. If a required reference is broken or cannot
|
||||||
|
be resolved, the entire dimension keeps its last valid geometry and reference
|
||||||
|
records. A successful refresh regenerates its displayed and exported geometry.
|
||||||
|
|
||||||
- Perspective/oblique viewport projections and malformed/missing reference chains.
|

|
||||||
- Ambiguous intersections within one batched block; they are not attached to a
|
|
||||||
guessed pair of children. Intersections between separate identified chains work.
|
|
||||||
- Array inserts without an addressable instance index, nesting beyond eight levels,
|
|
||||||
and radial measurements of nonuniformly transformed circular geometry.
|
|
||||||
- Imported quadrant references whose moved geometry no longer validates the stored
|
|
||||||
point; newly acquired circle/arc quadrants keep their source-local angle.
|
|
||||||
- Tangency with no verified solution, two dependent perpendicular/tangent references
|
|
||||||
requiring a coupled solve, unsupported snap modes, and imported measurement data that does not identify the old viewport compensation.
|
|
||||||
|
|
||||||
The osnap codes and reference fields follow the
|
## Limits
|
||||||
[Autodesk DIMASSOC reference](https://help.autodesk.com/cloudhelp/2024/ENU/AutoCAD-DXF/files/GUID-C0B96256-A911-4B4D-85E6-EB4AF2C91E27.htm).
|
|
||||||
The restricted imported-marker interpretation above is validated by fixtures;
|
|
||||||
it is not a claim that every exporter uses an interchangeable marker scheme.
|
|
||||||
|
|
||||||
## Validation
|
Unsupported cases preserve saved dimensions:
|
||||||
|
|
||||||
`src/app/viewport_dimension_tests.rs` exercises snap-engine acquisition, command
|
- Perspective or oblique views; missing or malformed reference chains.
|
||||||
commits for all five commands, object picks, input retries, active positive and
|
- Array instances without an addressable instance index, nesting beyond eight
|
||||||
negative style factors, exploded dimensions, undo/redo, DXF/DWG persistence,
|
levels, and radial measurements of nonuniformly scaled circles or arcs.
|
||||||
clipping, overlapping viewports, pan, scale, and twist. These are headless tests;
|
- Intersections without two identifiable source chains inside a batched block.
|
||||||
they do not establish visual acceptance in the desktop UI.
|
- Ambiguous imported endpoint, midpoint, or quadrant markers. A stored point can
|
||||||
|
validate a feature; it cannot establish an otherwise unknown identity.
|
||||||
|
- Tangency without a verified solution, or two perpendicular/tangent references
|
||||||
|
requiring a coupled solve.
|
||||||
|
- Imported viewport scaling without OCS scale metadata or
|
||||||
|
`ACAD_DIMASSOC_CALC_DIMLFAC` identifying the saved compensation.
|
||||||
|
|
||||||
Association tests also exercise regeneration, nearest and quadrant parameters,
|
## Tests
|
||||||
angular arc/polyline object picks, spline tangency,
|
|
||||||
perpendicularity under nonuniform block scale, nested paths, intersection source
|
|
||||||
edits, preserved overrides, erased sources, copy detachment, saved picture
|
|
||||||
invalidation, and history. The checked-in DXF fixture is exercised directly.
|
|
||||||
The original private drawing can be checked separately:
|
|
||||||
|
|
||||||
```powershell
|
Run `cargo test --locked --lib viewport_dimension`. Tests cover the five commands,
|
||||||
$env:OPENCAD_VIEWPORT_REGRESSION_DXF = 'C:\path\to\115448.dxf'
|
mixed sheet/model input, styles, clipping, object picks, source/viewport edits,
|
||||||
cargo test --locked --offline --lib viewport_dimension_original -- --ignored --nocapture
|
snap features, nested paths, intersections, preserved overrides, undo/redo, and
|
||||||
```
|
DXF/DWG persistence. The synthetic association fixture is in
|
||||||
|
`tests/fixtures/dim_assoc/`.
|
||||||
Do not treat passing headless checks as completed desktop visual acceptance.
|
|
||||||
+21
-27
@@ -98,7 +98,7 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
self.last_point = None;
|
self.last_point = None;
|
||||||
// Points collected in the space being left are meaningless in the new
|
// Points collected in the space being left are meaningless in the new
|
||||||
// one (PR1).
|
// one.
|
||||||
self.clear_accepted_snaps();
|
self.clear_accepted_snaps();
|
||||||
self.pending_click_snap = None;
|
self.pending_click_snap = None;
|
||||||
self.snapper.from_point = None;
|
self.snapper.from_point = None;
|
||||||
@@ -386,7 +386,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
// Typed / dynamic-input / headless points carry no snap, but the
|
// Typed / dynamic-input / headless points carry no snap, but the
|
||||||
// accepted-snap list must stay index-parallel with the points the
|
// accepted-snap list must stay index-parallel with the points the
|
||||||
// command collects (PR1). Interactive picks record themselves in
|
// command collects. Interactive picks record themselves in
|
||||||
// the click handler and never reach here.
|
// the click handler and never reach here.
|
||||||
if !self.record_accepted_snap(i, None, None, *point) {
|
if !self.record_accepted_snap(i, None, None, *point) {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -404,7 +404,9 @@ impl OpenCADStudio {
|
|||||||
self.push_ucs_to_cmd(i);
|
self.push_ucs_to_cmd(i);
|
||||||
}
|
}
|
||||||
if let StepInput::EntityPick(handle, point) = &input {
|
if let StepInput::EntityPick(handle, point) = &input {
|
||||||
if !self.dimension_acquisition_allowed(i, None) { return Task::none(); }
|
if !self.dimension_acquisition_allowed(i, None) {
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
let solid_pick = matches!(
|
let solid_pick = matches!(
|
||||||
self.tabs[i].scene.document.get_entity(*handle),
|
self.tabs[i].scene.document.get_entity(*handle),
|
||||||
Some(
|
Some(
|
||||||
@@ -477,7 +479,11 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
let ctrl = self.ctrl_down;
|
let ctrl = self.ctrl_down;
|
||||||
let shift = self.shift_down;
|
let shift = self.shift_down;
|
||||||
let picked_handle = if let StepInput::EntityPick(h, _) = &input { Some(*h) } else { None };
|
let picked_handle = if let StepInput::EntityPick(h, _) = &input {
|
||||||
|
Some(*h)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
let result: Option<CmdResult> = {
|
let result: Option<CmdResult> = {
|
||||||
let Some(cmd) = self.tabs[i].active_cmd.as_mut() else {
|
let Some(cmd) = self.tabs[i].active_cmd.as_mut() else {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
@@ -2366,17 +2372,15 @@ impl OpenCADStudio {
|
|||||||
// through a viewport carries the compensation as a negative
|
// through a viewport carries the compensation as a negative
|
||||||
// DIMLFAC override.
|
// DIMLFAC override.
|
||||||
if !preserve_base_style {
|
if !preserve_base_style {
|
||||||
crate::scene::creation_style::apply_current_creation_styles(&self.tabs[i].scene.document, &mut entity);
|
crate::scene::creation_style::apply_current_creation_styles(
|
||||||
|
&self.tabs[i].scene.document,
|
||||||
|
&mut entity,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if !self.apply_viewport_dimension_measurement(i, &mut entity) {
|
if !self.apply_viewport_dimension_measurement(i, &mut entity) {
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
// ...and is deliberately *not* associated in paper space: its
|
// Projected points cannot use direct paper-space source inference.
|
||||||
// definition points are projected paper coordinates, so a
|
|
||||||
// paper-space association would bind it to geometry that is
|
|
||||||
// not there. PR3 records the real dimension -> viewport ->
|
|
||||||
// entity chain instead. One gate, shared with
|
|
||||||
// `infer_dimension_sources_guarded`.
|
|
||||||
let association_allowed = self.dimension_association_allowed(i);
|
let association_allowed = self.dimension_association_allowed(i);
|
||||||
let inherited_dimension = if preserve_base_style {
|
let inherited_dimension = if preserve_base_style {
|
||||||
match &entity {
|
match &entity {
|
||||||
@@ -2438,20 +2442,15 @@ impl OpenCADStudio {
|
|||||||
} else {
|
} else {
|
||||||
let delta_safe = self.delta_add_safe(i, &entity);
|
let delta_safe = self.delta_add_safe(i, &entity);
|
||||||
let pending = self.begin_undo(i, label, 1, delta_safe);
|
let pending = self.begin_undo(i, label, 1, delta_safe);
|
||||||
if let Some(handle) = self.commit_entity_handle_with_dimension_policy(
|
if let Some(handle) =
|
||||||
entity,
|
self.commit_entity_handle_with_dimension_policy(entity, preserve_base_style)
|
||||||
preserve_base_style,
|
{
|
||||||
) {
|
|
||||||
if association_mode == 2 && association_allowed {
|
if association_mode == 2 && association_allowed {
|
||||||
let mut changes = vec![
|
let mut changes = vec![(handle, crate::scene::ChangeKind::Modified)];
|
||||||
(handle, crate::scene::ChangeKind::Modified),
|
|
||||||
];
|
|
||||||
match association {
|
match association {
|
||||||
crate::command::DimensionAssociationInput::Infer(source) => {
|
crate::command::DimensionAssociationInput::Infer(source) => {
|
||||||
let sources: Vec<_> = source.map_or_else(
|
let sources: Vec<_> = source.map_or_else(
|
||||||
|| {
|
|| self.infer_dimension_sources_guarded(i, handle),
|
||||||
self.infer_dimension_sources_guarded(i, handle)
|
|
||||||
},
|
|
||||||
|source| {
|
|source| {
|
||||||
if single_source_dimension {
|
if single_source_dimension {
|
||||||
vec![Some(source)]
|
vec![Some(source)]
|
||||||
@@ -2481,12 +2480,7 @@ impl OpenCADStudio {
|
|||||||
changes.dedup_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(&changes);
|
||||||
} else if association_mode == 2 {
|
} else if association_mode == 2 {
|
||||||
// Measured through a layout viewport: a paper-space
|
// Preserve the acquired viewport and source paths.
|
||||||
// association would bind projected coordinates to
|
|
||||||
// geometry that is not on the sheet. Record the real
|
|
||||||
// chain instead — dimension -> viewport -> block
|
|
||||||
// path -> entity -> feature — from the AcceptedSnaps
|
|
||||||
// PR1 captured, in definition-point slot order.
|
|
||||||
self.attach_viewport_dimension_association(i, handle);
|
self.attach_viewport_dimension_association(i, handle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ impl OpenCADStudio {
|
|||||||
// is not constrained by ortho/polar relative to a previous command's endpoint.
|
// is not constrained by ortho/polar relative to a previous command's endpoint.
|
||||||
self.last_point = None;
|
self.last_point = None;
|
||||||
// A new command collects its own points, so the previous command's
|
// A new command collects its own points, so the previous command's
|
||||||
// accepted snaps must not leak into it (PR1).
|
// accepted snaps must not leak into it.
|
||||||
self.clear_accepted_snaps();
|
self.clear_accepted_snaps();
|
||||||
// Starting a command restarts the right-click cycle, so its first
|
// Starting a command restarts the right-click cycle, so its first
|
||||||
// right-click acts as Enter rather than opening the context menu.
|
// right-click acts as Enter rather than opening the context menu.
|
||||||
|
|||||||
+26
-29
@@ -1,5 +1,5 @@
|
|||||||
//! Paper-space dimension acquisition and measurement. Only acquired geometry
|
//! Paper-space dimension acquisition and measurement. Acquiring model geometry
|
||||||
//! carries viewport compensation; free and typed sheet coordinates stay on the sheet.
|
//! supplies a viewport scale; sheet-only dimensions retain paper units.
|
||||||
|
|
||||||
use super::OpenCADStudio;
|
use super::OpenCADStudio;
|
||||||
use crate::command::DimensionAssociationSource;
|
use crate::command::DimensionAssociationSource;
|
||||||
@@ -14,7 +14,7 @@ use glam::DVec3;
|
|||||||
pub(crate) enum DimensionMeasureSpace {
|
pub(crate) enum DimensionMeasureSpace {
|
||||||
Direct,
|
Direct,
|
||||||
Viewport(ViewportFrame),
|
Viewport(ViewportFrame),
|
||||||
Mixed,
|
ConflictingViewports,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
@@ -26,33 +26,23 @@ impl OpenCADStudio {
|
|||||||
selection.left_dragging = false;
|
selection.left_dragging = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)] // Consumed by the dependent association change.
|
|
||||||
pub(crate) fn dimension_measuring_snaps(&self, _i: usize) -> Vec<AcceptedSnap> {
|
|
||||||
self.accepted_snaps().to_vec()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn dimension_measure_space(&self, i: usize) -> DimensionMeasureSpace {
|
pub(crate) fn dimension_measure_space(&self, i: usize) -> DimensionMeasureSpace {
|
||||||
let scene = &self.tabs[i].scene;
|
let scene = &self.tabs[i].scene;
|
||||||
if scene.current_layout == "Model" || scene.active_viewport.is_some() {
|
if scene.current_layout == "Model" || scene.active_viewport.is_some() {
|
||||||
return DimensionMeasureSpace::Direct;
|
return DimensionMeasureSpace::Direct;
|
||||||
}
|
}
|
||||||
let mut frame: Option<ViewportFrame> = None;
|
let mut frame: Option<ViewportFrame> = None;
|
||||||
let mut direct = false;
|
for f in self.accepted_snaps().iter().filter_map(|snap| snap.frame) {
|
||||||
for snap in self.accepted_snaps() {
|
match frame {
|
||||||
match snap.frame {
|
None => frame = Some(f),
|
||||||
Some(f) => match frame {
|
Some(previous) if previous.viewport == f.viewport => {}
|
||||||
None => frame = Some(f),
|
Some(_) => return DimensionMeasureSpace::ConflictingViewports,
|
||||||
Some(previous) if previous.viewport == f.viewport => {}
|
|
||||||
Some(_) => return DimensionMeasureSpace::Mixed,
|
|
||||||
},
|
|
||||||
None => direct = true,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
match (frame, direct) {
|
frame.map_or(
|
||||||
(None, _) => DimensionMeasureSpace::Direct,
|
DimensionMeasureSpace::Direct,
|
||||||
(Some(f), false) => DimensionMeasureSpace::Viewport(f),
|
DimensionMeasureSpace::Viewport,
|
||||||
_ => DimensionMeasureSpace::Mixed,
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Reject an incompatible measuring input before it advances the command.
|
/// Reject an incompatible measuring input before it advances the command.
|
||||||
@@ -68,8 +58,15 @@ impl OpenCADStudio {
|
|||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if self.accepted_snaps().iter().any(|s| s.viewport != viewport) {
|
// Sheet geometry and free sheet points may accompany one viewport.
|
||||||
self.command_line.push_error(crate::t!("Pick geometry from the same viewport, or use paper-space points for the whole dimension.").as_ref());
|
// A second viewport would make the measurement scale ambiguous.
|
||||||
|
if viewport.is_some_and(|viewport| {
|
||||||
|
self.accepted_snaps()
|
||||||
|
.iter()
|
||||||
|
.filter_map(|s| s.viewport)
|
||||||
|
.any(|previous| previous != viewport)
|
||||||
|
}) {
|
||||||
|
self.command_line.push_error(crate::t!("Model geometry must come from one viewport. Paper-space points can be included.").as_ref());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
@@ -134,8 +131,8 @@ impl OpenCADStudio {
|
|||||||
);
|
);
|
||||||
let frame = match self.dimension_measure_space(i) {
|
let frame = match self.dimension_measure_space(i) {
|
||||||
DimensionMeasureSpace::Direct => return true,
|
DimensionMeasureSpace::Direct => return true,
|
||||||
DimensionMeasureSpace::Mixed => {
|
DimensionMeasureSpace::ConflictingViewports => {
|
||||||
self.command_line.push_error(crate::t!("Pick geometry from the same viewport, or use paper-space points for the whole dimension.").as_ref());
|
self.command_line.push_error(crate::t!("Model geometry must come from one viewport. Paper-space points can be included.").as_ref());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
DimensionMeasureSpace::Viewport(f) => f,
|
DimensionMeasureSpace::Viewport(f) => f,
|
||||||
@@ -214,13 +211,13 @@ impl OpenCADStudio {
|
|||||||
if slots.is_empty() {
|
if slots.is_empty() {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
}
|
}
|
||||||
let measuring = self.dimension_measuring_snaps(i);
|
let measuring = self.accepted_snaps();
|
||||||
slots
|
slots
|
||||||
.iter()
|
.iter()
|
||||||
.enumerate()
|
.enumerate()
|
||||||
.map(|(index, slot)| {
|
.map(|(index, slot)| {
|
||||||
let snap = measuring.get(index)?.clone();
|
let snap = measuring.get(index)?.clone();
|
||||||
let frame = snap.frame?;
|
let frame = snap.frame;
|
||||||
let slot = DVec3::new(slot.x, slot.y, slot.z);
|
let slot = DVec3::new(slot.x, slot.y, slot.z);
|
||||||
if snap
|
if snap
|
||||||
.paper_point
|
.paper_point
|
||||||
@@ -232,7 +229,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
Some(AcceptedSnap {
|
Some(AcceptedSnap {
|
||||||
paper_point: slot,
|
paper_point: slot,
|
||||||
model_point: frame.paper_to_model(slot),
|
model_point: frame.map_or(slot, |frame| frame.paper_to_model(slot)),
|
||||||
..snap
|
..snap
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
+4
-13
@@ -717,21 +717,12 @@ pub(super) struct OpenCADStudio {
|
|||||||
show_layout_tabs: bool,
|
show_layout_tabs: bool,
|
||||||
/// Last point committed by a drawing command — used as ortho/polar base.
|
/// Last point committed by a drawing command — used as ortho/polar base.
|
||||||
last_point: Option<glam::DVec3>,
|
last_point: Option<glam::DVec3>,
|
||||||
/// Viewport frame the live snap marker is currently looking through (PR1).
|
/// Viewport used by the current snap; the displayed point is in paper space.
|
||||||
/// Set by the cursor-move handler when the displayed snap came from a
|
|
||||||
/// layout viewport, cleared otherwise. Display/bookkeeping only — the point
|
|
||||||
/// itself is already projected onto the sheet.
|
|
||||||
pub(crate) vp_snap_frame: Option<crate::scene::viewport_ref::ViewportFrame>,
|
pub(crate) vp_snap_frame: Option<crate::scene::viewport_ref::ViewportFrame>,
|
||||||
/// Snaps accepted by the active command's point steps, parallel to the
|
/// Acquired coordinates and source identities in command-step order.
|
||||||
/// points it collected (PR1). A command that does not care keeps using the
|
/// Cleared when the command starts or ends.
|
||||||
/// plain paper point; dimension commands (PR2) read the model point,
|
|
||||||
/// viewport and frame from here, and PR3 reads `source`. Cleared whenever a
|
|
||||||
/// command starts or ends.
|
|
||||||
accepted_snaps: Vec<crate::scene::viewport_ref::AcceptedSnap>,
|
accepted_snaps: Vec<crate::scene::viewport_ref::AcceptedSnap>,
|
||||||
/// Snap resolved by the click currently being handled, with the viewport
|
/// Click result retained until the command accepts its point.
|
||||||
/// frame it came through (PR1). Set inside the click handler's point
|
|
||||||
/// resolution so the point-accept path further down can record the
|
|
||||||
/// [`crate::scene::viewport_ref::AcceptedSnap`] without recomputing.
|
|
||||||
pending_click_snap: Option<(
|
pending_click_snap: Option<(
|
||||||
crate::snap::SnapResult,
|
crate::snap::SnapResult,
|
||||||
Option<crate::scene::viewport_ref::ViewportFrame>,
|
Option<crate::scene::viewport_ref::ViewportFrame>,
|
||||||
|
|||||||
+16
-5
@@ -1277,13 +1277,22 @@ impl OpenCADStudio {
|
|||||||
&self.tabs[i].scene.document,
|
&self.tabs[i].scene.document,
|
||||||
d.base().common.handle,
|
d.base().common.handle,
|
||||||
);
|
);
|
||||||
let statuses = self.tabs[i].scene.dimension_association_status(d.base().common.handle);
|
let statuses = self.tabs[i]
|
||||||
let slots = self.tabs[i].scene.dimension_association_slot_points(d.base().common.handle).len();
|
.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() {
|
let status = if statuses.is_empty() {
|
||||||
t!("Nonassociative")
|
t!("Nonassociative")
|
||||||
} else if statuses.iter().any(|(_, status)| matches!(status, crate::scene::ReferenceStatus::Broken(_))) {
|
} else if statuses.iter().any(|(_, status)| {
|
||||||
|
matches!(status, crate::scene::ReferenceStatus::Broken(_))
|
||||||
|
}) {
|
||||||
t!("Broken reference")
|
t!("Broken reference")
|
||||||
} else if statuses.iter().any(|(_, status)| matches!(status, crate::scene::ReferenceStatus::Unresolved)) {
|
} else if statuses.iter().any(|(_, status)| {
|
||||||
|
matches!(status, crate::scene::ReferenceStatus::Unresolved)
|
||||||
|
}) {
|
||||||
t!("Unresolved reference")
|
t!("Unresolved reference")
|
||||||
} else if statuses.len() < slots {
|
} else if statuses.len() < slots {
|
||||||
t!("Partially associated")
|
t!("Partially associated")
|
||||||
@@ -1293,7 +1302,9 @@ impl OpenCADStudio {
|
|||||||
general.props.push(crate::scene::model::object::Property {
|
general.props.push(crate::scene::model::object::Property {
|
||||||
label: t!("Association status").into_owned(),
|
label: t!("Association status").into_owned(),
|
||||||
field: "association_status",
|
field: "association_status",
|
||||||
value: crate::scene::model::object::PropValue::ReadOnly(status.into_owned()),
|
value: crate::scene::model::object::PropValue::ReadOnly(
|
||||||
|
status.into_owned(),
|
||||||
|
),
|
||||||
});
|
});
|
||||||
general.props.push(crate::scene::model::object::Property {
|
general.props.push(crate::scene::model::object::Property {
|
||||||
label: t!("Associative").into_owned(),
|
label: t!("Associative").into_owned(),
|
||||||
|
|||||||
+38
-18
@@ -2059,24 +2059,24 @@ impl OpenCADStudio {
|
|||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
// Paper-space snapping THROUGH layout viewports (PR1). `edit_cam`
|
// Paper-space snapping through layout viewports. `edit_cam`
|
||||||
// is None here, so `cursor_world` is the paper point and the sheet
|
// is None here, so `cursor_world` is the paper point and the sheet
|
||||||
// fills the canvas (pane-local == canvas pixels). The viewport hit
|
// fills the canvas (pane-local == canvas pixels). The viewport hit
|
||||||
// arrives already projected onto the sheet, so everything
|
// arrives already projected onto the sheet, so everything
|
||||||
// downstream keeps working in paper coordinates — LINE still draws
|
// downstream keeps working in paper coordinates — LINE still draws
|
||||||
// at the projected paper point.
|
// at the projected paper point.
|
||||||
self.vp_snap_frame = None;
|
self.vp_snap_frame = None;
|
||||||
if edit_cam.is_none() && !needs_entity && !is_gathering && !needs_structure
|
if edit_cam.is_none()
|
||||||
|
&& !needs_entity
|
||||||
|
&& !is_gathering
|
||||||
|
&& !needs_structure
|
||||||
&& !needs_tan
|
&& !needs_tan
|
||||||
{
|
{
|
||||||
if let Some((vp_hit, frame)) =
|
if let Some((vp_hit, frame)) =
|
||||||
self.paper_viewport_snap(i, p_full, vp_size, cursor_world)
|
self.paper_viewport_snap(i, p_full, vp_size, cursor_world)
|
||||||
{
|
{
|
||||||
let merged = crate::snap::merge_snap(
|
let merged =
|
||||||
self.tabs[i].snap_result,
|
crate::snap::merge_snap(self.tabs[i].snap_result, Some(vp_hit), p_full);
|
||||||
Some(vp_hit),
|
|
||||||
p_full,
|
|
||||||
);
|
|
||||||
if merged.is_some_and(|hit| hit.viewport.is_some()) {
|
if merged.is_some_and(|hit| hit.viewport.is_some()) {
|
||||||
self.vp_snap_frame = Some(frame);
|
self.vp_snap_frame = Some(frame);
|
||||||
}
|
}
|
||||||
@@ -3701,7 +3701,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Paper-space snapping through layout viewports (PR1). Mirrors
|
// Paper-space snapping through layout viewports. Mirrors
|
||||||
// the cursor-move pass: the click recomputes snapping from
|
// the cursor-move pass: the click recomputes snapping from
|
||||||
// scratch, so the viewport query has to run here too. The hit
|
// scratch, so the viewport query has to run here too. The hit
|
||||||
// comes back already projected onto the sheet.
|
// comes back already projected onto the sheet.
|
||||||
@@ -3710,8 +3710,7 @@ impl OpenCADStudio {
|
|||||||
if let Some((vp_hit, frame)) =
|
if let Some((vp_hit, frame)) =
|
||||||
self.paper_viewport_snap(i, p_full, (vw, vh), raw)
|
self.paper_viewport_snap(i, p_full, (vw, vh), raw)
|
||||||
{
|
{
|
||||||
let merged =
|
let merged = crate::snap::merge_snap(snap_hit, Some(vp_hit), p_full);
|
||||||
crate::snap::merge_snap(snap_hit, Some(vp_hit), p_full);
|
|
||||||
if merged.is_some_and(|hit| hit.viewport.is_some()) {
|
if merged.is_some_and(|hit| hit.viewport.is_some()) {
|
||||||
click_frame = Some(frame);
|
click_frame = Some(frame);
|
||||||
}
|
}
|
||||||
@@ -4046,7 +4045,10 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.dimension_acquisition_allowed(i, None) { self.finish_command_click(i); return Task::none(); }
|
if !self.dimension_acquisition_allowed(i, None) {
|
||||||
|
self.finish_command_click(i);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
let shift = self.shift_down;
|
let shift = self.shift_down;
|
||||||
let result = self.tabs[i].active_cmd.as_mut().map(|c| {
|
let result = self.tabs[i].active_cmd.as_mut().map(|c| {
|
||||||
// Shift-swap state for TRIM/EXTEND (#336).
|
// Shift-swap state for TRIM/EXTEND (#336).
|
||||||
@@ -4126,11 +4128,21 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
result
|
result
|
||||||
} else if let Some(result) = {
|
} else if let Some(result) = {
|
||||||
let dimension = self.tabs[i].active_cmd.as_ref().is_some_and(|c| c.measures_through_viewports());
|
let dimension = self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|c| c.measures_through_viewports());
|
||||||
if dimension {
|
if dimension {
|
||||||
let per_px = self.tabs[i].scene.camera.borrow().ortho_size() as f64 * 2.0 / (bounds.height as f64).max(1.0);
|
let per_px = self.tabs[i].scene.camera.borrow().ortho_size() as f64 * 2.0
|
||||||
self.try_dimension_viewport_entity_pick(i, pick_wcs, crate::ui::overlay::pick_box_aperture_px(self.pick_box) as f64 * per_px)
|
/ (bounds.height as f64).max(1.0);
|
||||||
} else { None }
|
self.try_dimension_viewport_entity_pick(
|
||||||
|
i,
|
||||||
|
pick_wcs,
|
||||||
|
crate::ui::overlay::pick_box_aperture_px(self.pick_box) as f64 * per_px,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
} {
|
} {
|
||||||
Some(result)
|
Some(result)
|
||||||
} else if self.tabs[i]
|
} else if self.tabs[i]
|
||||||
@@ -4139,7 +4151,12 @@ impl OpenCADStudio {
|
|||||||
.is_some_and(|command| command.entity_pick_accepts_points())
|
.is_some_and(|command| command.entity_pick_accepts_points())
|
||||||
{
|
{
|
||||||
let pending = self.pending_click_snap.take();
|
let pending = self.pending_click_snap.take();
|
||||||
if !self.record_accepted_snap(i, pending.map(|(hit, _)| hit), pending.and_then(|(_, frame)| frame), pick_wcs) {
|
if !self.record_accepted_snap(
|
||||||
|
i,
|
||||||
|
pending.map(|(hit, _)| hit),
|
||||||
|
pending.and_then(|(_, frame)| frame),
|
||||||
|
pick_wcs,
|
||||||
|
) {
|
||||||
self.finish_command_click(i);
|
self.finish_command_click(i);
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
@@ -4212,7 +4229,7 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Record what this point step accepted, alongside the point
|
// Record what this point step accepted, alongside the point
|
||||||
// itself (PR1): paper point, model point, viewport, frame and
|
// itself: paper point, model point, viewport, frame and
|
||||||
// geometry identity. Commands that don't care keep consuming
|
// geometry identity. Commands that don't care keep consuming
|
||||||
// `world_pt` exactly as before.
|
// `world_pt` exactly as before.
|
||||||
let pending = self.pending_click_snap.take();
|
let pending = self.pending_click_snap.take();
|
||||||
@@ -4221,7 +4238,10 @@ impl OpenCADStudio {
|
|||||||
pending.map(|(hit, _)| hit),
|
pending.map(|(hit, _)| hit),
|
||||||
pending.and_then(|(_, frame)| frame),
|
pending.and_then(|(_, frame)| frame),
|
||||||
world_pt,
|
world_pt,
|
||||||
) { self.finish_command_click(i); return Task::none(); }
|
) {
|
||||||
|
self.finish_command_click(i);
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
self.last_point = Some(world_pt);
|
self.last_point = Some(world_pt);
|
||||||
// The one-shot snap override is spent by this pick —
|
// The one-shot snap override is spent by this pick —
|
||||||
// restore the running osnap configuration (#337).
|
// restore the running osnap configuration (#337).
|
||||||
|
|||||||
@@ -1,24 +1,7 @@
|
|||||||
//! Paper-space snapping THROUGH layout viewports (PR1).
|
//! Snap to displayed model geometry while a paper-space command is active.
|
||||||
//!
|
//! Viewport queries use the renderer's resident wires and interaction index,
|
||||||
//! In a paper layout with no active viewport, `Scene::hit_test_wires` returns
|
//! then project the accepted model point onto the sheet. Ordinary selection
|
||||||
//! only the paper sheet's own entities — viewport contents are deliberately not
|
//! continues to use paper-space hit testing.
|
||||||
//! interactive, and that must stay true for *selection*. Snapping, however,
|
|
||||||
//! should see the model geometry a viewport displays, so a paper-space LINE /
|
|
||||||
//! DIMENSION can pick a real model endpoint and land on the paper pixel where
|
|
||||||
//! it is drawn.
|
|
||||||
//!
|
|
||||||
//! This module adds that as a SEPARATE query. It never changes
|
|
||||||
//! `hit_test_wires`. For each displayed content viewport under the cursor
|
|
||||||
//! (top-most first) it runs the EXISTING snap engine against that viewport's
|
|
||||||
//! resident model wires, using the viewport's own camera and screen rectangle
|
|
||||||
//! (`Scene::viewport_edit_frame_for` — the same adapter MSPACE editing uses),
|
|
||||||
//! then maps the accepted model point back onto the sheet with
|
|
||||||
//! [`ViewportFrame::model_to_paper`].
|
|
||||||
//!
|
|
||||||
//! Performance: no projected geometry is rebuilt per pointer event. The wire
|
|
||||||
//! set comes from `model_wires_for_viewport_arc`, which is the renderer's own
|
|
||||||
//! resident, camera-independent `Arc`, and the cursor-local broad phase reuses
|
|
||||||
//! the shared interaction index built over it.
|
|
||||||
|
|
||||||
use iced::Point;
|
use iced::Point;
|
||||||
|
|
||||||
@@ -34,37 +17,14 @@ const MAX_ACCEPTED_SNAPS: usize = 64;
|
|||||||
|
|
||||||
impl OpenCADStudio {
|
impl OpenCADStudio {
|
||||||
/// Snaps accepted by the active command's point steps, oldest first, in
|
/// Snaps accepted by the active command's point steps, oldest first, in
|
||||||
/// step order. Consumed by the dimension commands in PR2/PR3.
|
/// step order.
|
||||||
#[allow(dead_code)]
|
|
||||||
pub(crate) fn accepted_snaps(&self) -> &[AcceptedSnap] {
|
pub(crate) fn accepted_snaps(&self) -> &[AcceptedSnap] {
|
||||||
&self.accepted_snaps
|
&self.accepted_snaps
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The most recently accepted snap, if any.
|
/// Infer direct sources only when the measurement uses the active space.
|
||||||
#[allow(dead_code)]
|
/// Projected model points must retain their acquired source identities;
|
||||||
pub(crate) fn last_accepted_snap(&self) -> Option<&AcceptedSnap> {
|
/// proximity on the sheet could bind them to unrelated paper geometry.
|
||||||
self.accepted_snaps.last()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `Scene::infer_dimension_sources`, suppressed when the dimension being
|
|
||||||
/// created measures through a layout viewport.
|
|
||||||
///
|
|
||||||
/// Inference works by looking for geometry near the dimension's own
|
|
||||||
/// definition points, in whatever space it is handed. A paper-space
|
|
||||||
/// dimension measured through a viewport has PAPER definition points that
|
|
||||||
/// happen to sit over projected model geometry, so unguarded inference
|
|
||||||
/// would either associate it with unrelated paper-sheet geometry at those
|
|
||||||
/// coordinates, or match model geometry using paper coordinates. Neither
|
|
||||||
/// is correct, and a wrong association is worse than none. Returning an
|
|
||||||
/// empty source list makes `attach_dimension_association` a no-op.
|
|
||||||
///
|
|
||||||
/// The classification is
|
|
||||||
/// [`OpenCADStudio::dimension_measure_space`] — the *same* one that
|
|
||||||
/// decides whether to compensate the measurement, so the two can never
|
|
||||||
/// disagree. In particular it ignores the trailing dimension-line
|
|
||||||
/// placement click, which routinely lands inside a viewport rectangle for
|
|
||||||
/// an ordinary paper-space dimension and must not suppress its
|
|
||||||
/// association.
|
|
||||||
pub(crate) fn infer_dimension_sources_guarded(
|
pub(crate) fn infer_dimension_sources_guarded(
|
||||||
&self,
|
&self,
|
||||||
tab: usize,
|
tab: usize,
|
||||||
@@ -76,14 +36,8 @@ impl OpenCADStudio {
|
|||||||
self.tabs[tab].scene.infer_dimension_sources(dimension)
|
self.tabs[tab].scene.infer_dimension_sources(dimension)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The single gate on creating a paper-space association for the
|
/// Whether the dimension can use ordinary active-space associations.
|
||||||
/// dimension the active command is committing.
|
/// Placement clicks are excluded by the measurement classification.
|
||||||
///
|
|
||||||
/// `false` exactly when the dimension measures through (or partly through)
|
|
||||||
/// a layout viewport, whatever supplied its sources — inference,
|
|
||||||
/// an explicit object pick, or an explicit source list. Everything in the
|
|
||||||
/// commit path that used to decide this for itself now asks here, so the
|
|
||||||
/// measurement rule and the association rule cannot drift apart.
|
|
||||||
pub(crate) fn dimension_association_allowed(&self, tab: usize) -> bool {
|
pub(crate) fn dimension_association_allowed(&self, tab: usize) -> bool {
|
||||||
matches!(
|
matches!(
|
||||||
self.dimension_measure_space(tab),
|
self.dimension_measure_space(tab),
|
||||||
@@ -177,7 +131,7 @@ impl OpenCADStudio {
|
|||||||
) else {
|
) else {
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
let distance = crate::scene::viewport_dim_seam::feature_pick_distance(
|
let distance = crate::scene::viewport_dimension_pick::feature_pick_distance(
|
||||||
&entity,
|
&entity,
|
||||||
accepted.model_point,
|
accepted.model_point,
|
||||||
Some(source.snap_type),
|
Some(source.snap_type),
|
||||||
@@ -207,33 +161,15 @@ impl OpenCADStudio {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Append an already-built accepted snap, keeping the retention cap.
|
/// Retain a bounded history for point and object picks.
|
||||||
///
|
|
||||||
/// Used by paths that are not point steps (the dimension object pick
|
|
||||||
/// resolved through a viewport) and therefore never reach the click
|
|
||||||
/// handler.
|
|
||||||
pub(crate) fn push_accepted_snap(&mut self, accepted: AcceptedSnap) {
|
pub(crate) fn push_accepted_snap(&mut self, accepted: AcceptedSnap) {
|
||||||
if self.accepted_snaps.len() >= MAX_ACCEPTED_SNAPS {
|
if self.accepted_snaps.len() >= MAX_ACCEPTED_SNAPS {
|
||||||
self.accepted_snaps.remove(0);
|
self.accepted_snaps.remove(0);
|
||||||
}
|
}
|
||||||
self.accepted_snaps.push(accepted);
|
self.accepted_snaps.push(accepted);
|
||||||
}
|
}
|
||||||
/// Snap the paper-space cursor to model geometry seen through a layout
|
/// Query visible viewport geometry with a canvas-pixel cursor and sheet point.
|
||||||
/// viewport.
|
/// Returns paper coordinates and canvas pixels, plus the source viewport frame.
|
||||||
///
|
|
||||||
/// * `i` — tab index.
|
|
||||||
/// * `cursor_canvas` — cursor in canvas pixels.
|
|
||||||
/// * `canvas` — canvas size in pixels.
|
|
||||||
/// * `cursor_paper` — the paper-space point under the cursor.
|
|
||||||
///
|
|
||||||
/// Returns the hit expressed in PAPER coordinates (`world` projected onto
|
|
||||||
/// the sheet, `screen` in canvas pixels) together with the frame it came
|
|
||||||
/// through, so the caller can merge it with the ordinary paper-sheet snap
|
|
||||||
/// and later recover the model point.
|
|
||||||
///
|
|
||||||
/// `None` in the Model layout, while a viewport is active (MSPACE already
|
|
||||||
/// snaps in model space), when snapping is off, or when the cursor is not
|
|
||||||
/// over a displayed viewport.
|
|
||||||
pub(in crate::app) fn paper_viewport_snap(
|
pub(in crate::app) fn paper_viewport_snap(
|
||||||
&mut self,
|
&mut self,
|
||||||
i: usize,
|
i: usize,
|
||||||
@@ -316,10 +252,7 @@ impl OpenCADStudio {
|
|||||||
let Some(hit) = hit.filter(|h| h.snap_type != SnapType::Grid) else {
|
let Some(hit) = hit.filter(|h| h.snap_type != SnapType::Grid) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
// The wire set is the whole model, not a clipped copy, so the only
|
// The snap aperture can reach outside the visible clip boundary.
|
||||||
// way a feature outside the visible part could be offered is via
|
|
||||||
// the aperture reaching past the clip. Reject those: a snap must be
|
|
||||||
// a real feature the viewport actually displays.
|
|
||||||
let paper = frame.model_to_paper(hit.world);
|
let paper = frame.model_to_paper(hit.world);
|
||||||
if !self.tabs[i]
|
if !self.tabs[i]
|
||||||
.scene
|
.scene
|
||||||
|
|||||||
@@ -149,24 +149,13 @@ fn viewport_dimension_creation_preserves_active_units_and_roundtrips() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn viewport_dimension_mixed_and_degenerate_picks_allow_retry() {
|
fn viewport_dimension_conflicting_viewports_and_degenerate_picks_allow_retry() {
|
||||||
let (mut app, line, frame) = fixture();
|
let (mut app, line, frame) = fixture();
|
||||||
let i = app.active_tab;
|
let i = app.active_tab;
|
||||||
let _ = app.dispatch_command("DIMALIGNED");
|
let _ = app.dispatch_command("DIMALIGNED");
|
||||||
point(&mut app, frame, line, DVec3::ZERO);
|
point(&mut app, frame, line, DVec3::ZERO);
|
||||||
point(&mut app, frame, line, DVec3::ZERO);
|
point(&mut app, frame, line, DVec3::ZERO);
|
||||||
assert_eq!(app.accepted_snaps().len(), 1);
|
assert_eq!(app.accepted_snaps().len(), 1);
|
||||||
let _ = app.feed_command(StepInput::Point(DVec3::new(60.0, 50.0, 0.0)));
|
|
||||||
assert_eq!(app.accepted_snaps().len(), 1);
|
|
||||||
assert_eq!(
|
|
||||||
app.tabs[i]
|
|
||||||
.active_cmd
|
|
||||||
.as_ref()
|
|
||||||
.unwrap()
|
|
||||||
.dimension_acquired_points()
|
|
||||||
.len(),
|
|
||||||
1
|
|
||||||
);
|
|
||||||
let other = ViewportFrame {
|
let other = ViewportFrame {
|
||||||
viewport: Handle::new(0xFFFF),
|
viewport: Handle::new(0xFFFF),
|
||||||
..frame
|
..frame
|
||||||
@@ -318,7 +307,12 @@ fn viewport_dimension_eligibility_is_shared_and_uses_clipping() {
|
|||||||
let top = scene.add_entity(overlay);
|
let top = scene.add_entity(overlay);
|
||||||
let paper = DVec3::new(55.0, 50.0, 0.0);
|
let paper = DVec3::new(55.0, 50.0, 0.0);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
scene.viewport_frame_at_paper_point(paper).unwrap().viewport,
|
scene
|
||||||
|
.viewport_frames_at_paper_point(paper)
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.viewport,
|
||||||
top
|
top
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -340,7 +334,12 @@ fn viewport_dimension_eligibility_is_shared_and_uses_clipping() {
|
|||||||
vp.clip_boundary_handle = clip;
|
vp.clip_boundary_handle = clip;
|
||||||
}
|
}
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
scene.viewport_frame_at_paper_point(paper).unwrap().viewport,
|
scene
|
||||||
|
.viewport_frames_at_paper_point(paper)
|
||||||
|
.into_iter()
|
||||||
|
.next()
|
||||||
|
.unwrap()
|
||||||
|
.viewport,
|
||||||
first.viewport
|
first.viewport
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -354,7 +353,7 @@ fn viewport_dimension_eligibility_is_shared_and_uses_clipping() {
|
|||||||
if let Some(EntityType::Viewport(vp)) = scene.document.get_entity_mut(first.viewport) {
|
if let Some(EntityType::Viewport(vp)) = scene.document.get_entity_mut(first.viewport) {
|
||||||
vp.status.is_on = false;
|
vp.status.is_on = false;
|
||||||
}
|
}
|
||||||
assert!(scene.viewport_frame_at_paper_point(paper).is_none());
|
assert!(scene.viewport_frames_at_paper_point(paper).is_empty());
|
||||||
assert!(scene.dimension_pick_through_viewport(paper, 0.2).is_none());
|
assert!(scene.dimension_pick_through_viewport(paper, 0.2).is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -623,8 +622,8 @@ fn viewport_dimension_spline_tangency_is_parallel_and_validated() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "Set OPENCAD_VIEWPORT_REGRESSION_DXF to the original local regression drawing"]
|
#[ignore = "Set OPENCAD_VIEWPORT_REGRESSION_DXF to an external regression fixture"]
|
||||||
fn viewport_dimension_original_drawing_keeps_unchanged_measurements() {
|
fn viewport_dimension_external_fixture_keeps_unchanged_measurements() {
|
||||||
use crate::scene::ChangeKind;
|
use crate::scene::ChangeKind;
|
||||||
let path = std::env::var("OPENCAD_VIEWPORT_REGRESSION_DXF").expect("regression drawing path");
|
let path = std::env::var("OPENCAD_VIEWPORT_REGRESSION_DXF").expect("regression drawing path");
|
||||||
let doc = acadrust::DxfReader::from_file(std::path::Path::new(&path))
|
let doc = acadrust::DxfReader::from_file(std::path::Path::new(&path))
|
||||||
@@ -820,7 +819,9 @@ fn viewport_dimension_nested_block_path_survives_edit_and_copy() {
|
|||||||
let root = scene.add_entity(EntityType::Insert(root));
|
let root = scene.add_entity(EntityType::Insert(root));
|
||||||
scene.set_current_layout("Dimensions".into());
|
scene.set_current_layout("Dimensions".into());
|
||||||
let transform =
|
let transform =
|
||||||
crate::scene::dimension_assoc_chain::block_transform(&scene.document, &[root, nested]);
|
crate::scene::dimension_assoc_chain::walk_chain(&scene.document, &[root, nested, leaf])
|
||||||
|
.unwrap()
|
||||||
|
.transform;
|
||||||
let model = |x| {
|
let model = |x| {
|
||||||
let v = transform.apply(Vector3::new(x, 0.0, 0.0));
|
let v = transform.apply(Vector3::new(x, 0.0, 0.0));
|
||||||
DVec3::new(v.x, v.y, v.z)
|
DVec3::new(v.x, v.y, v.z)
|
||||||
@@ -1095,3 +1096,249 @@ fn viewport_dimension_quadrant_keeps_its_feature_after_translation() {
|
|||||||
.bump_entities(&[(source, ChangeKind::Modified)]);
|
.bump_entities(&[(source, ChangeKind::Modified)]);
|
||||||
assert!((displayed(&app.tabs[i].scene.document) - 400.0).abs() < 1e-5);
|
assert!((displayed(&app.tabs[i].scene.document) - 400.0).abs() < 1e-5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn start_centerline_dimension(
|
||||||
|
command: &str,
|
||||||
|
paper_first: bool,
|
||||||
|
paper_kind: SnapType,
|
||||||
|
factor: f64,
|
||||||
|
) -> (OpenCADStudio, Handle, Handle, ViewportFrame) {
|
||||||
|
let (mut app, model, frame) = fixture();
|
||||||
|
let i = app.active_tab;
|
||||||
|
let paper = app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.add_entity(EntityType::Line(Line::from_points(
|
||||||
|
Vector3::new(55.0, 40.0, 0.0),
|
||||||
|
Vector3::new(55.0, 60.0, 0.0),
|
||||||
|
)));
|
||||||
|
let mut style = acadrust::tables::DimStyle::new("CenterlineUnits");
|
||||||
|
style.dimlfac = factor;
|
||||||
|
app.tabs[i].scene.document.dim_styles.add(style).unwrap();
|
||||||
|
app.tabs[i].scene.document.header.current_dimstyle_name = "CenterlineUnits".into();
|
||||||
|
let _ = app.dispatch_command(command);
|
||||||
|
let mut paper_hit = hit(frame, paper, DVec3::new(55.0, 50.0, 0.0));
|
||||||
|
paper_hit.world = DVec3::new(55.0, 50.0, 0.0);
|
||||||
|
paper_hit.model_point = None;
|
||||||
|
paper_hit.viewport = None;
|
||||||
|
paper_hit.snap_type = paper_kind;
|
||||||
|
let mut picks = [
|
||||||
|
(paper_hit, None),
|
||||||
|
(hit(frame, model, DVec3::X * 100.0), Some(frame)),
|
||||||
|
];
|
||||||
|
if !paper_first {
|
||||||
|
picks.reverse();
|
||||||
|
}
|
||||||
|
for (snap, context) in picks {
|
||||||
|
assert!(app.record_accepted_snap(i, Some(snap), context, snap.world));
|
||||||
|
let result = app.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_mut()
|
||||||
|
.unwrap()
|
||||||
|
.on_point(snap.world);
|
||||||
|
app.sync_dimension_snaps(i);
|
||||||
|
let _ = app.apply_cmd_result(result);
|
||||||
|
}
|
||||||
|
(app, model, paper, frame)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn viewport_dimension_paper_centerline_uses_viewport_units_in_either_order() {
|
||||||
|
for command in ["DIMLINEAR", "DIMALIGNED"] {
|
||||||
|
for paper_first in [true, false] {
|
||||||
|
let (mut app, _, paper, _) =
|
||||||
|
start_centerline_dimension(command, paper_first, SnapType::Midpoint, 25.4);
|
||||||
|
let paper_snap = app
|
||||||
|
.accepted_snaps()
|
||||||
|
.iter()
|
||||||
|
.find(|s| s.viewport.is_none())
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(paper_snap.source.as_ref().unwrap().source.handle, paper);
|
||||||
|
assert_eq!(paper_snap.paper_point, paper_snap.model_point);
|
||||||
|
let _ = app.feed_command(StepInput::Point(DVec3::new(60.0, 80.0, 0.0)));
|
||||||
|
let doc = &app.tabs[app.active_tab].scene.document;
|
||||||
|
assert!(
|
||||||
|
(displayed(doc) - 1270.0).abs() < 1e-3,
|
||||||
|
"{command}: {}",
|
||||||
|
displayed(doc)
|
||||||
|
);
|
||||||
|
app.undo_steps(1);
|
||||||
|
app.redo_steps(1);
|
||||||
|
assert!((displayed(&app.tabs[app.active_tab].scene.document) - 1270.0).abs() < 1e-3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn viewport_dimension_paper_centerline_tracks_each_owning_space() {
|
||||||
|
use crate::scene::{ChangeKind, ReferenceStatus};
|
||||||
|
for (paper_first, kind) in [(true, SnapType::Midpoint), (false, SnapType::Perpendicular)] {
|
||||||
|
let (mut app, model, paper, frame) =
|
||||||
|
start_centerline_dimension("DIMALIGNED", paper_first, kind, 1.0);
|
||||||
|
let i = app.active_tab;
|
||||||
|
let _ = app.feed_command(StepInput::Point(DVec3::new(60.0, 80.0, 0.0)));
|
||||||
|
let dim = dimension(&app.tabs[i].scene.document).base().common.handle;
|
||||||
|
assert!((displayed(&app.tabs[i].scene.document) - 50.0).abs() < 1e-4);
|
||||||
|
let assoc = app.tabs[i].scene.dimension_association(dim).unwrap();
|
||||||
|
assert!(assoc.trans_space);
|
||||||
|
assert!(assoc
|
||||||
|
.references
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.any(|r| r.xrefs == vec![paper]));
|
||||||
|
assert!(assoc
|
||||||
|
.references
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.any(|r| r.xrefs == vec![frame.viewport, model]));
|
||||||
|
assert!(app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.dimension_association_status(dim)
|
||||||
|
.iter()
|
||||||
|
.all(|(_, s)| *s == ReferenceStatus::Resolved));
|
||||||
|
let pending = app.begin_undo(i, "Move paper centerline", 1, true).unwrap();
|
||||||
|
let before = app.tabs[i].scene.document.get_entity_arc(paper);
|
||||||
|
app.tabs[i].scene.record_undo_before(paper, before);
|
||||||
|
let EntityType::Line(line) = app.tabs[i].scene.document.get_entity_mut(paper).unwrap()
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
line.start.x = 56.0;
|
||||||
|
line.end.x = 56.0;
|
||||||
|
app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.bump_entities(&[(paper, ChangeKind::Modified)]);
|
||||||
|
app.commit_undo_delta(i, pending);
|
||||||
|
assert!((displayed(&app.tabs[i].scene.document) - 40.0).abs() < 1e-4);
|
||||||
|
app.undo_steps(1);
|
||||||
|
assert!((displayed(&app.tabs[i].scene.document) - 50.0).abs() < 1e-4);
|
||||||
|
app.redo_steps(1);
|
||||||
|
let EntityType::Viewport(vp) = app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity_mut(frame.viewport)
|
||||||
|
.unwrap()
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
vp.view_height = 500.0;
|
||||||
|
vp.custom_scale = 0.2;
|
||||||
|
app.tabs[i].scene.notify_viewport_changed(frame.viewport);
|
||||||
|
// Model endpoint now appears at paper x=70; paper centerline stays x=56.
|
||||||
|
assert!((displayed(&app.tabs[i].scene.document) - 70.0).abs() < 1e-4);
|
||||||
|
let EntityType::Viewport(vp) = app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity_mut(frame.viewport)
|
||||||
|
.unwrap()
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
vp.view_target.x = 20.0;
|
||||||
|
app.tabs[i].scene.notify_viewport_changed(frame.viewport);
|
||||||
|
assert!((displayed(&app.tabs[i].scene.document) - 50.0).abs() < 1e-4);
|
||||||
|
let EntityType::Line(line) = app.tabs[i].scene.document.get_entity_mut(model).unwrap()
|
||||||
|
else {
|
||||||
|
panic!()
|
||||||
|
};
|
||||||
|
line.end.x = 160.0;
|
||||||
|
app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.bump_entities(&[(model, ChangeKind::Modified)]);
|
||||||
|
assert!((displayed(&app.tabs[i].scene.document) - 110.0).abs() < 1e-4);
|
||||||
|
for ext in ["dxf", "dwg"] {
|
||||||
|
let doc = &app.tabs[i].scene.document;
|
||||||
|
let bytes = crate::io::save_to_bytes(doc, ext, doc.version).unwrap();
|
||||||
|
let mut scene = Scene::new();
|
||||||
|
scene.document = crate::io::load_bytes(&format!("centerline.{ext}"), bytes).unwrap();
|
||||||
|
scene.notify_viewport_changed(frame.viewport);
|
||||||
|
assert!((displayed(&scene.document) - 110.0).abs() < 1e-4, "{ext}");
|
||||||
|
assert!(scene
|
||||||
|
.dimension_association_status(dim)
|
||||||
|
.iter()
|
||||||
|
.all(|(_, s)| *s == ReferenceStatus::Resolved));
|
||||||
|
}
|
||||||
|
let before = app.tabs[i].scene.document.get_entity(dim).unwrap().clone();
|
||||||
|
let model_owner = app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity(model)
|
||||||
|
.unwrap()
|
||||||
|
.common()
|
||||||
|
.owner_handle;
|
||||||
|
app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity_mut(paper)
|
||||||
|
.unwrap()
|
||||||
|
.common_mut()
|
||||||
|
.owner_handle = model_owner;
|
||||||
|
app.tabs[i].scene.notify_viewport_changed(frame.viewport);
|
||||||
|
assert_eq!(app.tabs[i].scene.document.get_entity(dim).unwrap(), &before);
|
||||||
|
assert!(app.tabs[i]
|
||||||
|
.scene
|
||||||
|
.dimension_association_status(dim)
|
||||||
|
.iter()
|
||||||
|
.any(|(_, s)| *s == ReferenceStatus::Unresolved));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn viewport_dimension_invalid_paths_leave_no_partial_association() {
|
||||||
|
use crate::scene::viewport_ref::SnapSourceRef;
|
||||||
|
use acadrust::entities::{DimensionAligned, Insert};
|
||||||
|
use acadrust::tables::BlockRecord;
|
||||||
|
|
||||||
|
let (mut app, model, frame) = fixture();
|
||||||
|
let scene = &mut app.tabs[app.active_tab].scene;
|
||||||
|
let mut block = BlockRecord::new("Collapsed");
|
||||||
|
block.handle = scene.document.allocate_handle();
|
||||||
|
let owner = block.handle;
|
||||||
|
scene.document.block_records.add(block).unwrap();
|
||||||
|
let mut leaf = Line::from_points(Vector3::ZERO, Vector3::UNIT_X);
|
||||||
|
leaf.common.owner_handle = owner;
|
||||||
|
let leaf = scene.document.add_entity(EntityType::Line(leaf)).unwrap();
|
||||||
|
scene.set_current_layout("Model".into());
|
||||||
|
let mut insert = Insert::new("Collapsed", Vector3::ZERO);
|
||||||
|
insert.set_x_scale(0.0);
|
||||||
|
insert.set_y_scale(0.0);
|
||||||
|
let root = scene.add_entity(EntityType::Insert(insert));
|
||||||
|
scene.set_current_layout("Dimensions".into());
|
||||||
|
let paper = scene.add_entity(EntityType::Line(Line::from_points(
|
||||||
|
Vector3::new(55.0, 50.0, 0.0),
|
||||||
|
Vector3::new(55.0, 60.0, 0.0),
|
||||||
|
)));
|
||||||
|
for (source, path) in [(model, vec![Handle::new(u64::MAX)]), (leaf, vec![root])] {
|
||||||
|
let dim = scene.add_entity(EntityType::Dimension(Dimension::Aligned(
|
||||||
|
DimensionAligned::new(Vector3::new(50.0, 50.0, 0.0), Vector3::new(55.0, 50.0, 0.0)),
|
||||||
|
)));
|
||||||
|
let invalid = AcceptedSnap {
|
||||||
|
paper_point: DVec3::new(50.0, 50.0, 0.0),
|
||||||
|
model_point: DVec3::ZERO,
|
||||||
|
viewport: Some(frame.viewport),
|
||||||
|
frame: Some(frame),
|
||||||
|
source: Some(SnapSourceRef {
|
||||||
|
source: DimensionAssociationSource::inferred(source),
|
||||||
|
block_path: path,
|
||||||
|
snap_type: SnapType::Endpoint,
|
||||||
|
intersection: None,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
let mut valid = AcceptedSnap::free(DVec3::new(55.0, 50.0, 0.0));
|
||||||
|
valid.source = Some(SnapSourceRef {
|
||||||
|
source: DimensionAssociationSource::inferred(paper),
|
||||||
|
block_path: Vec::new(),
|
||||||
|
snap_type: SnapType::Endpoint,
|
||||||
|
intersection: None,
|
||||||
|
});
|
||||||
|
scene.attach_viewport_dimension_association(dim, &[Some(invalid), Some(valid)]);
|
||||||
|
let association = scene.dimension_association(dim).unwrap();
|
||||||
|
assert!(!association.trans_space);
|
||||||
|
assert!(association.references[0].is_empty());
|
||||||
|
assert_eq!(association.references[1][0].xrefs, vec![paper]);
|
||||||
|
assert_eq!(scene.dimension_association_sources(dim), vec![paper]);
|
||||||
|
assert_eq!(
|
||||||
|
scene.dimension_association_status(dim),
|
||||||
|
vec![(1, crate::scene::ReferenceStatus::Resolved)]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
+8
-8
@@ -2245,23 +2245,23 @@ pub trait CadCommand: Send {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` when the command needs entity picking (hit-test) instead of point picking.
|
/// Supports acquiring dimension geometry through a paper-space viewport.
|
||||||
/// PR2: this command creates a dimension whose measurement must be
|
|
||||||
/// compensated when its points are picked through a paper-space viewport,
|
|
||||||
/// and whose explicit object pick may land on model geometry displayed
|
|
||||||
/// inside one. See `crate::app::dim_viewport`.
|
|
||||||
fn measures_through_viewports(&self) -> bool {
|
fn measures_through_viewports(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Definition points acquired so far, in the command's working space.
|
/// Definition points acquired so far, in the command's working space.
|
||||||
/// Used to retain references only for accepted measuring inputs.
|
/// Used to retain references only for accepted measuring inputs.
|
||||||
fn dimension_acquired_points(&self) -> Vec<DVec3> { Vec::new() }
|
fn dimension_acquired_points(&self) -> Vec<DVec3> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
|
||||||
/// The next point places annotation rather than acquiring geometry.
|
/// The next point places annotation rather than acquiring geometry.
|
||||||
fn dimension_placement_pending(&self) -> bool { false }
|
fn dimension_placement_pending(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Needs entity hit-testing instead of point input.
|
||||||
fn needs_entity_pick(&self) -> bool {
|
fn needs_entity_pick(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,12 +255,9 @@ pub fn set_on_entity(entity: &mut EntityType, code: i16, value: Option<XDataValu
|
|||||||
}
|
}
|
||||||
|
|
||||||
let legacy = common.extended_data.get_record("ACAD_DSTYLE").is_some()
|
let legacy = common.extended_data.get_record("ACAD_DSTYLE").is_some()
|
||||||
&& common
|
&& common.extended_data.get_record("ACAD").is_some_and(
|
||||||
.extended_data
|
|rec| !matches!(rec.values.first(), Some(XDataValue::String(n)) if n == "DSTYLE"),
|
||||||
.get_record("ACAD")
|
);
|
||||||
.is_some_and(|rec| {
|
|
||||||
!matches!(rec.values.first(), Some(XDataValue::String(n)) if n == "DSTYLE")
|
|
||||||
});
|
|
||||||
let app = if legacy { "ACAD_DSTYLE" } else { "ACAD" };
|
let app = if legacy { "ACAD_DSTYLE" } else { "ACAD" };
|
||||||
|
|
||||||
let values = if kept.is_empty() {
|
let values = if kept.is_empty() {
|
||||||
@@ -281,8 +278,7 @@ pub fn set_on_entity(entity: &mut EntityType, code: i16, value: Option<XDataValu
|
|||||||
|
|
||||||
let mut rebuilt = ExtendedData::new();
|
let mut rebuilt = ExtendedData::new();
|
||||||
for record in common.extended_data.records() {
|
for record in common.extended_data.records() {
|
||||||
if record.application_name != app
|
if record.application_name != app && !(!legacy && record.application_name == "ACAD_DSTYLE")
|
||||||
&& !(!legacy && record.application_name == "ACAD_DSTYLE")
|
|
||||||
{
|
{
|
||||||
rebuilt.add_record(record.clone());
|
rebuilt.add_record(record.clone());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1638,11 +1638,12 @@ pub(crate) fn dimension_in_paper_space(dimension: &Dimension, document: &CadDocu
|
|||||||
if !owner.is_valid() {
|
if !owner.is_valid() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
document.objects.values().any(|object| matches!(object,
|
document.objects.values().any(|object| {
|
||||||
acadrust::objects::ObjectType::Layout(layout)
|
matches!(object,
|
||||||
if layout.name != "Model" && layout.block_record == owner
|
acadrust::objects::ObjectType::Layout(layout)
|
||||||
))
|
if layout.name != "Model" && layout.block_record == owner
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The value a dimension's text reports for its measurement: the raw
|
/// The value a dimension's text reports for its measurement: the raw
|
||||||
@@ -1654,7 +1655,10 @@ pub(crate) fn dimension_in_paper_space(dimension: &Dimension, document: &CadDocu
|
|||||||
/// use when they want the number the user reads.
|
/// use when they want the number the user reads.
|
||||||
pub(crate) fn displayed_measurement(dimension: &Dimension, style: Option<&DimStyle>) -> f64 {
|
pub(crate) fn displayed_measurement(dimension: &Dimension, style: Option<&DimStyle>) -> f64 {
|
||||||
let raw = dimension.measurement();
|
let raw = dimension.measurement();
|
||||||
if matches!(dimension, Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)) {
|
if matches!(
|
||||||
|
dimension,
|
||||||
|
Dimension::Angular2Ln(_) | Dimension::Angular3Pt(_)
|
||||||
|
) {
|
||||||
return raw;
|
return raw;
|
||||||
}
|
}
|
||||||
let lfac = style.map(|s| s.dimlfac).unwrap_or(1.0);
|
let lfac = style.map(|s| s.dimlfac).unwrap_or(1.0);
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ impl CadCommand for AlignedDimensionCommand {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PR2: points and object picks may come through a paper-space viewport;
|
/// Points and object picks may come through a paper-space viewport;
|
||||||
/// the committed dimension then reports the model measurement.
|
/// the committed dimension then reports the model measurement.
|
||||||
fn measures_through_viewports(&self) -> bool {
|
fn measures_through_viewports(&self) -> bool {
|
||||||
true
|
true
|
||||||
@@ -169,7 +169,8 @@ impl CadCommand for AlignedDimensionCommand {
|
|||||||
|
|
||||||
fn dimension_acquired_points(&self) -> Vec<DVec3> {
|
fn dimension_acquired_points(&self) -> Vec<DVec3> {
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::First => vec![], Step::Second(p) => vec![p],
|
Step::First => vec![],
|
||||||
|
Step::Second(p) => vec![p],
|
||||||
Step::DimLine { p1, p2 } => vec![p1, p2],
|
Step::DimLine { p1, p2 } => vec![p1, p2],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -482,7 +482,7 @@ impl CadCommand for AngularDimensionCommand {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PR2: points and object picks may come through a paper-space viewport;
|
/// Points and object picks may come through a paper-space viewport;
|
||||||
/// the committed dimension then reports the model measurement.
|
/// the committed dimension then reports the model measurement.
|
||||||
fn measures_through_viewports(&self) -> bool {
|
fn measures_through_viewports(&self) -> bool {
|
||||||
true
|
true
|
||||||
@@ -490,11 +490,27 @@ impl CadCommand for AngularDimensionCommand {
|
|||||||
|
|
||||||
fn dimension_acquired_points(&self) -> Vec<DVec3> {
|
fn dimension_acquired_points(&self) -> Vec<DVec3> {
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::Vertex => vec![], Step::FirstRay(p) => vec![p],
|
Step::Vertex => vec![],
|
||||||
Step::SecondRay { vertex, first } | Step::CircleSecondRay { vertex, first, .. } => vec![vertex, first],
|
Step::FirstRay(p) => vec![p],
|
||||||
Step::SecondLine { first_start, first_end, .. } => vec![first_start, first_end],
|
Step::SecondRay { vertex, first } | Step::CircleSecondRay { vertex, first, .. } => {
|
||||||
Step::ArcPoint3 { vertex, first, second } => vec![vertex, first, second],
|
vec![vertex, first]
|
||||||
Step::ArcPoint2 { first_start, first_end, second_start, second_end } => vec![first_start, first_end, second_start, second_end],
|
}
|
||||||
|
Step::SecondLine {
|
||||||
|
first_start,
|
||||||
|
first_end,
|
||||||
|
..
|
||||||
|
} => vec![first_start, first_end],
|
||||||
|
Step::ArcPoint3 {
|
||||||
|
vertex,
|
||||||
|
first,
|
||||||
|
second,
|
||||||
|
} => vec![vertex, first, second],
|
||||||
|
Step::ArcPoint2 {
|
||||||
|
first_start,
|
||||||
|
first_end,
|
||||||
|
second_start,
|
||||||
|
second_end,
|
||||||
|
} => vec![first_start, first_end, second_start, second_end],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ impl CadCommand for DiameterDimensionCommand {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PR2: points and object picks may come through a paper-space viewport;
|
/// Points and object picks may come through a paper-space viewport;
|
||||||
/// the committed dimension then reports the model measurement.
|
/// the committed dimension then reports the model measurement.
|
||||||
fn measures_through_viewports(&self) -> bool {
|
fn measures_through_viewports(&self) -> bool {
|
||||||
true
|
true
|
||||||
@@ -140,7 +140,8 @@ impl CadCommand for DiameterDimensionCommand {
|
|||||||
|
|
||||||
fn dimension_acquired_points(&self) -> Vec<DVec3> {
|
fn dimension_acquired_points(&self) -> Vec<DVec3> {
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::SelectObject => vec![], Step::DimLine(source) => vec![dvec(source.point_at_angle(source.start_angle))],
|
Step::SelectObject => vec![],
|
||||||
|
Step::DimLine(source) => vec![dvec(source.point_at_angle(source.start_angle))],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -217,7 +217,7 @@ impl CadCommand for LinearDimensionCommand {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PR2: points and object picks may come through a paper-space viewport;
|
/// Points and object picks may come through a paper-space viewport;
|
||||||
/// the committed dimension then reports the model measurement.
|
/// the committed dimension then reports the model measurement.
|
||||||
fn measures_through_viewports(&self) -> bool {
|
fn measures_through_viewports(&self) -> bool {
|
||||||
true
|
true
|
||||||
@@ -225,7 +225,8 @@ impl CadCommand for LinearDimensionCommand {
|
|||||||
|
|
||||||
fn dimension_acquired_points(&self) -> Vec<DVec3> {
|
fn dimension_acquired_points(&self) -> Vec<DVec3> {
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::FirstPoint => vec![], Step::SecondPoint(p) => vec![p],
|
Step::FirstPoint => vec![],
|
||||||
|
Step::SecondPoint(p) => vec![p],
|
||||||
Step::DimensionLine { first, second } => vec![first, second],
|
Step::DimensionLine { first, second } => vec![first, second],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ impl CadCommand for RadiusDimensionCommand {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
/// PR2: points and object picks may come through a paper-space viewport;
|
/// Points and object picks may come through a paper-space viewport;
|
||||||
/// the committed dimension then reports the model measurement.
|
/// the committed dimension then reports the model measurement.
|
||||||
fn measures_through_viewports(&self) -> bool {
|
fn measures_through_viewports(&self) -> bool {
|
||||||
true
|
true
|
||||||
@@ -138,7 +138,8 @@ impl CadCommand for RadiusDimensionCommand {
|
|||||||
|
|
||||||
fn dimension_acquired_points(&self) -> Vec<DVec3> {
|
fn dimension_acquired_points(&self) -> Vec<DVec3> {
|
||||||
match self.step {
|
match self.step {
|
||||||
Step::SelectObject => vec![], Step::DimLine(source) => vec![dvec(source.point_at_angle(source.start_angle))],
|
Step::SelectObject => vec![],
|
||||||
|
Step::DimLine(source) => vec![dvec(source.point_at_angle(source.start_angle))],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+125
-149
@@ -370,15 +370,7 @@ fn source_marker(entity: &EntityType, point: Vector3) -> Option<i32> {
|
|||||||
|
|
||||||
// ── Chain-aware reference resolution ──────────────────────────────────────
|
// ── Chain-aware reference resolution ──────────────────────────────────────
|
||||||
|
|
||||||
/// How a resolved feature point travels from the entity it belongs to up into
|
/// Transform an entity-local feature through its block path and optional viewport.
|
||||||
/// the space the dimension is drawn in.
|
|
||||||
///
|
|
||||||
/// `transform` is the block-instance path (entity-local -> model).
|
|
||||||
/// `frame` is the layout viewport the chain looks through, when it has one;
|
|
||||||
/// with a frame the dimension lives on the sheet and every point it holds is
|
|
||||||
/// a *paper* point, while the value it displays is a *model* length. Keeping
|
|
||||||
/// both directions in one place is what stops the viewport compensation from
|
|
||||||
/// being applied twice (see `viewport_ref::measurement_rule`).
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
#[derive(Clone, Copy, Debug)]
|
||||||
pub(crate) struct ChainMap {
|
pub(crate) struct ChainMap {
|
||||||
pub transform: Transform,
|
pub transform: Transform,
|
||||||
@@ -528,21 +520,10 @@ fn hint_error(entity: &EntityType, point: Vector3) -> f64 {
|
|||||||
f64::INFINITY
|
f64::INFINITY
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The stored osnap point, brought into the entity's own coordinates.
|
/// Interpret a stored point as a source-space or dimension-space hint.
|
||||||
///
|
/// Validate both interpretations against the geometry before using the hint
|
||||||
/// Which space the file wrote it in is not recoverable from the record: our
|
/// to disambiguate a feature; the stored point is never the resolved result.
|
||||||
/// own pre-PR3 writer stored a model/WCS point, AutoCAD's trans-space records
|
fn local_hint(map: &ChainMap, entity: &EntityType, stored: Vector3) -> Option<Vector3> {
|
||||||
/// hold the point in the space the dimension is drawn in, and both appear in
|
|
||||||
/// 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> {
|
|
||||||
let _ = scene;
|
|
||||||
let inverse = chain::invert(&map.transform)?;
|
let inverse = chain::invert(&map.transform)?;
|
||||||
let mut candidates = vec![inverse.apply(stored)];
|
let mut candidates = vec![inverse.apply(stored)];
|
||||||
if map.frame.is_some() {
|
if map.frame.is_some() {
|
||||||
@@ -610,7 +591,7 @@ pub fn resolve_reference_chain(
|
|||||||
.max(reference.osnap_point.z.abs())
|
.max(reference.osnap_point.z.abs())
|
||||||
< 1e40
|
< 1e40
|
||||||
{
|
{
|
||||||
local_hint(scene, &map, entity, reference.osnap_point)
|
local_hint(&map, entity, reference.osnap_point)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
};
|
};
|
||||||
@@ -636,9 +617,8 @@ pub fn resolve_reference_chain(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The pre-PR3 marker/parameter resolution, unchanged in behaviour but now
|
/// Resolve a marker/parameter in entity-local coordinates.
|
||||||
/// evaluated in the *entity's own* coordinates rather than assumed to be in
|
/// The caller applies the block and viewport transforms.
|
||||||
/// model space. Lifting through the block path is the caller's job.
|
|
||||||
fn resolve_local_by_marker(
|
fn resolve_local_by_marker(
|
||||||
entity: &EntityType,
|
entity: &EntityType,
|
||||||
reference: &AssocDimensionReference,
|
reference: &AssocDimensionReference,
|
||||||
@@ -1224,8 +1204,7 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Persist the association for a dimension whose points were picked
|
/// Persist the association for a dimension whose points were picked
|
||||||
/// through [`AcceptedSnap`]s — the entry point PR2 calls once it has
|
/// through [`AcceptedSnap`]s, preserving each source and its owning space.
|
||||||
/// created a paper-space dimension that measures model geometry.
|
|
||||||
///
|
///
|
||||||
/// `snaps` is positional: one slot per definition point, in the same order
|
/// `snaps` is positional: one slot per definition point, in the same order
|
||||||
/// [`dimension_reference_points`] returns them (linear/aligned: first,
|
/// [`dimension_reference_points`] returns them (linear/aligned: first,
|
||||||
@@ -1245,13 +1224,7 @@ impl Scene {
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// A radial dimension has ONE reference, and it is an
|
// Radial references identify a circle/arc and angle, rather than a vertex.
|
||||||
// `AcDbOsnapPointRef` of osnap type 10 carrying the *radial* marker
|
|
||||||
// (which circle / which polyline arc segment) and the angle around it,
|
|
||||||
// not a vertex marker. `source_reference` would return the generic
|
|
||||||
// curve-parameter marker `-2`, which `radial_source_for_marker` cannot
|
|
||||||
// find, so radial goes down its own path — the same one
|
|
||||||
// `attach_dimension_association_sources` uses for model space.
|
|
||||||
let radial_dimension = matches!(
|
let radial_dimension = matches!(
|
||||||
dimension_entity,
|
dimension_entity,
|
||||||
Dimension::Radius(_) | Dimension::Diameter(_) | Dimension::LargeRadial(_)
|
Dimension::Radius(_) | Dimension::Diameter(_) | Dimension::LargeRadial(_)
|
||||||
@@ -1278,18 +1251,17 @@ impl Scene {
|
|||||||
let mut xrefs = Vec::new();
|
let mut xrefs = Vec::new();
|
||||||
if let Some(viewport) = snap.viewport {
|
if let Some(viewport) = snap.viewport {
|
||||||
xrefs.push(viewport);
|
xrefs.push(viewport);
|
||||||
trans_space = true;
|
|
||||||
}
|
}
|
||||||
xrefs.extend(source.block_path.iter().copied());
|
xrefs.extend(source.block_path.iter().copied());
|
||||||
xrefs.push(entity_handle);
|
xrefs.push(entity_handle);
|
||||||
reactor_targets.extend(xrefs.iter().copied());
|
let Ok(walked) = chain::walk_chain(&self.document, &xrefs) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let Some(inverse) = chain::invert(&walked.transform) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
let local = inverse.apply(dvec3(snap.model_point));
|
||||||
let osnap_type = chain::osnap_type_for(source.snap_type);
|
let osnap_type = chain::osnap_type_for(source.snap_type);
|
||||||
let local = Self::point_in_entity_space(
|
|
||||||
&self.document,
|
|
||||||
&source.block_path,
|
|
||||||
dvec3(snap.model_point),
|
|
||||||
);
|
|
||||||
let marker_parameter = source
|
let marker_parameter = source
|
||||||
.source
|
.source
|
||||||
.marker
|
.marker
|
||||||
@@ -1314,6 +1286,8 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
reactor_targets.extend(intersection_objects.iter().copied());
|
reactor_targets.extend(intersection_objects.iter().copied());
|
||||||
}
|
}
|
||||||
|
trans_space |= snap.viewport.is_some();
|
||||||
|
reactor_targets.extend(xrefs.iter().copied());
|
||||||
associativity |= 1 << index;
|
associativity |= 1 << index;
|
||||||
references[index].push(AssocDimensionReference {
|
references[index].push(AssocDimensionReference {
|
||||||
class_name: "AcDbOsnapPointRef".to_string(),
|
class_name: "AcDbOsnapPointRef".to_string(),
|
||||||
@@ -1323,8 +1297,8 @@ impl Scene {
|
|||||||
main_subent_type: 1,
|
main_subent_type: 1,
|
||||||
main_gs_marker: marker,
|
main_gs_marker: marker,
|
||||||
osnap_distance: parameter,
|
osnap_distance: parameter,
|
||||||
// Stored in MODEL space. `local_hint` reads either convention,
|
// Store the source's owning-space point: model coordinates for
|
||||||
// so an imported paper-space point still resolves.
|
// viewport features, paper coordinates for direct sheet features.
|
||||||
osnap_point: dvec3(snap.model_point),
|
osnap_point: dvec3(snap.model_point),
|
||||||
..AssocDimensionReference::default()
|
..AssocDimensionReference::default()
|
||||||
});
|
});
|
||||||
@@ -1351,12 +1325,7 @@ impl Scene {
|
|||||||
dimension: Handle,
|
dimension: Handle,
|
||||||
snaps: &[Option<AcceptedSnap>],
|
snaps: &[Option<AcceptedSnap>],
|
||||||
) {
|
) {
|
||||||
let Some(snap) = snaps
|
let Some(snap) = snaps.iter().flatten().find(|snap| snap.source.is_some()) else {
|
||||||
.iter()
|
|
||||||
.flatten()
|
|
||||||
.find(|snap| snap.source.is_some())
|
|
||||||
.cloned()
|
|
||||||
else {
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let source = snap.source.as_ref().expect("filtered above");
|
let source = snap.source.as_ref().expect("filtered above");
|
||||||
@@ -1364,14 +1333,17 @@ impl Scene {
|
|||||||
let Some(entity) = self.document.get_entity(entity_handle) else {
|
let Some(entity) = self.document.get_entity(entity_handle) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
// The acquired point in the source entity's own coordinates, so the
|
let mut xrefs = Vec::new();
|
||||||
// radial geometry and the angle are both computed where the entity
|
xrefs.extend(snap.viewport);
|
||||||
// lives.
|
xrefs.extend(source.block_path.iter().copied());
|
||||||
let local = Self::point_in_entity_space(
|
xrefs.push(entity_handle);
|
||||||
&self.document,
|
let Ok(walked) = chain::walk_chain(&self.document, &xrefs) else {
|
||||||
&source.block_path,
|
return;
|
||||||
dvec3(snap.model_point),
|
};
|
||||||
);
|
let Some(inverse) = chain::invert(&walked.transform) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let local = inverse.apply(dvec3(snap.model_point));
|
||||||
let radial = match source.source.marker {
|
let radial = match source.source.marker {
|
||||||
Some(marker) => radial_source_for_marker(entity, marker),
|
Some(marker) => radial_source_for_marker(entity, marker),
|
||||||
None => radial_source_at(entity, local),
|
None => radial_source_at(entity, local),
|
||||||
@@ -1382,14 +1354,7 @@ impl Scene {
|
|||||||
} else {
|
} else {
|
||||||
radial.angle_at(dpoint(local))
|
radial.angle_at(dpoint(local))
|
||||||
};
|
};
|
||||||
let mut xrefs = Vec::new();
|
let trans_space = snap.viewport.is_some();
|
||||||
let mut trans_space = false;
|
|
||||||
if let Some(viewport) = snap.viewport {
|
|
||||||
xrefs.push(viewport);
|
|
||||||
trans_space = true;
|
|
||||||
}
|
|
||||||
xrefs.extend(source.block_path.iter().copied());
|
|
||||||
xrefs.push(entity_handle);
|
|
||||||
let reactor_targets = xrefs.clone();
|
let reactor_targets = xrefs.clone();
|
||||||
let reference = AssocDimensionReference {
|
let reference = AssocDimensionReference {
|
||||||
class_name: "AcDbOsnapPointRef".to_string(),
|
class_name: "AcDbOsnapPointRef".to_string(),
|
||||||
@@ -1431,28 +1396,64 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A model-space point brought down into the coordinates of the entity at
|
/// Select one measurement viewport while allowing direct references to
|
||||||
/// the end of `block_path`. Identity for top-level geometry.
|
/// geometry owned by the dimension's paper layout. A model-space chain
|
||||||
fn point_in_entity_space(
|
/// without its viewport cannot be interpreted as a paper-space anchor.
|
||||||
document: &acadrust::CadDocument,
|
fn association_measurement_frame(
|
||||||
block_path: &[Handle],
|
&self,
|
||||||
point: Vector3,
|
association: &AssocDimensionAssociation,
|
||||||
) -> Vector3 {
|
) -> Result<Option<ViewportFrame>, ()> {
|
||||||
if block_path.is_empty() {
|
let mut viewport = None;
|
||||||
return point;
|
for reference in association.references.iter().flatten() {
|
||||||
|
for handles in [&reference.xrefs, &reference.intersection_objects] {
|
||||||
|
let Some(root) = handles.first() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if matches!(
|
||||||
|
self.document.get_entity(*root),
|
||||||
|
Some(EntityType::Viewport(_))
|
||||||
|
) {
|
||||||
|
if viewport.is_some_and(|previous| previous != *root) {
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
viewport = Some(*root);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
chain::invert(&chain::block_transform(document, block_path))
|
let Some(viewport) = viewport else {
|
||||||
.map(|inverse| inverse.apply(point))
|
return if association.trans_space {
|
||||||
.unwrap_or(point)
|
Err(())
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
};
|
||||||
|
};
|
||||||
|
let owner = self
|
||||||
|
.document
|
||||||
|
.get_entity(association.dimension)
|
||||||
|
.ok_or(())?
|
||||||
|
.common()
|
||||||
|
.owner_handle;
|
||||||
|
for reference in association.references.iter().flatten() {
|
||||||
|
for handles in [&reference.xrefs, &reference.intersection_objects] {
|
||||||
|
let Some(root) = handles.first() else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
|
if self
|
||||||
|
.document
|
||||||
|
.get_entity(*root)
|
||||||
|
.ok_or(())?
|
||||||
|
.common()
|
||||||
|
.owner_handle
|
||||||
|
!= owner
|
||||||
|
{
|
||||||
|
return Err(());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.viewport_frame(viewport).map(Some).ok_or(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-slot health of a dimension's association, for the UI and for tests.
|
/// Resolve each reference's current status without changing its saved data.
|
||||||
///
|
|
||||||
/// Nothing here is persisted: brokenness is *derived* from whether the
|
|
||||||
/// chain still resolves. That is what makes undo restore a link for free —
|
|
||||||
/// 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 {
|
let Some(association) = self.dimension_association(dimension) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
@@ -1485,8 +1486,15 @@ impl Scene {
|
|||||||
.is_some()
|
.is_some()
|
||||||
});
|
});
|
||||||
let measurement_supported = measurement_supported
|
let measurement_supported = measurement_supported
|
||||||
&& association.references.iter().flatten()
|
&& association
|
||||||
.filter(|r| chain::needs_from_point(r.osnap_type)).count() <= 1;
|
.references
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.filter(|r| chain::needs_from_point(r.osnap_type))
|
||||||
|
.count()
|
||||||
|
<= 1;
|
||||||
|
let measurement_supported =
|
||||||
|
measurement_supported && self.association_measurement_frame(association).is_ok();
|
||||||
let points = self.dimension_association_slot_points(dimension);
|
let points = self.dimension_association_slot_points(dimension);
|
||||||
association
|
association
|
||||||
.references
|
.references
|
||||||
@@ -1519,7 +1527,7 @@ impl Scene {
|
|||||||
///
|
///
|
||||||
/// Layer visibility and viewport clipping are deliberately not consulted:
|
/// Layer visibility and viewport clipping are deliberately not consulted:
|
||||||
/// hiding a layer or clipping a viewport changes what a *new* snap may
|
/// hiding a layer or clipping a viewport changes what a *new* snap may
|
||||||
/// acquire (PR1's concern), never what an existing association points at.
|
/// acquire, never what an existing association points at.
|
||||||
pub fn dimension_association_sources(&self, dimension: Handle) -> Vec<Handle> {
|
pub fn dimension_association_sources(&self, dimension: Handle) -> Vec<Handle> {
|
||||||
let Some(association) = self.dimension_association(dimension) else {
|
let Some(association) = self.dimension_association(dimension) else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
@@ -1553,15 +1561,8 @@ impl Scene {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Duplicate dimension associations across a COPY/MIRROR/ARRAY handle map.
|
/// Preserve an association only when the dimension and every source-chain
|
||||||
///
|
/// handle were copied. Remap the complete chain to the new entities.
|
||||||
/// A copied dimension only keeps an association when *every* handle in
|
|
||||||
/// every chain was copied too — the viewport, each INSERT on the path and
|
|
||||||
/// the source entity. Then the whole chain is remapped onto the copies, so
|
|
||||||
/// the copy measures the copied geometry. Copying a dimension on its own
|
|
||||||
/// leaves it with no association at all, which is exactly what the model
|
|
||||||
/// space path did before this: an all-or-nothing rule, never a copy that
|
|
||||||
/// silently drives off the original's geometry.
|
|
||||||
pub(crate) fn copy_dimension_associations(
|
pub(crate) fn copy_dimension_associations(
|
||||||
&mut self,
|
&mut self,
|
||||||
handle_map: &rustc_hash::FxHashMap<Handle, Handle>,
|
handle_map: &rustc_hash::FxHashMap<Handle, Handle>,
|
||||||
@@ -1591,7 +1592,7 @@ impl Scene {
|
|||||||
if !complete {
|
if !complete {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let remap = |handles: &Vec<Handle>| -> Vec<Handle> {
|
let remap = |handles: &[Handle]| -> Vec<Handle> {
|
||||||
handles
|
handles
|
||||||
.iter()
|
.iter()
|
||||||
.map(|handle| handle_map.get(handle).copied().unwrap_or(*handle))
|
.map(|handle| handle_map.get(handle).copied().unwrap_or(*handle))
|
||||||
@@ -1640,15 +1641,7 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Report a layout viewport as changed so the dimensions drawn through it
|
/// Refresh dimensions when their viewport moves, pans, zooms, or twists.
|
||||||
/// are regenerated.
|
|
||||||
///
|
|
||||||
/// A viewport move / pan / zoom / twist changes where model geometry lands
|
|
||||||
/// on the sheet without touching that geometry, so nothing in the ordinary
|
|
||||||
/// entity-change path would notice. Every reference that looks through a
|
|
||||||
/// viewport carries that viewport's handle as `xrefs[0]`, so announcing the
|
|
||||||
/// viewport itself is all that is needed — the existing dependency lookup
|
|
||||||
/// in `refresh_associative_dimensions` finds the associations from there.
|
|
||||||
pub fn notify_viewport_changed(&mut self, viewport: Handle) {
|
pub fn notify_viewport_changed(&mut self, viewport: Handle) {
|
||||||
if matches!(
|
if matches!(
|
||||||
self.document.get_entity(viewport),
|
self.document.get_entity(viewport),
|
||||||
@@ -1750,17 +1743,25 @@ impl Scene {
|
|||||||
.chain(reference.intersection_objects.iter())
|
.chain(reference.intersection_objects.iter())
|
||||||
.any(|handle| changed.contains(handle))
|
.any(|handle| changed.contains(handle))
|
||||||
})
|
})
|
||||||
.then_some(association.clone())
|
.then(|| association.clone())
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut refreshed = Vec::new();
|
let mut refreshed = Vec::new();
|
||||||
for association in associations {
|
for association in associations {
|
||||||
|
let Ok(frame) = self.association_measurement_frame(&association) else {
|
||||||
|
continue;
|
||||||
|
};
|
||||||
// Dependent snaps need a resolved independent definition point.
|
// Dependent snaps need a resolved independent definition point.
|
||||||
// Two such features require a coupled solve; old points are not
|
// Two such features require a coupled solve; old points are not
|
||||||
// evidence of a solution after the geometry changes.
|
// evidence of a solution after the geometry changes.
|
||||||
if association.references.iter().flatten()
|
if association
|
||||||
.filter(|r| chain::needs_from_point(r.osnap_type)).count() > 1
|
.references
|
||||||
|
.iter()
|
||||||
|
.flatten()
|
||||||
|
.filter(|r| chain::needs_from_point(r.osnap_type))
|
||||||
|
.count()
|
||||||
|
> 1
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -1806,27 +1807,20 @@ impl Scene {
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let viewports: std::collections::HashSet<_> =
|
// Every point is already in paper space: direct sheet references
|
||||||
resolved.iter().flatten().map(|r| r.viewport).collect();
|
// stay there, and model references are projected individually.
|
||||||
if viewports.len() > 1 {
|
// Only the measurement factor is shared by the whole dimension.
|
||||||
continue;
|
let map = frame
|
||||||
}
|
|
||||||
// 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.
|
|
||||||
let map = resolved
|
|
||||||
.iter()
|
|
||||||
.flatten()
|
|
||||||
.next()
|
|
||||||
.and_then(|value| value.viewport)
|
|
||||||
.and_then(|handle| self.viewport_frame(handle))
|
|
||||||
.map(|frame| ChainMap {
|
.map(|frame| ChainMap {
|
||||||
transform: Transform::identity(),
|
transform: Transform::identity(),
|
||||||
frame: Some(frame),
|
frame: Some(frame),
|
||||||
})
|
})
|
||||||
.unwrap_or_else(ChainMap::identity);
|
.unwrap_or_else(ChainMap::identity);
|
||||||
let model_points: [Option<Vector3>; 4] =
|
let model_points: [Option<Vector3>; 4] = std::array::from_fn(|index| {
|
||||||
std::array::from_fn(|index| resolved[index].map(|value| value.model));
|
resolved[index]
|
||||||
|
.filter(|value| value.viewport.is_some())
|
||||||
|
.map(|value| value.model)
|
||||||
|
});
|
||||||
let resolved: [Option<Vector3>; 4] =
|
let resolved: [Option<Vector3>; 4] =
|
||||||
std::array::from_fn(|index| resolved[index].map(|value| value.space));
|
std::array::from_fn(|index| resolved[index].map(|value| value.space));
|
||||||
|
|
||||||
@@ -2117,18 +2111,7 @@ impl Scene {
|
|||||||
Dimension::Linear(linear) => linear_measurement(linear),
|
Dimension::Linear(linear) => linear_measurement(linear),
|
||||||
_ => dimension.measurement(),
|
_ => dimension.measurement(),
|
||||||
};
|
};
|
||||||
// `actual_measurement` stays RAW, in the dimension's own space —
|
// Store the raw sheet measurement; the formatter applies DIMLFAC once.
|
||||||
// paper, for a dimension drawn on the sheet through a viewport.
|
|
||||||
// The viewport compensation is carried by the negative DIMLFAC
|
|
||||||
// override and applied once, by the formatter. See
|
|
||||||
// `viewport_ref::measurement_rule`: substituting the model
|
|
||||||
// distance here (or multiplying by `MeasurementScale::model_factor`)
|
|
||||||
// would make a refreshed dimension read a different number from an
|
|
||||||
// identical freshly-created one.
|
|
||||||
//
|
|
||||||
// The similarity property of a viewport frame is what makes this
|
|
||||||
// exact: paper_span * (1 / scale) is the model span, for a rotated
|
|
||||||
// linear projection just as much as for a free distance.
|
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
map.frame.is_none()
|
map.frame.is_none()
|
||||||
|| model_points
|
|| model_points
|
||||||
@@ -2176,12 +2159,7 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// How far the midpoint of a dimension's measured points moved.
|
/// Average displacement of the dimension's resolved definition points.
|
||||||
///
|
|
||||||
/// Used to carry the sheet annotation — the dimension line's standoff and a
|
|
||||||
/// user-dragged text position — along with the geometry. Slots that did not
|
|
||||||
/// resolve keep their old point, so a one-ended edit shifts the annotation by
|
|
||||||
/// half the movement, which is what keeps the dimension line centred.
|
|
||||||
fn centroid_shift(before: [Option<Vector3>; 2], after: [Option<Vector3>; 2]) -> Vector3 {
|
fn centroid_shift(before: [Option<Vector3>; 2], after: [Option<Vector3>; 2]) -> Vector3 {
|
||||||
let mut delta = Vector3::new(0.0, 0.0, 0.0);
|
let mut delta = Vector3::new(0.0, 0.0, 0.0);
|
||||||
let mut count = 0.0;
|
let mut count = 0.0;
|
||||||
@@ -2194,8 +2172,6 @@ fn centroid_shift(before: [Option<Vector3>; 2], after: [Option<Vector3>; 2]) ->
|
|||||||
if count == 0.0 {
|
if count == 0.0 {
|
||||||
return Vector3::new(0.0, 0.0, 0.0);
|
return Vector3::new(0.0, 0.0, 0.0);
|
||||||
}
|
}
|
||||||
// Averaged over the slots that actually moved, not over both, so a
|
|
||||||
// dimension with one broken reference still tracks the live one.
|
|
||||||
Vector3::new(delta.x / count, delta.y / count, delta.z / count)
|
Vector3::new(delta.x / count, delta.y / count, delta.z / count)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,38 +1,8 @@
|
|||||||
//! Full reference chains for associative dimensions.
|
//! Resolve a dimension feature through `[viewport?, insert..., entity]`.
|
||||||
//!
|
//! `xrefs` stores the primary chain; `intersection_objects` stores the second
|
||||||
//! An `AssocDimensionReference` addresses a *feature* — an osnap point on a
|
//! intersection source. Features are evaluated in entity-local coordinates and
|
||||||
//! piece of geometry — through a chain of handles:
|
//! transformed through their block instances. The caller projects viewport
|
||||||
//!
|
//! references onto the sheet. All arithmetic uses `f64`.
|
||||||
//! ```text
|
|
||||||
//! dimension -> [viewport] -> [insert ...] -> entity -> feature
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! `AssocDimensionReference::xrefs` is that chain, outermost first. AutoCAD
|
|
||||||
//! calls the same list `mainObjectIds`; `intersection_objects`
|
|
||||||
//! (`intersectObjectIds`) is the second chain an INTERSECTION / APPARENT
|
|
||||||
//! INTERSECTION osnap needs.
|
|
||||||
//!
|
|
||||||
//! The pre-PR3 resolver only ever read `xrefs[0]`, which is correct for a
|
|
||||||
//! model-space dimension on top-level geometry and wrong for everything else:
|
|
||||||
//! a paper-space dimension measuring through a layout viewport stores the
|
|
||||||
//! VIEWPORT as `xrefs[0]`, so the resolver stopped at a viewport entity, found
|
|
||||||
//! no curve on it and gave up. Nine of the ten association objects in the
|
|
||||||
//! 115448 regression drawing are of that shape.
|
|
||||||
//!
|
|
||||||
//! This module owns:
|
|
||||||
//! * [`walk_chain`] — turn `xrefs` into a viewport, a block-instance path
|
|
||||||
//! with its accumulated transform, and the innermost entity.
|
|
||||||
//! * [`feature_point`] — evaluate the osnap feature on that entity, in the
|
|
||||||
//! entity's own coordinates, then lift it through the block path into
|
|
||||||
//! model space.
|
|
||||||
//!
|
|
||||||
//! Mapping model -> paper through the viewport frame is deliberately *not*
|
|
||||||
//! done here; the caller owns it, because only the caller knows whether the
|
|
||||||
//! association is `trans_space` and which [`ViewportFrame`] is current.
|
|
||||||
//!
|
|
||||||
//! All arithmetic is `f64`.
|
|
||||||
//!
|
|
||||||
//! [`ViewportFrame`]: super::viewport_ref::ViewportFrame
|
|
||||||
|
|
||||||
use acadrust::objects::AssocDimensionReference;
|
use acadrust::objects::AssocDimensionReference;
|
||||||
use acadrust::types::{Handle, Matrix4, Transform, Vector3};
|
use acadrust::types::{Handle, Matrix4, Transform, Vector3};
|
||||||
@@ -45,12 +15,8 @@ use crate::entities::curve::entity_curve;
|
|||||||
/// `AcDb::OsnapMode`, the value stored in
|
/// `AcDb::OsnapMode`, the value stored in
|
||||||
/// [`AssocDimensionReference::osnap_type`].
|
/// [`AssocDimensionReference::osnap_type`].
|
||||||
///
|
///
|
||||||
/// These are the AutoCAD ObjectARX codes, not our own [`crate::snap::SnapType`]
|
/// These are ObjectARX codes, independent of [`crate::snap::SnapType`].
|
||||||
/// discriminants. The pre-PR3 writer already emitted `END` for generic points
|
#[allow(dead_code)] // Include codes preserved from unsupported imported modes.
|
||||||
/// 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.
|
|
||||||
pub(crate) mod osnap {
|
pub(crate) mod osnap {
|
||||||
pub const NONE: u8 = 0;
|
pub const NONE: u8 = 0;
|
||||||
pub const END: u8 = 1;
|
pub const END: u8 = 1;
|
||||||
@@ -72,7 +38,7 @@ pub(crate) mod osnap {
|
|||||||
///
|
///
|
||||||
/// Modes with no ObjectARX equivalent (grid, object pick) map to
|
/// Modes with no ObjectARX equivalent (grid, object pick) map to
|
||||||
/// [`osnap::NONE`]; a reference carrying `NONE` resolves by marker/parameter
|
/// [`osnap::NONE`]; a reference carrying `NONE` resolves by marker/parameter
|
||||||
/// alone, exactly like the pre-PR3 records.
|
/// alone.
|
||||||
pub(crate) fn osnap_type_for(snap: crate::snap::SnapType) -> u8 {
|
pub(crate) fn osnap_type_for(snap: crate::snap::SnapType) -> u8 {
|
||||||
use crate::snap::SnapType as S;
|
use crate::snap::SnapType as S;
|
||||||
match snap {
|
match snap {
|
||||||
@@ -166,7 +132,11 @@ pub(crate) fn walk_chain(
|
|||||||
return Err(ChainError::Invalid);
|
return Err(ChainError::Invalid);
|
||||||
};
|
};
|
||||||
// Arrays need an instance index that this reference format does not carry.
|
// 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) {
|
if insert.row_count > 1
|
||||||
|
|| insert.column_count > 1
|
||||||
|
|| block_path.len() >= 8
|
||||||
|
|| block_path.contains(&handle)
|
||||||
|
{
|
||||||
return Err(ChainError::Invalid);
|
return Err(ChainError::Invalid);
|
||||||
}
|
}
|
||||||
let block = document
|
let block = document
|
||||||
@@ -182,64 +152,23 @@ pub(crate) fn walk_chain(
|
|||||||
Err(ChainError::Empty)
|
Err(ChainError::Empty)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Accumulated entity-local -> model transform for an ordered INSERT path.
|
/// Invert an affine block transform, rejecting singular or nonfinite matrices.
|
||||||
pub(crate) fn block_transform(document: &CadDocument, block_path: &[Handle]) -> Transform {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Affine inverse of `transform`.
|
|
||||||
///
|
|
||||||
/// `acadrust::types::Matrix4` has no inverse of its own, and a block path only
|
|
||||||
/// ever produces affine matrices (translate / rotate / scale), so this inverts
|
|
||||||
/// the 3x3 linear part by cofactors and back-substitutes the translation. A
|
|
||||||
/// degenerate (zero-scale) insert returns `None` rather than exploding.
|
|
||||||
pub(crate) fn invert(transform: &Transform) -> Option<Transform> {
|
pub(crate) fn invert(transform: &Transform) -> Option<Transform> {
|
||||||
let m = &transform.matrix.m;
|
let m = &transform.matrix.m;
|
||||||
let a = [
|
let matrix = glam::DMat4::from_cols_array_2d(&std::array::from_fn(|column| {
|
||||||
[m[0][0], m[0][1], m[0][2]],
|
std::array::from_fn(|row| m[row][column])
|
||||||
[m[1][0], m[1][1], m[1][2]],
|
}));
|
||||||
[m[2][0], m[2][1], m[2][2]],
|
if !matrix.is_finite() || matrix.determinant().abs() < 1e-18 {
|
||||||
];
|
|
||||||
let det = a[0][0] * (a[1][1] * a[2][2] - a[1][2] * a[2][1])
|
|
||||||
- a[0][1] * (a[1][0] * a[2][2] - a[1][2] * a[2][0])
|
|
||||||
+ a[0][2] * (a[1][0] * a[2][1] - a[1][1] * a[2][0]);
|
|
||||||
if !det.is_finite() || det.abs() < 1e-18 {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let inv_det = 1.0 / det;
|
let inverse = matrix.inverse();
|
||||||
let mut inverse = [[0.0f64; 3]; 3];
|
if !inverse.is_finite() {
|
||||||
for row in 0..3 {
|
return None;
|
||||||
for col in 0..3 {
|
|
||||||
// Cofactor of (col, row) — transposed, which is the adjugate.
|
|
||||||
let (r0, r1) = ((col + 1) % 3, (col + 2) % 3);
|
|
||||||
let (c0, c1) = ((row + 1) % 3, (row + 2) % 3);
|
|
||||||
inverse[row][col] = (a[r0][c0] * a[r1][c1] - a[r0][c1] * a[r1][c0]) * inv_det;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
let t = [m[0][3], m[1][3], m[2][3]];
|
let columns = inverse.to_cols_array_2d();
|
||||||
let translation = [
|
Some(Transform::from_matrix(Matrix4 {
|
||||||
-(inverse[0][0] * t[0] + inverse[0][1] * t[1] + inverse[0][2] * t[2]),
|
m: std::array::from_fn(|row| std::array::from_fn(|column| columns[column][row])),
|
||||||
-(inverse[1][0] * t[0] + inverse[1][1] * t[1] + inverse[1][2] * t[2]),
|
}))
|
||||||
-(inverse[2][0] * t[0] + inverse[2][1] * t[1] + inverse[2][2] * t[2]),
|
|
||||||
];
|
|
||||||
Some(Transform {
|
|
||||||
matrix: Matrix4 {
|
|
||||||
m: [
|
|
||||||
[inverse[0][0], inverse[0][1], inverse[0][2], translation[0]],
|
|
||||||
[inverse[1][0], inverse[1][1], inverse[1][2], translation[1]],
|
|
||||||
[inverse[2][0], inverse[2][1], inverse[2][2], translation[2]],
|
|
||||||
[0.0, 0.0, 0.0, 1.0],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Feature evaluation ────────────────────────────────────────────────────
|
// ── Feature evaluation ────────────────────────────────────────────────────
|
||||||
@@ -482,9 +411,7 @@ fn nearest_point(
|
|||||||
hint: Option<Vector3>,
|
hint: Option<Vector3>,
|
||||||
) -> Option<Vector3> {
|
) -> Option<Vector3> {
|
||||||
let planar = entity_curve(entity)?;
|
let planar = entity_curve(entity)?;
|
||||||
// Circles and arcs carry an *angle* in `osnap_distance` — that is the
|
// Circles and arcs store an angle; other curves use a normalized parameter.
|
||||||
// convention the pre-PR3 writer established and imported files share it.
|
|
||||||
// Every other curve carries the kernel's normalised 0..1 parameter.
|
|
||||||
match &planar.curve {
|
match &planar.curve {
|
||||||
KernelCurve::Circle(_) | KernelCurve::Arc(_) => match entity {
|
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)),
|
||||||
@@ -506,14 +433,8 @@ fn nearest_point(
|
|||||||
|
|
||||||
// ── Tangent ───────────────────────────────────────────────────────────────
|
// ── Tangent ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/// Tangent point on `entity` for the touch line through `context.from`.
|
/// Tangent point for the line through `context.from`.
|
||||||
///
|
/// Spline tangency solves `(C(u) - P) x C'(u) = 0` and validates the residual.
|
||||||
/// Circles and arcs have a closed form. A spline does not, so the stored
|
|
||||||
/// parameter (`osnap_distance`) seeds a Newton refinement of the tangency
|
|
||||||
/// condition `(C(u) - P) · C'(u) = 0`; with no usable parameter the seed is
|
|
||||||
/// the point on the spline nearest the stored osnap point. That is what makes
|
|
||||||
/// the 115448 drawing's tangent-to-spline reference survive an edit to the
|
|
||||||
/// spline instead of silently freezing at its imported location.
|
|
||||||
fn tangent_point(
|
fn tangent_point(
|
||||||
entity: &EntityType,
|
entity: &EntityType,
|
||||||
reference: &AssocDimensionReference,
|
reference: &AssocDimensionReference,
|
||||||
|
|||||||
+1
-1
@@ -43,7 +43,7 @@ mod limits;
|
|||||||
mod modify;
|
mod modify;
|
||||||
mod mspace;
|
mod mspace;
|
||||||
pub mod viewport_ref;
|
pub mod viewport_ref;
|
||||||
pub mod viewport_dim_seam;
|
pub mod viewport_dimension_pick;
|
||||||
pub mod named_parameters;
|
pub mod named_parameters;
|
||||||
mod page_setup;
|
mod page_setup;
|
||||||
mod paper;
|
mod paper;
|
||||||
|
|||||||
+30
-35
@@ -83,14 +83,7 @@ impl Scene {
|
|||||||
self.viewport_edit_frame_for(self.active_viewport?, canvas_px)
|
self.viewport_edit_frame_for(self.active_viewport?, canvas_px)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`viewport_edit_frame`] for an explicitly-named viewport, rather than
|
/// Camera and canvas rectangle for snapping through an inactive viewport.
|
||||||
/// only the active (MSPACE) one.
|
|
||||||
///
|
|
||||||
/// Paper-space snapping through viewports (PR1) needs the same camera +
|
|
||||||
/// screen rectangle pair for a viewport the user has NOT entered, so the
|
|
||||||
/// existing snap engine can run against that viewport's model wires on the
|
|
||||||
/// pixels the GPU actually drew them on. `viewport_edit_frame` is now a
|
|
||||||
/// thin wrapper over this.
|
|
||||||
pub fn viewport_edit_frame_for(
|
pub fn viewport_edit_frame_for(
|
||||||
&self,
|
&self,
|
||||||
vp_handle: Handle,
|
vp_handle: Handle,
|
||||||
@@ -101,15 +94,12 @@ impl Scene {
|
|||||||
Some((cam, full))
|
Some((cam, full))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Planar paper <-> model mapping for a layout viewport, derived from the
|
/// Planar paper/model mapping derived from the viewport's display camera.
|
||||||
/// *same* camera the renderer displays its contents with
|
/// Oblique and perspective views have no supported planar mapping.
|
||||||
/// ([`camera_for_viewport`]) so a snap computed through the frame lands on
|
pub fn viewport_frame(
|
||||||
/// the pixels the GPU drew.
|
&self,
|
||||||
///
|
vp_handle: Handle,
|
||||||
/// Returns `None` for a viewport whose view is not an in-plane orthographic
|
) -> Option<crate::scene::viewport_ref::ViewportFrame> {
|
||||||
/// top view (oblique / isometric / perspective viewports are unsupported by
|
|
||||||
/// the planar frame — callers fall back to no viewport snapping there).
|
|
||||||
pub fn viewport_frame(&self, vp_handle: Handle) -> Option<crate::scene::viewport_ref::ViewportFrame> {
|
|
||||||
use crate::scene::viewport_ref::ViewportFrame;
|
use crate::scene::viewport_ref::ViewportFrame;
|
||||||
|
|
||||||
let (paper_center, vp_height, locked) = match self.document.get_entity(vp_handle) {
|
let (paper_center, vp_height, locked) = match self.document.get_entity(vp_handle) {
|
||||||
@@ -120,7 +110,7 @@ impl Scene {
|
|||||||
),
|
),
|
||||||
_ => return None,
|
_ => return None,
|
||||||
};
|
};
|
||||||
if vp_height.abs() < 1e-9 {
|
if !paper_center.is_finite() || !vp_height.is_finite() || vp_height < 1e-9 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let cam = self.camera_for_viewport(vp_handle)?;
|
let cam = self.camera_for_viewport(vp_handle)?;
|
||||||
@@ -133,7 +123,7 @@ impl Scene {
|
|||||||
// an oblique 3-D view: unsupported.
|
// an oblique 3-D view: unsupported.
|
||||||
let right = (cam.rotation * glam::Vec3::X).as_dvec3();
|
let right = (cam.rotation * glam::Vec3::X).as_dvec3();
|
||||||
let up = (cam.rotation * glam::Vec3::Y).as_dvec3();
|
let up = (cam.rotation * glam::Vec3::Y).as_dvec3();
|
||||||
if right.z.abs() > 1e-6 || up.z.abs() > 1e-6 {
|
if !right.is_finite() || !up.is_finite() || right.z.abs() > 1e-6 || up.z.abs() > 1e-6 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let normal = right.cross(up);
|
let normal = right.cross(up);
|
||||||
@@ -142,7 +132,7 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
// Model units visible across the viewport's height -> paper per model.
|
// Model units visible across the viewport's height -> paper per model.
|
||||||
let model_height = cam.ortho_size() as f64 * 2.0;
|
let model_height = cam.ortho_size() as f64 * 2.0;
|
||||||
if model_height.abs() < 1e-12 {
|
if !model_height.is_finite() || model_height < 1e-12 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let scale = vp_height / model_height;
|
let scale = vp_height / model_height;
|
||||||
@@ -162,23 +152,28 @@ impl Scene {
|
|||||||
|
|
||||||
/// Visible, supported content viewports at a sheet point, topmost first.
|
/// Visible, supported content viewports at a sheet point, topmost first.
|
||||||
/// Shared by snapping and explicit dimension object picking.
|
/// 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> {
|
pub fn viewport_frames_at_paper_point(
|
||||||
if self.current_layout == "Model" || self.active_viewport.is_some() { return Vec::new(); }
|
&self,
|
||||||
self.layout_content_viewports().iter().rev().filter_map(|&handle| {
|
paper: glam::DVec3,
|
||||||
if !self.viewport_displays_content(handle) || !self.viewport_displays_paper_point(handle, paper.truncate()) {
|
) -> Vec<crate::scene::viewport_ref::ViewportFrame> {
|
||||||
return None;
|
if self.current_layout == "Model" || self.active_viewport.is_some() {
|
||||||
}
|
return Vec::new();
|
||||||
self.viewport_frame(handle)
|
}
|
||||||
}).collect()
|
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?
|
/// Test the viewport rectangle and optional nonrectangular clip boundary.
|
||||||
///
|
|
||||||
/// Always bounded by the viewport's paper rectangle; a viewport with a
|
|
||||||
/// `clip_boundary_handle` (non-rectangular / clipped viewport) additionally
|
|
||||||
/// has to contain the point inside that boundary polygon. Used so a
|
|
||||||
/// paper-space snap never reports a model feature the viewport does not
|
|
||||||
/// actually show.
|
|
||||||
pub fn viewport_displays_paper_point(&self, vp_handle: Handle, paper: glam::DVec2) -> bool {
|
pub fn viewport_displays_paper_point(&self, vp_handle: Handle, paper: glam::DVec2) -> bool {
|
||||||
let Some(EntityType::Viewport(vp)) = self.document.get_entity(vp_handle) else {
|
let Some(EntityType::Viewport(vp)) = self.document.get_entity(vp_handle) else {
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -674,10 +674,7 @@ pub(crate) fn clip_boundary_polygon_for_document(
|
|||||||
z: f32,
|
z: f32,
|
||||||
) -> Vec<[f32; 3]> {
|
) -> Vec<[f32; 3]> {
|
||||||
use std::f64::consts::TAU;
|
use std::f64::consts::TAU;
|
||||||
let Some(entity) = document
|
let Some(entity) = document.get_entity(handle) else {
|
||||||
.entities()
|
|
||||||
.find(|e| e.common().handle == handle)
|
|
||||||
else {
|
|
||||||
return vec![];
|
return vec![];
|
||||||
};
|
};
|
||||||
// Circles and ellipses tessellate directly — their `to_render` returns a
|
// Circles and ellipses tessellate directly — their `to_render` returns a
|
||||||
|
|||||||
@@ -1,20 +1,5 @@
|
|||||||
//! Object-pick resolution for paper-space dimensions taken through a layout
|
//! Pick dimension geometry through a layout viewport. Resolve block instances
|
||||||
//! viewport.
|
//! in model space, then project the picked entity onto the paper sheet.
|
||||||
//!
|
|
||||||
//! Paper-space hit testing only sees the sheet's own entities, so a dimension
|
|
||||||
//! command's explicit "select object" pick that lands inside a content
|
|
||||||
//! viewport finds nothing. [`Scene::dimension_pick_through_viewport`] maps the
|
|
||||||
//! click into model space through the viewport's
|
|
||||||
//! [`crate::scene::viewport_ref::ViewportFrame`], hit-tests the viewport's
|
|
||||||
//! resident model wires (descending into block instances and baking the
|
|
||||||
//! instance transform), and hands back a copy of the picked entity already
|
|
||||||
//! projected onto the sheet.
|
|
||||||
//!
|
|
||||||
//! The frame itself is built by [`Scene::viewport_frame`] (see
|
|
||||||
//! `crate::scene::mspace`), which derives it from the *live viewport camera*
|
|
||||||
//! so it always agrees with the pixels the renderer drew. It returns `None`
|
|
||||||
//! for oblique / perspective viewports, and every entry point here then
|
|
||||||
//! declines the pick rather than guessing a planar mapping.
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
@@ -27,7 +12,7 @@ use glam::DVec3;
|
|||||||
|
|
||||||
/// A model entity resolved by clicking inside a paper-space content viewport.
|
/// A model entity resolved by clicking inside a paper-space content viewport.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ViewportDimPick {
|
pub struct ViewportDimensionPick {
|
||||||
/// The viewport the click looked through.
|
/// The viewport the click looked through.
|
||||||
pub frame: ViewportFrame,
|
pub frame: ViewportFrame,
|
||||||
/// Handle of the innermost model entity that owns the picked geometry.
|
/// Handle of the innermost model entity that owns the picked geometry.
|
||||||
@@ -46,18 +31,6 @@ pub struct ViewportDimPick {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Scene {
|
impl Scene {
|
||||||
pub fn content_viewport_at_paper_point(&self, paper: DVec3) -> Option<Handle> {
|
|
||||||
self.viewport_frames_at_paper_point(paper)
|
|
||||||
.first()
|
|
||||||
.map(|f| f.viewport)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn viewport_frame_at_paper_point(&self, paper: DVec3) -> Option<ViewportFrame> {
|
|
||||||
self.viewport_frames_at_paper_point(paper)
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Resolve an explicit dimension object pick made on the sheet but landing
|
/// Resolve an explicit dimension object pick made on the sheet but landing
|
||||||
/// inside a content viewport, against the *model* geometry that viewport
|
/// inside a content viewport, against the *model* geometry that viewport
|
||||||
/// displays.
|
/// displays.
|
||||||
@@ -67,14 +40,13 @@ impl Scene {
|
|||||||
&self,
|
&self,
|
||||||
paper: DVec3,
|
paper: DVec3,
|
||||||
aperture_paper: f64,
|
aperture_paper: f64,
|
||||||
) -> Option<ViewportDimPick> {
|
) -> Option<ViewportDimensionPick> {
|
||||||
for frame in self.viewport_frames_at_paper_point(paper) {
|
for frame in self.viewport_frames_at_paper_point(paper) {
|
||||||
let model_point = frame.paper_to_model(paper);
|
let model_point = frame.paper_to_model(paper);
|
||||||
let aperture_model =
|
let aperture_model =
|
||||||
(aperture_paper.max(0.0) * frame.paper_to_model_length_factor()).max(1e-9);
|
(aperture_paper.max(0.0) * frame.paper_to_model_length_factor()).max(1e-9);
|
||||||
|
|
||||||
let Some(top) =
|
let Some(top) = self.nearest_model_wire_handle(&frame, model_point, aperture_model)
|
||||||
self.nearest_model_wire_handle(frame.viewport, model_point, aperture_model)
|
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -93,7 +65,7 @@ impl Scene {
|
|||||||
&EntityTransform::Affine(viewport_model_to_paper_transform(&frame)),
|
&EntityTransform::Affine(viewport_model_to_paper_transform(&frame)),
|
||||||
);
|
);
|
||||||
let entity_handle = paper_entity.as_entity().handle();
|
let entity_handle = paper_entity.as_entity().handle();
|
||||||
return Some(ViewportDimPick {
|
return Some(ViewportDimensionPick {
|
||||||
frame,
|
frame,
|
||||||
entity_handle: if entity_handle.is_valid() {
|
entity_handle: if entity_handle.is_valid() {
|
||||||
entity_handle
|
entity_handle
|
||||||
@@ -114,10 +86,11 @@ impl Scene {
|
|||||||
/// draws, so this matches what the user sees.
|
/// draws, so this matches what the user sees.
|
||||||
fn nearest_model_wire_handle(
|
fn nearest_model_wire_handle(
|
||||||
&self,
|
&self,
|
||||||
viewport: Handle,
|
frame: &ViewportFrame,
|
||||||
model_point: DVec3,
|
model_point: DVec3,
|
||||||
aperture: f64,
|
aperture: f64,
|
||||||
) -> Option<Handle> {
|
) -> Option<Handle> {
|
||||||
|
let viewport = frame.viewport;
|
||||||
let camera = self.camera_for_viewport(viewport)?;
|
let camera = self.camera_for_viewport(viewport)?;
|
||||||
let bounds = iced::Rectangle {
|
let bounds = iced::Rectangle {
|
||||||
x: 0.0,
|
x: 0.0,
|
||||||
@@ -150,9 +123,6 @@ impl Scene {
|
|||||||
.unwrap_or(DVec3::ZERO);
|
.unwrap_or(DVec3::ZERO);
|
||||||
let distance = wire_polyline_nearest(wire, offset, model_point);
|
let distance = wire_polyline_nearest(wire, offset, model_point);
|
||||||
if let Some((distance, nearest)) = distance {
|
if let Some((distance, nearest)) = distance {
|
||||||
let Some(frame) = self.viewport_frame(viewport) else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
if !self.viewport_displays_paper_point(
|
if !self.viewport_displays_paper_point(
|
||||||
viewport,
|
viewport,
|
||||||
frame.model_to_paper(nearest).truncate(),
|
frame.model_to_paper(nearest).truncate(),
|
||||||
@@ -188,19 +158,14 @@ impl Scene {
|
|||||||
) -> Option<(EntityType, Vec<Handle>)> {
|
) -> Option<(EntityType, Vec<Handle>)> {
|
||||||
let entity = self.document.get_entity(handle)?;
|
let entity = self.document.get_entity(handle)?;
|
||||||
match entity {
|
match entity {
|
||||||
EntityType::Insert(_) => {
|
EntityType::Insert(insert) => self.descend_block_instance(
|
||||||
let mut path = Vec::new();
|
insert,
|
||||||
let found = self.descend_block_instance(
|
Transform::identity(),
|
||||||
entity.clone(),
|
model_point,
|
||||||
Transform::identity(),
|
0,
|
||||||
model_point,
|
viewport,
|
||||||
&mut path,
|
kind,
|
||||||
0,
|
),
|
||||||
viewport,
|
|
||||||
kind,
|
|
||||||
)?;
|
|
||||||
Some((found.0, found.1))
|
|
||||||
}
|
|
||||||
other => {
|
other => {
|
||||||
feature_pick_distance(other, model_point, kind)?;
|
feature_pick_distance(other, model_point, kind)?;
|
||||||
Some((other.clone(), Vec::new()))
|
Some((other.clone(), Vec::new()))
|
||||||
@@ -212,21 +177,17 @@ impl Scene {
|
|||||||
/// Returns the entity already transformed into WCS plus the INSERT path.
|
/// Returns the entity already transformed into WCS plus the INSERT path.
|
||||||
fn descend_block_instance(
|
fn descend_block_instance(
|
||||||
&self,
|
&self,
|
||||||
insert_entity: EntityType,
|
insert: &acadrust::entities::Insert,
|
||||||
outer: Transform,
|
outer: Transform,
|
||||||
model_point: DVec3,
|
model_point: DVec3,
|
||||||
path: &mut Vec<Handle>,
|
|
||||||
depth: usize,
|
depth: usize,
|
||||||
viewport: Option<Handle>,
|
viewport: Option<Handle>,
|
||||||
kind: Option<crate::snap::SnapType>,
|
kind: Option<crate::snap::SnapType>,
|
||||||
) -> Option<(EntityType, Vec<Handle>)> {
|
) -> Option<(EntityType, Vec<Handle>)> {
|
||||||
const MAX_DEPTH: usize = 8;
|
const MAX_DEPTH: usize = 8;
|
||||||
if depth > MAX_DEPTH {
|
if depth >= MAX_DEPTH {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let EntityType::Insert(insert) = &insert_entity else {
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
if insert.row_count > 1 || insert.column_count > 1 {
|
if insert.row_count > 1 || insert.column_count > 1 {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -234,10 +195,9 @@ impl Scene {
|
|||||||
let local = crate::scene::render_graph::insert_transform(&self.document, insert);
|
let local = crate::scene::render_graph::insert_transform(&self.document, insert);
|
||||||
let combined = local.then(&outer);
|
let combined = local.then(&outer);
|
||||||
let block = self.document.block_records.get(&insert.block_name)?;
|
let block = self.document.block_records.get(&insert.block_name)?;
|
||||||
let children: Vec<Handle> = block.entity_handles.clone();
|
|
||||||
|
|
||||||
let mut best: Option<(f64, EntityType, Vec<Handle>)> = None;
|
let mut best: Option<(f64, EntityType, Vec<Handle>)> = None;
|
||||||
for child in children {
|
for &child in &block.entity_handles {
|
||||||
let Some(child_entity) = self.document.get_entity(child) else {
|
let Some(child_entity) = self.document.get_entity(child) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
@@ -249,13 +209,11 @@ impl Scene {
|
|||||||
if common.invisible || layer.is_some_and(|l| l.flags.off || l.flags.frozen) || frozen {
|
if common.invisible || layer.is_some_and(|l| l.flags.off || l.flags.frozen) || frozen {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if matches!(child_entity, EntityType::Insert(_)) {
|
if let EntityType::Insert(child_insert) = child_entity {
|
||||||
let mut nested = Vec::new();
|
|
||||||
if let Some((entity, mut nested_path)) = self.descend_block_instance(
|
if let Some((entity, mut nested_path)) = self.descend_block_instance(
|
||||||
child_entity.clone(),
|
child_insert,
|
||||||
combined,
|
combined,
|
||||||
model_point,
|
model_point,
|
||||||
&mut nested,
|
|
||||||
depth + 1,
|
depth + 1,
|
||||||
viewport,
|
viewport,
|
||||||
kind,
|
kind,
|
||||||
@@ -300,8 +258,6 @@ impl Scene {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
let (_, entity, found_path) = best?;
|
let (_, entity, found_path) = best?;
|
||||||
path.clear();
|
|
||||||
path.extend(found_path.iter().copied());
|
|
||||||
Some((entity, found_path))
|
Some((entity, found_path))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+8
-120
@@ -187,17 +187,8 @@ impl AcceptedSnap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The two independent factors that turn a raw measured distance into the
|
/// Length dimensions store raw distances in their owning space. The displayed
|
||||||
/// number a linear/aligned/radial dimension displays.
|
/// value uses `user_lfac * viewport_compensation`; angular dimensions are exempt.
|
||||||
///
|
|
||||||
/// * `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)]
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
pub struct MeasurementScale {
|
pub struct MeasurementScale {
|
||||||
pub user_lfac: f64,
|
pub user_lfac: f64,
|
||||||
@@ -252,59 +243,13 @@ impl MeasurementScale {
|
|||||||
common.extended_data = data;
|
common.extended_data = data;
|
||||||
}
|
}
|
||||||
|
|
||||||
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.
|
/// Combined factor applied to a distance measured on the paper sheet.
|
||||||
pub fn paper_factor(&self) -> f64 {
|
pub fn paper_factor(&self) -> f64 {
|
||||||
self.user_lfac * self.viewport_compensation
|
self.user_lfac * self.viewport_compensation
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Factor applied to a distance already measured in model space (the
|
/// Positive DIMLFAC applies everywhere; negative DIMLFAC applies only on
|
||||||
/// viewport compensation must not be applied again).
|
/// paper. Zero means one. Saved viewport compensation is already in DIMLFAC.
|
||||||
pub fn model_factor(&self) -> f64 {
|
|
||||||
self.user_lfac
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The DXF DIMLFAC sign convention, resolved without a [`ViewportFrame`].
|
|
||||||
///
|
|
||||||
/// 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. 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 {
|
pub fn user_lfac_for_space(dimlfac: f64, paper_space: bool) -> f64 {
|
||||||
if dimlfac == 0.0 {
|
if dimlfac == 0.0 {
|
||||||
1.0
|
1.0
|
||||||
@@ -318,39 +263,6 @@ impl MeasurementScale {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// # Where the viewport compensation lives, once
|
|
||||||
///
|
|
||||||
/// A dimension that sits on a paper layout but measures model geometry seen
|
|
||||||
/// through a viewport has **paper** definition points and displays a **model**
|
|
||||||
/// number. Exactly one rule governs that, everywhere:
|
|
||||||
///
|
|
||||||
/// > `Dimension::actual_measurement` (DXF group 42) is always the **raw**
|
|
||||||
/// > geometric measurement in the dimension's *own* space — paper, for a
|
|
||||||
/// > viewport dimension. The viewport compensation lives **only** in the
|
|
||||||
/// > magnitude of the negative DIMLFAC override
|
|
||||||
/// > ([`MeasurementScale::viewport_dimlfac_override`]), and is applied
|
|
||||||
/// > **once**, at format time, by the linear formatter.
|
|
||||||
///
|
|
||||||
/// So for a 1:10 viewport and a user DIMLFAC of 1: a 100-unit model line is
|
|
||||||
/// 10 paper units, `actual_measurement` is `10`, the override is `-10`,
|
|
||||||
/// [`MeasurementScale::user_lfac_for_space`] resolves that to `10` for a
|
|
||||||
/// paper-space dimension, and the text reads `100`.
|
|
||||||
///
|
|
||||||
/// The consequence for association refresh
|
|
||||||
/// (`Scene::refresh_associative_dimensions`) is that after re-placing a
|
|
||||||
/// viewport dimension's definition points on the sheet it must write the
|
|
||||||
/// **paper** measurement back — it must *not* substitute the model distance,
|
|
||||||
/// and it must *not* multiply by [`MeasurementScale::model_factor`]. Doing
|
|
||||||
/// either makes a refreshed dimension read a different number from an
|
|
||||||
/// identical freshly-created one (`user_lfac` too many times). This is the
|
|
||||||
/// rule that makes `attach_viewport_dimension_association` + refresh agree
|
|
||||||
/// with creation by construction, and it is why `actual_measurement` stays
|
|
||||||
/// raw: that is also what the DXF format specifies and what export writes.
|
|
||||||
///
|
|
||||||
/// Angular dimensions are exempt on both counts — a similarity preserves
|
|
||||||
/// angles, so there is no compensation and no override.
|
|
||||||
pub mod measurement_rule {}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -385,34 +297,10 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn negative_dimlfac_only_applies_through_viewports() {
|
fn dimlfac_sign_depends_on_dimension_space() {
|
||||||
let f = frame(0.5, 0.0);
|
for (factor, paper, model) in [(-2.0, 2.0, 1.0), (0.0, 1.0, 1.0), (3.5, 3.5, 3.5)] {
|
||||||
let through = MeasurementScale::from_dimlfac(-2.0, Some(&f));
|
assert_eq!(MeasurementScale::user_lfac_for_space(factor, true), paper);
|
||||||
assert_eq!(through.user_lfac, 2.0);
|
assert_eq!(MeasurementScale::user_lfac_for_space(factor, false), model);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn user_lfac_for_space_matches_from_dimlfac() {
|
|
||||||
let f = frame(0.5, 0.0);
|
|
||||||
for lfac in [-2.0, 0.0, 1.0, 3.5] {
|
|
||||||
assert_eq!(
|
|
||||||
MeasurementScale::user_lfac_for_space(lfac, true),
|
|
||||||
MeasurementScale::from_dimlfac(lfac, Some(&f)).user_lfac
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
MeasurementScale::user_lfac_for_space(lfac, false),
|
|
||||||
MeasurementScale::from_dimlfac(lfac, None).user_lfac
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+62
-40
@@ -9,9 +9,9 @@ use iced::time::Instant;
|
|||||||
use iced::{Point, Rectangle};
|
use iced::{Point, Rectangle};
|
||||||
|
|
||||||
use cadkernel::geom2d::Curve;
|
use cadkernel::geom2d::Curve;
|
||||||
|
use acadrust::types::Handle;
|
||||||
|
|
||||||
use crate::command::{DimensionAssociationSource, TangentObject};
|
use crate::command::{DimensionAssociationSource, TangentObject};
|
||||||
use acadrust::types::Handle;
|
|
||||||
use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel};
|
use crate::scene::model::wire_model::{SnapHint, TangentGeom, WireModel};
|
||||||
use crate::scene::pick::interaction_index::WireSource;
|
use crate::scene::pick::interaction_index::WireSource;
|
||||||
const DEFAULT_OSNAP_RADIUS_PX: f32 = 15.0;
|
const DEFAULT_OSNAP_RADIUS_PX: f32 = 15.0;
|
||||||
@@ -90,7 +90,7 @@ pub struct SnapResult {
|
|||||||
/// Layout viewport this snap was seen *through*, when the snap ran against
|
/// Layout viewport this snap was seen *through*, when the snap ran against
|
||||||
/// model geometry displayed by a paper-space viewport. `None` for ordinary
|
/// model geometry displayed by a paper-space viewport. `None` for ordinary
|
||||||
/// model-space / paper-sheet snaps. Filled by the paper-space viewport snap
|
/// model-space / paper-sheet snaps. Filled by the paper-space viewport snap
|
||||||
/// query, never by the engine itself. (PR1)
|
/// query, never by the engine itself.
|
||||||
pub viewport: Option<Handle>,
|
pub viewport: Option<Handle>,
|
||||||
/// Owning entity of the geometry the snap landed on, when the engine could
|
/// Owning entity of the geometry the snap landed on, when the engine could
|
||||||
/// attribute the feature to a wire.
|
/// attribute the feature to a wire.
|
||||||
@@ -102,7 +102,7 @@ pub struct SnapResult {
|
|||||||
/// the instance path needs the document and the accepted model point, so
|
/// the instance path needs the document and the accepted model point, so
|
||||||
/// it happens once at accept time, in
|
/// it happens once at accept time, in
|
||||||
/// `OpenCADStudio::resolve_source_block_path`, which fills
|
/// `OpenCADStudio::resolve_source_block_path`, which fills
|
||||||
/// [`crate::scene::viewport_ref::SnapSourceRef::block_path`]. (PR1)
|
/// [`crate::scene::viewport_ref::SnapSourceRef::block_path`].
|
||||||
pub source: Option<DimensionAssociationSource>,
|
pub source: Option<DimensionAssociationSource>,
|
||||||
/// Second real object at an intersection, when both objects are known.
|
/// Second real object at an intersection, when both objects are known.
|
||||||
pub secondary_source: Option<DimensionAssociationSource>,
|
pub secondary_source: Option<DimensionAssociationSource>,
|
||||||
@@ -996,10 +996,10 @@ impl Snapper {
|
|||||||
extension_base2: None,
|
extension_base2: None,
|
||||||
extension_origin: None,
|
extension_origin: None,
|
||||||
extension_dir: None,
|
extension_dir: None,
|
||||||
viewport: None,
|
viewport: None,
|
||||||
source: None,
|
source: None,
|
||||||
secondary_source: None,
|
secondary_source: None,
|
||||||
model_point: None,
|
model_point: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1234,7 +1234,8 @@ impl Snapper {
|
|||||||
|
|
||||||
let mut try_pt = |world: glam::DVec3,
|
let mut try_pt = |world: glam::DVec3,
|
||||||
snap_type: SnapType,
|
snap_type: SnapType,
|
||||||
src: Option<DimensionAssociationSource>, secondary: Option<DimensionAssociationSource>| {
|
src: Option<DimensionAssociationSource>,
|
||||||
|
secondary: Option<DimensionAssociationSource>| {
|
||||||
let screen = world_to_screen(world, view_rot, eye, bounds);
|
let screen = world_to_screen(world, view_rot, eye, bounds);
|
||||||
if !in_bounds(screen) {
|
if !in_bounds(screen) {
|
||||||
return;
|
return;
|
||||||
@@ -1261,10 +1262,10 @@ impl Snapper {
|
|||||||
extension_base2: None,
|
extension_base2: None,
|
||||||
extension_origin: None,
|
extension_origin: None,
|
||||||
extension_dir: None,
|
extension_dir: None,
|
||||||
viewport: None,
|
viewport: None,
|
||||||
source: src,
|
source: src,
|
||||||
secondary_source: secondary,
|
secondary_source: secondary,
|
||||||
model_point: None,
|
model_point: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1318,7 +1319,9 @@ impl Snapper {
|
|||||||
try_pt(
|
try_pt(
|
||||||
DVec3::from_array(point),
|
DVec3::from_array(point),
|
||||||
SnapType::Endpoint,
|
SnapType::Endpoint,
|
||||||
wire_source(wire), None);
|
wire_source(wire),
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// Tessellated open curves have no key-vertex set. Their only
|
// Tessellated open curves have no key-vertex set. Their only
|
||||||
// endpoints are first/last, so testing candidate wires remains
|
// endpoints are first/last, so testing candidate wires remains
|
||||||
@@ -1338,7 +1341,9 @@ impl Snapper {
|
|||||||
try_pt(
|
try_pt(
|
||||||
wp_f64(wire, wire.points.len() - 1),
|
wp_f64(wire, wire.points.len() - 1),
|
||||||
SnapType::Endpoint,
|
SnapType::Endpoint,
|
||||||
wire_source(wire), None);
|
wire_source(wire),
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1361,7 +1366,9 @@ impl Snapper {
|
|||||||
try_pt(
|
try_pt(
|
||||||
wp_f64(wire, wire.points.len() - 1),
|
wp_f64(wire, wire.points.len() - 1),
|
||||||
SnapType::Endpoint,
|
SnapType::Endpoint,
|
||||||
src, None);
|
src,
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1412,7 +1419,9 @@ impl Snapper {
|
|||||||
try_pt(
|
try_pt(
|
||||||
nearest_on_segment(cursor_world, seg.a, seg.b),
|
nearest_on_segment(cursor_world, seg.a, seg.b),
|
||||||
SnapType::Nearest,
|
SnapType::Nearest,
|
||||||
wires.source_wire(seg.wire).and_then(wire_source), None);
|
wires.source_wire(seg.wire).and_then(wire_source),
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for wire in in_range_wires.iter() {
|
for wire in in_range_wires.iter() {
|
||||||
@@ -1436,7 +1445,9 @@ impl Snapper {
|
|||||||
try_pt(
|
try_pt(
|
||||||
foot,
|
foot,
|
||||||
SnapType::Perpendicular,
|
SnapType::Perpendicular,
|
||||||
wires.source_wire(seg.wire).and_then(wire_source), None);
|
wires.source_wire(seg.wire).and_then(wire_source),
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -1465,7 +1476,9 @@ impl Snapper {
|
|||||||
try_pt(
|
try_pt(
|
||||||
point,
|
point,
|
||||||
SnapType::Intersection,
|
SnapType::Intersection,
|
||||||
wires.source_wire(segment.wire).and_then(wire_source), None);
|
wires.source_wire(segment.wire).and_then(wire_source),
|
||||||
|
None,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1563,7 +1576,7 @@ impl Snapper {
|
|||||||
for &wire_j in &local_wires[idx + 1..] {
|
for &wire_j in &local_wires[idx + 1..] {
|
||||||
if let Some(points) = exact_curve_intersections(wire_i, wire_j) {
|
if let Some(points) = exact_curve_intersections(wire_i, wire_j) {
|
||||||
// Two wires meet here; attribute the feature to the
|
// Two wires meet here; attribute the feature to the
|
||||||
// first. PR3 resolves the true pair.
|
// first. Association resolution requires both source paths.
|
||||||
let src = wire_source(wire_i);
|
let src = wire_source(wire_i);
|
||||||
for point in points {
|
for point in points {
|
||||||
try_pt(point, SnapType::Intersection, src, wire_source(wire_j));
|
try_pt(point, SnapType::Intersection, src, wire_source(wire_j));
|
||||||
@@ -1600,7 +1613,9 @@ impl Snapper {
|
|||||||
try_pt(
|
try_pt(
|
||||||
pt,
|
pt,
|
||||||
SnapType::Intersection,
|
SnapType::Intersection,
|
||||||
wires.source_wire(a.wire).and_then(wire_source), wires.source_wire(b.wire).and_then(wire_source));
|
wires.source_wire(a.wire).and_then(wire_source),
|
||||||
|
wires.source_wire(b.wire).and_then(wire_source),
|
||||||
|
);
|
||||||
if exact_cursor {
|
if exact_cursor {
|
||||||
break 'intersection_sweep;
|
break 'intersection_sweep;
|
||||||
}
|
}
|
||||||
@@ -1646,7 +1661,12 @@ impl Snapper {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(pt) = seg_intersect_3d(a0, a1, b0, b1) {
|
if let Some(pt) = seg_intersect_3d(a0, a1, b0, b1) {
|
||||||
try_pt(pt, SnapType::Intersection, wire_source(wire_i), wire_source(wire_j));
|
try_pt(
|
||||||
|
pt,
|
||||||
|
SnapType::Intersection,
|
||||||
|
wire_source(wire_i),
|
||||||
|
wire_source(wire_j),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1772,7 +1792,9 @@ impl Snapper {
|
|||||||
try_pt(
|
try_pt(
|
||||||
segment_a.a + ta as f64 * (segment_a.b - segment_a.a),
|
segment_a.a + ta as f64 * (segment_a.b - segment_a.a),
|
||||||
SnapType::ApparentIntersection,
|
SnapType::ApparentIntersection,
|
||||||
wires.source_wire(segment_a.wire).and_then(wire_source), wires.source_wire(segment_b.wire).and_then(wire_source));
|
wires.source_wire(segment_a.wire).and_then(wire_source),
|
||||||
|
wires.source_wire(segment_b.wire).and_then(wire_source),
|
||||||
|
);
|
||||||
if dist2(apparent_screen, cursor_screen) <= f32::EPSILON {
|
if dist2(apparent_screen, cursor_screen) <= f32::EPSILON {
|
||||||
break 'apparent_sweep;
|
break 'apparent_sweep;
|
||||||
}
|
}
|
||||||
@@ -1812,7 +1834,9 @@ impl Snapper {
|
|||||||
try_pt(
|
try_pt(
|
||||||
wa0 + ta as f64 * (wa1 - wa0),
|
wa0 + ta as f64 * (wa1 - wa0),
|
||||||
SnapType::ApparentIntersection,
|
SnapType::ApparentIntersection,
|
||||||
wire_source(wire_i), wire_source(wire_j));
|
wire_source(wire_i),
|
||||||
|
wire_source(wire_j),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2033,10 +2057,10 @@ impl Snapper {
|
|||||||
extension_base2: None,
|
extension_base2: None,
|
||||||
extension_origin: None,
|
extension_origin: None,
|
||||||
extension_dir: None,
|
extension_dir: None,
|
||||||
viewport: None,
|
viewport: None,
|
||||||
source: wire_source(wire),
|
source: wire_source(wire),
|
||||||
secondary_source: None,
|
secondary_source: None,
|
||||||
model_point: None,
|
model_point: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2206,19 +2230,13 @@ fn snap_tier(t: SnapType) -> u8 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Candidate ordering: tier first, then cursor distance, and only when two
|
/// Merge paper and viewport snaps using the engine's ordering.
|
||||||
/// candidates are effectively equidistant (coincident features) the classic
|
/// Both screen positions must use canvas pixels.
|
||||||
/// sub-priority — so an Endpoint still beats an Intersection sitting on the
|
pub fn merge_snap(
|
||||||
/// exact same point. Distances are screen-px²; 4.0 ≈ a 2 px coincidence band.
|
a: Option<SnapResult>,
|
||||||
/// Choose between two independently-computed snap results using the SAME
|
b: Option<SnapResult>,
|
||||||
/// priority-then-distance rule the engine applies internally
|
cursor: Point,
|
||||||
/// ([`snap_better`]): a higher-priority snap inside the aperture wins even
|
) -> Option<SnapResult> {
|
||||||
/// when a lower-priority one is nearer, and distance only breaks ties.
|
|
||||||
///
|
|
||||||
/// Used to merge the ordinary paper-sheet snap with the viewport snap taken
|
|
||||||
/// through a layout viewport (PR1). Both `screen` fields must be expressed in
|
|
||||||
/// the same pixel frame.
|
|
||||||
pub fn merge_snap(a: Option<SnapResult>, b: Option<SnapResult>, cursor: Point) -> Option<SnapResult> {
|
|
||||||
match (a, b) {
|
match (a, b) {
|
||||||
(Some(a), Some(b)) => {
|
(Some(a), Some(b)) => {
|
||||||
let (at, asub) = (snap_tier(a.snap_type), snap_priority(a.snap_type));
|
let (at, asub) = (snap_tier(a.snap_type), snap_priority(a.snap_type));
|
||||||
@@ -2235,6 +2253,10 @@ pub fn merge_snap(a: Option<SnapResult>, b: Option<SnapResult>, cursor: Point) -
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Candidate ordering: tier first, then cursor distance, and only when two
|
||||||
|
/// candidates are effectively equidistant (coincident features) the classic
|
||||||
|
/// sub-priority — so an Endpoint still beats an Intersection sitting on the
|
||||||
|
/// exact same point. Distances are screen-px²; 4.0 ≈ a 2 px coincidence band.
|
||||||
fn snap_better(tier: u8, d2: f32, sub: u8, best: (u8, f32, u8)) -> bool {
|
fn snap_better(tier: u8, d2: f32, sub: u8, best: (u8, f32, u8)) -> bool {
|
||||||
let (bt, bd2, bsub) = best;
|
let (bt, bd2, bsub) = best;
|
||||||
if tier != bt {
|
if tier != bt {
|
||||||
|
|||||||
Vendored
+7
-13
@@ -1,16 +1,10 @@
|
|||||||
# Viewport association fixture
|
# Viewport association fixture
|
||||||
|
|
||||||
`viewport_associations.dxf` is a synthetic paper-space fixture for line, circle,
|
`viewport_associations.dxf` contains line, circle, arc, polyline, spline,
|
||||||
arc, polyline, spline, block-instance, and intersection references through one
|
block-instance, and intersection references through a 1:2 viewport.
|
||||||
1:2 viewport. `src/app/viewport_dimension_tests.rs` loads it and exercises actual
|
`src/app/viewport_dimension_tests.rs` tests refresh and intersection source edits.
|
||||||
association refresh and an edit to the second intersection source.
|
|
||||||
|
|
||||||
Each dimension carries `DIMLFAC=-2` and `ACAD_DIMASSOC_CALC_DIMLFAC=-2`. These are
|
Each dimension stores `DIMLFAC=-2` and `ACAD_DIMASSOC_CALC_DIMLFAC=-2` so its paper
|
||||||
necessary for its paper definition points and saved model measurements to agree;
|
definition points measure in model units. The spline tangent from `(0, 300)`
|
||||||
the original fixture omitted both factors. The spline tangent also uses the
|
touches `(90, 283.125)` at parameter `0.75`; tests verify its tangency and preserve
|
||||||
actual touch point `(90, 283.125)` at parameter `0.75`, measured from `(0, 300)`.
|
the dimension if an edited reference cannot resolve.
|
||||||
The original endpoint `(120, 300)` was not tangent. Tests verify the cross product
|
|
||||||
with the spline derivative and preservation after an unsupported reference edit.
|
|
||||||
|
|
||||||
The larger original `115448.dxf` is private and is not included. An opt-in test
|
|
||||||
uses `OPENCAD_VIEWPORT_REGRESSION_DXF` to read a local copy without modifying it.
|
|
||||||
Reference in new issue
Block a user