forked from 1b-agent/opencadstudio-upstream
feat(viewport): unify in-viewport editing on the model-space path
Replace the leaky paper-projection approach to MSPACE editing with one coordinate adapter that routes all interaction through the viewport's own camera — the exact camera+rect the GPU draws the content with. - Add Scene::viewport_edit_frame(canvas) -> (camera_for_viewport, viewport_screen_rect): the unified adapter. Inside a floating viewport the viewport is the active pane, so snap/pick/grip/preview/DYN run in model space and land on the pixels the GPU draws. Tracks pan/zoom/twist exactly (twist handled by camera_from_view); crop is pixel-preserving so full-rect projection + cursor offset matches. - Route snap-move, click-pick, grip-drag, grip hit-test, grip render, hover (HoverDwellTick), selection (single/box/poly/cycle) and command entity-pick through it via pick_view / cursor_model_point helpers and an edit_cam-aware "active pane = viewport rect" override, so the existing tile offset plumbing just works. Fixes wrong-object hover/selection and stale snap after pan/zoom. - DYN: project anchors with the viewport camera and shift to canvas space so the guide/labels sit correctly (also fixes a latent tiled-model bug). - Separate grid snap from object snap entirely: new Snapper.grid_snap_on flag, Grid removed from the osnap mode set, evaluated independently of the osnap master. Object snap never catches grid; grid corners are picked only when grid snap is on, at lowest priority. Default off. - grip.rs: project_rte + grips_to_screen_rte / find_hit_grip_rte taking (view_rot, eye) so grips project through the composed view. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
4aa8e6681f
commit
a66d873e51
5 files changed
+428
-172
No files matched your search
+61
-19
@@ -50,7 +50,9 @@ pub const ALL_SNAP_MODES: &[(SnapType, &str, &str)] = &[
|
||||
(SnapType::Nearest, "✧", "Nearest"),
|
||||
(SnapType::ApparentIntersection, "✗", "Apparent Intersection"),
|
||||
(SnapType::Parallel, "∥", "Parallel"),
|
||||
(SnapType::Grid, "⊞", "Grid"),
|
||||
// NOTE: Grid is intentionally NOT an object-snap mode. Grid snap is a
|
||||
// separate system (`Snapper::grid_snap_on`) so object snap never catches a
|
||||
// grid point; it is toggled on its own and handled directly in `snap()`.
|
||||
];
|
||||
|
||||
// ── Snap result ───────────────────────────────────────────────────────────
|
||||
@@ -86,6 +88,9 @@ pub struct Snapper {
|
||||
pub snap_enabled: bool,
|
||||
/// Which snap modes are configured (used when `snap_enabled` is true).
|
||||
pub enabled: HashSet<SnapType>,
|
||||
/// Grid snap on/off — a system fully separate from object snap. When on,
|
||||
/// `snap()` can pick the nearest grid corner; object snap never does.
|
||||
pub grid_snap_on: bool,
|
||||
/// World-space grid spacing.
|
||||
pub grid_spacing: f32,
|
||||
/// Pixel-radius snap aperture, shared by OSNAP, tracking, polar and
|
||||
@@ -121,6 +126,7 @@ impl Default for Snapper {
|
||||
Self {
|
||||
snap_enabled: false,
|
||||
enabled,
|
||||
grid_snap_on: false,
|
||||
grid_spacing: 1.0,
|
||||
osnap_radius_px: CROSSHAIR_ARM * 0.25,
|
||||
otrack_enabled: false,
|
||||
@@ -147,6 +153,19 @@ impl Snapper {
|
||||
self.snap_enabled = !self.snap_enabled;
|
||||
}
|
||||
|
||||
/// Grid snap on/off — independent of the object-snap master and mode set.
|
||||
pub fn grid_snap(&self) -> bool {
|
||||
self.grid_snap_on
|
||||
}
|
||||
|
||||
pub fn toggle_grid_snap(&mut self) {
|
||||
self.grid_snap_on = !self.grid_snap_on;
|
||||
}
|
||||
|
||||
pub fn set_grid_snap(&mut self, on: bool) {
|
||||
self.grid_snap_on = on;
|
||||
}
|
||||
|
||||
pub fn toggle(&mut self, t: SnapType) {
|
||||
if !self.enabled.remove(&t) {
|
||||
self.enabled.insert(t);
|
||||
@@ -422,6 +441,7 @@ impl Snapper {
|
||||
s.insert(SnapType::Tangent);
|
||||
s
|
||||
},
|
||||
grid_snap_on: false,
|
||||
grid_spacing: self.grid_spacing,
|
||||
osnap_radius_px: self.osnap_radius_px,
|
||||
otrack_enabled: false,
|
||||
@@ -458,9 +478,6 @@ impl Snapper {
|
||||
grid_origin: Vec3,
|
||||
grid_rot: Mat4,
|
||||
) -> Option<SnapResult> {
|
||||
if !self.snap_enabled {
|
||||
return None;
|
||||
}
|
||||
// Object-snap selection is priority-then-distance, NOT nearest-wins.
|
||||
// "Continuous" snaps (Nearest, Perpendicular, …) sit on the geometry
|
||||
// and are therefore almost always closer to the cursor than a discrete
|
||||
@@ -473,6 +490,46 @@ impl Snapper {
|
||||
let mut best_rank = u8::MAX;
|
||||
let mut best_d2 = f32::MAX;
|
||||
|
||||
// ── Grid snap — a SEPARATE system from object snap ───────────────────
|
||||
// Grid snap has its own toggle (`grid_snap_on`) and is independent of
|
||||
// the object-snap master (`snap_enabled`) and the object-snap mode set.
|
||||
// Object snaps therefore NEVER catch grid points; only when grid snap is
|
||||
// on can a grid corner be picked. It is evaluated first and at the
|
||||
// lowest priority, so any object snap inside the aperture overrides it.
|
||||
if self.grid_snap_on {
|
||||
let s = self.grid_spacing as f64;
|
||||
if s.abs() > 1e-9 {
|
||||
// Round in the UCS grid frame, then map back to world.
|
||||
let ax = grid_rot.transform_vector3(Vec3::X).as_dvec3();
|
||||
let ay = grid_rot.transform_vector3(Vec3::Y).as_dvec3();
|
||||
let az = grid_rot.transform_vector3(Vec3::Z).as_dvec3();
|
||||
let origin = grid_origin.as_dvec3();
|
||||
let rel = cursor_world - origin;
|
||||
let ux = (rel.dot(ax) / s).round() * s;
|
||||
let uy = (rel.dot(ay) / s).round() * s;
|
||||
let uz = (rel.dot(az) / s).round() * s;
|
||||
let gp = origin + ax * ux + ay * uy + az * uz;
|
||||
let screen = world_to_screen(gp, view_rot, eye, bounds);
|
||||
let d2 = dist2(screen, cursor_screen);
|
||||
if d2 < radius2 {
|
||||
best = Some(SnapResult {
|
||||
world: gp,
|
||||
screen,
|
||||
snap_type: SnapType::Grid,
|
||||
tangent_obj: None,
|
||||
});
|
||||
best_rank = snap_priority(SnapType::Grid);
|
||||
best_d2 = d2;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Object snaps are gated by the object-snap master toggle. With it off
|
||||
// only the grid result (if any) stands.
|
||||
if !self.snap_enabled {
|
||||
return best;
|
||||
}
|
||||
|
||||
// World-space snap radius — derived from the view scale so wires whose
|
||||
// entire extent is clearly outside the snap circle can be skipped cheaply
|
||||
// before projecting any of their vertices to screen space.
|
||||
@@ -757,21 +814,6 @@ impl Snapper {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Grid ───────────────────────────────────────────────────────────
|
||||
if self.is_on(SnapType::Grid) {
|
||||
let s = self.grid_spacing as f64;
|
||||
// Round in the UCS grid frame, then map back to world.
|
||||
let ax = grid_rot.transform_vector3(Vec3::X).as_dvec3();
|
||||
let ay = grid_rot.transform_vector3(Vec3::Y).as_dvec3();
|
||||
let az = grid_rot.transform_vector3(Vec3::Z).as_dvec3();
|
||||
let origin = grid_origin.as_dvec3();
|
||||
let rel = cursor_world - origin;
|
||||
let ux = (rel.dot(ax) / s).round() * s;
|
||||
let uy = (rel.dot(ay) / s).round() * s;
|
||||
let uz = (rel.dot(az) / s).round() * s;
|
||||
try_pt(origin + ax * ux + ay * uy + az * uz, SnapType::Grid);
|
||||
}
|
||||
|
||||
// ── Tangent ────────────────────────────────────────────────────────
|
||||
// Operates directly on tangent_geoms geometry — independent of the
|
||||
// wire.points rendering structure so polyline segments work correctly.
|
||||
|
||||
Reference in new issue
Block a user