Files
OneCAD-Draft/tests/zoom_extents_coverage.rs
T
PanosandClaude Opus 5 d596b5a6be fix(view): zoom cursor per navigation tool, and ZOOM EXTENTS frames everything
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 <noreply@anthropic.com>
2026-10-01 16:27:26 +03:00

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()
);
}