forked from 1b-agent/opencadstudio-upstream
Keep grip edits on the drawing plane and preserve constraint undo
This commit is contained in:
1 parent
64e64c80a1
commit
78ab1c8013
4 files changed
+124
-30
No files matched your search
Generated
+2
-2
@@ -896,7 +896,7 @@ checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
|
||||
[[package]]
|
||||
name = "cadkernel"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=d3830bb5230bd80e9f8abaa78b9f85d15161536a#d3830bb5230bd80e9f8abaa78b9f85d15161536a"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=6ceda43233e59224b7683a69917b62ab26ea3ec3#6ceda43233e59224b7683a69917b62ab26ea3ec3"
|
||||
dependencies = [
|
||||
"acadrust",
|
||||
"cavalier_contours",
|
||||
@@ -907,7 +907,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "cadkernel-constraints"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=d3830bb5230bd80e9f8abaa78b9f85d15161536a#d3830bb5230bd80e9f8abaa78b9f85d15161536a"
|
||||
source = "git+https://github.com/HakanSeven12/cadkernel.git?rev=6ceda43233e59224b7683a69917b62ab26ea3ec3#6ceda43233e59224b7683a69917b62ab26ea3ec3"
|
||||
dependencies = [
|
||||
"nalgebra",
|
||||
]
|
||||
|
||||
+2
-2
@@ -32,8 +32,8 @@ rfd = "0.17"
|
||||
clap = { version = "4", features = ["derive", "string"] }
|
||||
env_logger = "0.11"
|
||||
acadrust = { git = "https://github.com/HakanSeven12/cadcodec.git", rev = "6f3711809e39e9ffdf16a63086079b6475d97175", features = ["serde"] }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "d3830bb5230bd80e9f8abaa78b9f85d15161536a", features = ["acis", "offset"] }
|
||||
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "d3830bb5230bd80e9f8abaa78b9f85d15161536a" }
|
||||
cadkernel = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6ceda43233e59224b7683a69917b62ab26ea3ec3", features = ["acis", "offset"] }
|
||||
cadkernel-constraints = { git = "https://github.com/HakanSeven12/cadkernel.git", rev = "6ceda43233e59224b7683a69917b62ab26ea3ec3" }
|
||||
dwg-thumbnailer = { path = "crates/dwg-thumbnailer" }
|
||||
flate2 = "1"
|
||||
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "bmp", "tiff"] }
|
||||
|
||||
@@ -1254,14 +1254,12 @@ impl OpenCADStudio {
|
||||
.filter(|handle| seen_handles.insert(*handle))
|
||||
.collect();
|
||||
|
||||
// Initialize once: constraint solves may add neighbors to this gesture.
|
||||
// Wire entities use the overlay; solid meshes stay visible and move live.
|
||||
if self.grip_preview_handles != edited_handles {
|
||||
if self.grip_preview_handles.is_empty() {
|
||||
if self.grip_dirty_before.is_none() {
|
||||
self.grip_dirty_before = Some(self.tabs[i].dirty);
|
||||
}
|
||||
for handle in std::mem::take(&mut self.grip_preview_handles) {
|
||||
self.tabs[i].scene.preview_hidden.remove(&handle);
|
||||
}
|
||||
// Interactive Add Vertex seeds this with the entity from
|
||||
// before insertion so append + placement is one undo step.
|
||||
// Normal grip drags still snapshot their current entities here.
|
||||
@@ -5964,6 +5962,61 @@ mod selection_preview_tests {
|
||||
use super::*;
|
||||
use crate::app::{HoverDwell, OpenCADStudio, HOVER_DWELL_MS};
|
||||
|
||||
#[test]
|
||||
fn grip_moves_keep_perpendicular_constraints_live_and_undoable() {
|
||||
use acadrust::{entities::Line, types::Vector3, EntityType};
|
||||
use crate::scene::sketch_constraints::{ConstraintKind, SketchRef};
|
||||
|
||||
let mut app = OpenCADStudio::new_for_test();
|
||||
app.automation_op(r#"{"op":"new"}"#);
|
||||
let i = app.active_tab;
|
||||
app.snapper.snap_enabled = false;
|
||||
app.snapper.grid_snap_on = false;
|
||||
app.snapper.otrack_enabled = false;
|
||||
app.ortho_mode = false;
|
||||
app.polar_mode = false;
|
||||
app.tabs[i].scene.selection.borrow_mut().vp_size = (800.0, 600.0);
|
||||
let a = app.tabs[i].scene.add_entity(EntityType::Line(Line::from_points(
|
||||
Vector3::new(-10.0, 0.0, 0.0), Vector3::new(10.0, 0.0, 0.0),
|
||||
)));
|
||||
let b = app.tabs[i].scene.add_entity(EntityType::Line(Line::from_points(
|
||||
Vector3::new(15.0, -10.0, 0.0), Vector3::new(15.0, 10.0, 0.0),
|
||||
)));
|
||||
let _ = app.apply_cmd_result(crate::command::CmdResult::AddSketchConstraint {
|
||||
kind: ConstraintKind::Perpendicular,
|
||||
refs: vec![SketchRef::whole(a), SketchRef::whole(b)],
|
||||
driving_param: None,
|
||||
label: "Perpendicular constraint",
|
||||
});
|
||||
let geometry = |app: &OpenCADStudio| [a, b].map(|handle| {
|
||||
match app.tabs[i].scene.document.get_entity(handle).unwrap() {
|
||||
EntityType::Line(line) => [line.start, line.end],
|
||||
_ => panic!("expected line"),
|
||||
}
|
||||
});
|
||||
let before = geometry(&app);
|
||||
app.tabs[i].active_grip = Some(GripEdit::single(a, 1, false, glam::DVec3::new(10.0, 0.0, 0.0)));
|
||||
for cursor in [Point::new(520.0, 220.0), Point::new(490.0, 180.0)] {
|
||||
let _ = app.on_viewport_move(cursor);
|
||||
let current = geometry(&app);
|
||||
for point in current.iter().flatten() {
|
||||
assert_eq!(point.z, 0.0, "grip left the drawing plane: {point:?}");
|
||||
}
|
||||
let direction = |ends: [Vector3; 2]| glam::DVec3::new(
|
||||
ends[1].x - ends[0].x, ends[1].y - ends[0].y, ends[1].z - ends[0].z,
|
||||
).normalize();
|
||||
assert!(direction(current[0]).dot(direction(current[1])).abs() < 1e-8);
|
||||
assert_ne!(current[1], before[1], "constrained neighbor must follow each grip frame");
|
||||
}
|
||||
let _ = app.on_viewport_left_release();
|
||||
assert!(app.tabs[i].active_grip.is_none());
|
||||
let after = geometry(&app);
|
||||
app.undo_steps(1);
|
||||
assert_eq!(geometry(&app), before);
|
||||
app.redo_steps(1);
|
||||
assert_eq!(geometry(&app), after);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
#[test]
|
||||
fn hyperlink_click_respects_existing_pointer_owners() {
|
||||
|
||||
+63
-22
@@ -317,10 +317,8 @@ impl Camera {
|
||||
))
|
||||
}
|
||||
|
||||
/// Unproject a screen point onto an arbitrary world plane in f64. The ray
|
||||
/// is built in eye-relative space (precise), intersected with the plane
|
||||
/// expressed relative to the eye, then shifted back by the f64 eye — so the
|
||||
/// returned world point keeps full precision at large absolute coordinates.
|
||||
/// Unproject in f64, with the kernel intersection relative to the plane
|
||||
/// origin so rendering precision never becomes a geometry elevation.
|
||||
pub fn unproject_on_plane(
|
||||
&self,
|
||||
screen: Point,
|
||||
@@ -329,32 +327,31 @@ impl Camera {
|
||||
plane_point: glam::DVec3,
|
||||
) -> glam::DVec3 {
|
||||
let eye = self.eye();
|
||||
let ndc_x = (screen.x / bounds.width) * 2.0 - 1.0;
|
||||
let ndc_y = 1.0 - (screen.y / bounds.height) * 2.0;
|
||||
let inv = self.view_proj_rte(bounds).inverse();
|
||||
let ndc_x = (screen.x as f64 / bounds.width as f64) * 2.0 - 1.0;
|
||||
let ndc_y = 1.0 - (screen.y as f64 / bounds.height as f64) * 2.0;
|
||||
let inv = self.view_proj_rte(bounds).as_dmat4().inverse();
|
||||
// Ray origin / direction in eye-relative space.
|
||||
let (ray_origin, ray_dir) = match self.projection {
|
||||
Projection::Perspective => {
|
||||
let near_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
|
||||
let far_pt = inv.project_point3(Vec3::new(ndc_x, ndc_y, 1.0));
|
||||
let near_pt = inv.project_point3(DVec3::new(ndc_x, ndc_y, 0.0));
|
||||
let far_pt = inv.project_point3(DVec3::new(ndc_x, ndc_y, 1.0));
|
||||
(near_pt, (far_pt - near_pt).normalize())
|
||||
}
|
||||
Projection::Orthographic => {
|
||||
let origin = inv.project_point3(Vec3::new(ndc_x, ndc_y, 0.0));
|
||||
let forward = self.rotation * Vec3::NEG_Z;
|
||||
let origin = inv.project_point3(DVec3::new(ndc_x, ndc_y, 0.0));
|
||||
let forward = (self.rotation * Vec3::NEG_Z).as_dvec3();
|
||||
(origin, forward)
|
||||
}
|
||||
};
|
||||
// Plane point relative to the eye (small) for a precise intersection.
|
||||
let plane_rel = (plane_point - eye).as_vec3();
|
||||
let denom = ray_dir.dot(plane_normal);
|
||||
let rel_hit = if denom.abs() < 1e-6 {
|
||||
plane_rel
|
||||
} else {
|
||||
let t = (plane_rel - ray_origin).dot(plane_normal) / denom;
|
||||
ray_origin + ray_dir * t
|
||||
};
|
||||
eye + rel_hit.as_dvec3()
|
||||
cadkernel::space::plane::intersect_line_plane(
|
||||
(ray_origin - (plane_point - eye)).to_array(),
|
||||
ray_dir.to_array(),
|
||||
[0.0; 3],
|
||||
plane_normal.as_dvec3().to_array(),
|
||||
1e-6,
|
||||
)
|
||||
.map(|point| plane_point + DVec3::from_array(point))
|
||||
.unwrap_or(plane_point)
|
||||
}
|
||||
|
||||
/// Eye position split into two f32 (high + low) emulating f64, for the
|
||||
@@ -389,7 +386,7 @@ impl Camera {
|
||||
|
||||
/// Project a screen point onto the plane through the orbit target.
|
||||
pub fn pick_on_target_plane(&self, screen: Point, bounds: Rectangle) -> glam::DVec3 {
|
||||
let forward = (self.target.as_vec3() - self.eye().as_vec3()).normalize_or(Vec3::NEG_Z);
|
||||
let forward = self.rotation * Vec3::NEG_Z;
|
||||
self.unproject_on_plane(screen, bounds, forward, self.target)
|
||||
}
|
||||
|
||||
@@ -904,6 +901,50 @@ mod rte_tests {
|
||||
use super::*;
|
||||
use iced::Rectangle;
|
||||
|
||||
#[test]
|
||||
fn cursor_picking_preserves_the_requested_plane() {
|
||||
let bounds = Rectangle::new(Point::ORIGIN, iced::Size::new(800.0, 600.0));
|
||||
for projection in [Projection::Orthographic, Projection::Perspective] {
|
||||
for distance in [0.01, 60.36, 71.08106, 100_000.0] {
|
||||
for origin in [DVec3::ZERO, DVec3::new(639_792.184_2, 4_517_057.531_7, 12.5)] {
|
||||
let camera = Camera {
|
||||
target: origin,
|
||||
distance,
|
||||
projection,
|
||||
..Camera::default()
|
||||
};
|
||||
for cursor in [Point::new(400.0, 300.0), Point::new(530.0, 215.0)] {
|
||||
let point = camera.pick_on_target_plane(cursor, bounds);
|
||||
assert_eq!(point.z, origin.z, "{projection:?}, distance={distance}, {point:?}");
|
||||
let normal = Vec3::new(0.2, -0.3, 1.0).normalize();
|
||||
let point = camera.pick_on_plane(cursor, bounds, normal, origin);
|
||||
let residual = (point - origin).dot(normal.as_dvec3());
|
||||
assert!(residual.abs() < 1e-9, "plane residual={residual}, {projection:?}, distance={distance}");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn target_plane_picking_is_translation_invariant() {
|
||||
let bounds = Rectangle::new(Point::ORIGIN, iced::Size::new(800.0, 600.0));
|
||||
let cursor = Point::new(530.0, 215.0);
|
||||
for projection in [Projection::Orthographic, Projection::Perspective] {
|
||||
let camera = Camera {
|
||||
rotation: Quat::from_rotation_z(0.7) * Quat::from_rotation_x(0.4),
|
||||
projection,
|
||||
distance: 0.01,
|
||||
..Camera::default()
|
||||
};
|
||||
let offset = DVec3::new(639_792.184_2, 4_517_057.531_7, 12.5);
|
||||
let translated = Camera { target: offset, ..camera.clone() };
|
||||
let here = camera.pick_on_target_plane(cursor, bounds);
|
||||
let there = translated.pick_on_target_plane(cursor, bounds) - offset;
|
||||
assert!(here.distance(there) < 1e-9, "{projection:?}: {here:?} vs {there:?}");
|
||||
}
|
||||
}
|
||||
|
||||
/// The relative-to-eye view carries rotation and nothing else, so where the
|
||||
/// camera stands must not reach it at all.
|
||||
///
|
||||
|
||||
Reference in new issue
Block a user