forked from 1b-agent/opencadstudio-upstream
Two viewport-navigation defects, both measured against commercial solutions.
The cursor stayed a hand during a zoom. `selection_overlay` took one boolean
for all three interactive navigation tools, so ZOOM Dynamic and ORBIT both
showed PAN's grab hand — which reads as "drag the sheet", the wrong promise
for either. Split it into a `NavCursor` enum: PAN keeps the open/closed hand,
ORBIT gets four-way arrows, and ZOOM Dynamic gets the magnifier carrying the
sign of the zoom actually under way — "+" dragging up, "-" dragging down,
latched with a 2 px dead zone so a hand held still cannot strobe the glyph.
ZOOM Dynamic also reused `box_anchor` to remember where a drag began, so a
selection marquee rubber-banded across the screen while the view zoomed; the
drawing is now suppressed, the state left alone for `reproject_box_anchor`.
ZOOM EXTENTS did not reach the whole drawing, for three separate reasons, now
answered by one shared `Scene::visible_block_bounds`:
- A quartile filter dropped any wire whose centroid sat outside the
inter-quartile consensus of the others. It was meant to stop one junk wire
from a bad parse poisoning the box, but it could not tell parser junk from
an outlying detail, a survey point, or a block placed away from the rest,
and silently refused to frame them. Gone; the absolute 1e16 sentinel gate
the paper path already used is the only filter, and the XLine / Ray display
segments that motivated the heuristic are handled explicitly.
- HATCH tessellates to no wires at all, and the model path never read the
fill models, so a drawing whose outermost geometry is a fill — or which is
nothing but fills — had nothing to frame. It now folds them in, reusing the
renderer's own warm cache on the hot path rather than re-exploding every
INSERT per ZOOM EXTENTS.
- Inside a floating viewport the fit came from `compute_model_space_extents`,
which walked only `key_vertices` — empty for every tessellated curve.
Circles, arcs, ellipses and splines contributed nothing. Fixing it also
corrects plot Extents, sheet-set framing, viewport auto-fit and SpaceMouse
model bounds, which all inherited that blind spot.
Bounds stay f64 into the camera: a UTM drawing lost whole metres to the old
f32 round-trip. Drawing extents also no longer read the camera — the result is
cached per geometry epoch, and a floating viewport's camera is derived FROM
the extents, so `camera_for_viewport` recursed into a stack overflow.
`model_space_extents_mixed_entities_accuracy` asserted the curve-blind result;
it now asserts the true bounds.
Co-Authored-By: Claude Opus 5 <[email protected]>
124 lines
4.2 KiB
Rust
124 lines
4.2 KiB
Rust
//! ZOOM EXTENTS must frame every visible entity, the way commercial solutions
|
|
//! do — no heuristic allowed to decide that part of the drawing does not count.
|
|
//!
|
|
//! Two things used to stop it. A quartile filter dropped any wire whose
|
|
//! centroid sat far from the consensus of the others, so a detail parked away
|
|
//! from the main body was silently left outside the fit. And HATCH tessellates
|
|
//! to no wires at all, so a drawing whose outermost geometry is a fill had
|
|
//! nothing for the model-space walk to read.
|
|
#![cfg(not(target_arch = "wasm32"))]
|
|
|
|
use codec::entities::hatch::{BoundaryEdge, BoundaryPath, LineEdge};
|
|
use codec::entities::{Hatch, Line};
|
|
use codec::types::{Vector2, Vector3};
|
|
use codec::EntityType;
|
|
use OpenCADStudio::scene::Scene;
|
|
|
|
fn add_line(scene: &mut Scene, x1: f64, y1: f64, x2: f64, y2: f64) {
|
|
let mut line = Line::new();
|
|
line.start = Vector3::new(x1, y1, 0.0);
|
|
line.end = Vector3::new(x2, y2, 0.0);
|
|
scene.add_entity(EntityType::Line(line));
|
|
}
|
|
|
|
/// A solid square fill with corners (x, y) .. (x + size, y + size).
|
|
fn square_hatch(x: f64, y: f64, size: f64) -> Hatch {
|
|
let mut path = BoundaryPath::new();
|
|
for (sx, sy, ex, ey) in [
|
|
(0.0, 0.0, size, 0.0),
|
|
(size, 0.0, size, size),
|
|
(size, size, 0.0, size),
|
|
(0.0, size, 0.0, 0.0),
|
|
] {
|
|
path.edges.push(BoundaryEdge::Line(LineEdge {
|
|
start: Vector2::new(x + sx, y + sy),
|
|
end: Vector2::new(x + ex, y + ey),
|
|
}));
|
|
}
|
|
let mut hatch = Hatch::new();
|
|
hatch.paths.push(path);
|
|
hatch
|
|
}
|
|
|
|
#[test]
|
|
fn fit_all_reaches_an_entity_parked_far_from_the_rest() {
|
|
let mut scene = Scene::new();
|
|
// A dense consensus cluster — more than the eight wires the old quartile
|
|
// filter needed before it switched itself on.
|
|
for i in 0..12 {
|
|
let x = i as f64;
|
|
add_line(&mut scene, x, 0.0, x, 10.0);
|
|
}
|
|
// …and one line far outside it. Nothing about it is invalid: it is simply
|
|
// somewhere else, which is exactly what the filter could not tell apart
|
|
// from parser junk.
|
|
add_line(&mut scene, 50_000.0, 50_000.0, 50_010.0, 50_010.0);
|
|
|
|
scene.fit_all();
|
|
|
|
// The fit centres on the extents box, so the target alone says whether the
|
|
// remote line counted: dropping it leaves the camera on the cluster at
|
|
// about (5.5, 5) instead of halfway out to (25005, 25005).
|
|
let cam = scene.camera.borrow();
|
|
assert!(
|
|
(cam.target.x - 25_005.0).abs() < 50.0 && (cam.target.y - 25_005.0).abs() < 50.0,
|
|
"the fit must span cluster and remote line, target = {:?}",
|
|
cam.target
|
|
);
|
|
assert!(
|
|
cam.ortho_size() >= 25_005.0,
|
|
"the view must be wide enough to hold both, ortho_size = {}",
|
|
cam.ortho_size()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fit_all_frames_a_hatch_only_drawing() {
|
|
let mut scene = Scene::new();
|
|
scene.add_entity(EntityType::Hatch(square_hatch(100.0, 200.0, 40.0)));
|
|
|
|
scene.fit_all();
|
|
|
|
// Nothing else is in the drawing, so without the fill the camera never
|
|
// moves and stays on the origin.
|
|
let cam = scene.camera.borrow();
|
|
assert!(
|
|
(cam.target.x - 120.0).abs() < 1.0 && (cam.target.y - 220.0).abs() < 1.0,
|
|
"a hatch is geometry: ZOOM EXTENTS must centre on it, target = {:?}",
|
|
cam.target
|
|
);
|
|
assert!(
|
|
cam.ortho_size() >= 20.0,
|
|
"the view must hold the whole fill, ortho_size = {}",
|
|
cam.ortho_size()
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn fit_all_keeps_a_hatch_outside_the_wires_in_view() {
|
|
let mut scene = Scene::new();
|
|
for i in 0..12 {
|
|
let x = i as f64;
|
|
add_line(&mut scene, x, 0.0, x, 10.0);
|
|
}
|
|
// The fill is the outermost thing in the drawing; the wires alone would
|
|
// frame a 12 x 10 strip and leave it off screen.
|
|
scene.add_entity(EntityType::Hatch(square_hatch(500.0, 500.0, 50.0)));
|
|
|
|
scene.fit_all();
|
|
|
|
// Wires alone centre near (5.5, 5); including the fill pulls the centre
|
|
// out to about (275, 275).
|
|
let cam = scene.camera.borrow();
|
|
assert!(
|
|
cam.target.x > 200.0 && cam.target.y > 200.0,
|
|
"the fill must widen the fit past the wires, target = {:?}",
|
|
cam.target
|
|
);
|
|
assert!(
|
|
cam.ortho_size() >= 275.0,
|
|
"the view must reach the far corner of the fill, ortho_size = {}",
|
|
cam.ortho_size()
|
|
);
|
|
}
|