* Smart Extrude: direction from the picked side, Cut into a body stays a Cut (#1257) - A sketch profile (or a thin extrude's curve) extrudes toward the side it was picked from: from the bottom view it goes down. Body faces still go outward. - The automatic operation is latched per side of the profile: a drag that starts into a body is a Cut and stays one past the far side; a Join does not become a Cut when it reaches another body. Passing through zero decides afresh for the new side. - An explicit operation, Direction or Reverse ends the automatic choices. - auto_operation also looks just past the profile, so a long cut through a body reads as a Cut from the start (the engine's operation: "auto" too). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Scenario s07: the XZ profile seen from behind now extrudes toward the camera Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
165 lines
6.4 KiB
Rust
165 lines
6.4 KiB
Rust
//! Smart Extrude (#1257): which way a new extrude goes and what it does to the bodies, decided
|
|
//! from where the profile was picked and where the arrow is dragged, and kept steady while the
|
|
//! drag goes on.
|
|
//!
|
|
//! - Direction: a sketch profile (or a thin extrude's curve) extrudes toward the side it was
|
|
//! picked from, so a profile picked from the bottom view goes down. A body's planar face
|
|
//! always goes outward. Once the user sets the direction (Reverse, Direction), picks no
|
|
//! longer change it.
|
|
//! - Operation: the first choice for each side of the profile is latched. A drag that starts
|
|
//! into a body is a Cut and stays one however far it goes, through the far side included;
|
|
//! one that starts as a Join does not turn into a Cut when it reaches another body. When
|
|
//! the distance passes through zero onto the other side (a drag reversed past the start, or
|
|
//! Reverse), the operation is decided afresh for that side and latched there. A zero
|
|
//! distance changes nothing. Choosing an operation by hand ends the automatic choice for
|
|
//! the rest of the extrude.
|
|
|
|
use solidcraft_engine::Sel;
|
|
use solidcraft_engine::geom::Vec3;
|
|
|
|
/// The extrude window's automatic state.
|
|
#[derive(Clone, Debug, Default, PartialEq)]
|
|
pub struct Smart {
|
|
/// The picks the latch and the direction were decided for.
|
|
pub picked: Vec<Sel>,
|
|
/// The direction was set by hand (or comes from a saved extrude): picks keep it.
|
|
pub manual_dir: bool,
|
|
/// The automatic operation (an index into the operations) and the side it was chosen for.
|
|
pub latch: Option<(usize, i8)>,
|
|
}
|
|
|
|
/// One-side direction for a new pick: 0 (along the normal) or 1 (reversed). `ray` is the view
|
|
/// ray toward the pick; a ray running along the normal looks at the back of the profile, so
|
|
/// the extrude comes back toward the camera. Body faces always go outward.
|
|
pub fn picked_direction(ray: Vec3, normal: Vec3, body_face: bool) -> usize {
|
|
usize::from(!body_face && ray.dot(normal) > 1e-9)
|
|
}
|
|
|
|
/// Which side of the profile an extrude goes to: +1 along the normal, -1 against it, 0 for
|
|
/// none (a zero distance). Symmetric and two-sided extrudes go both ways, so always +1.
|
|
pub fn side(distance: f64, direction: usize) -> i8 {
|
|
if direction >= 2 {
|
|
return 1;
|
|
}
|
|
let d = if direction == 1 { -distance } else { distance };
|
|
if !d.is_finite() || d.abs() < 1e-9 {
|
|
0
|
|
} else if d > 0.0 {
|
|
1
|
|
} else {
|
|
-1
|
|
}
|
|
}
|
|
|
|
/// The operation to show: the latched one while the extrude stays on the side it was chosen
|
|
/// for, otherwise `fresh` (what the geometry says now), which is then latched for this side.
|
|
/// `current` stands while the distance is zero.
|
|
pub fn operation(fresh: usize, current: usize, side: i8, latch: &mut Option<(usize, i8)>) -> usize {
|
|
if side == 0 {
|
|
return current;
|
|
}
|
|
match *latch {
|
|
Some((op, s)) if s == side => op,
|
|
_ => {
|
|
*latch = Some((fresh, side));
|
|
fresh
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The view ray at `at`: from the eye for a perspective view, along the view for a parallel one.
|
|
pub fn view_ray(cam: &solidcraft_engine::render::Camera, at: Vec3) -> Vec3 {
|
|
if cam.fov > 0.0 { (at - cam.eye()).normalized().unwrap_or(-cam.back()) } else { -cam.back() }
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
const NEW: usize = 0;
|
|
const JOIN: usize = 1;
|
|
const CUT: usize = 2;
|
|
|
|
#[test]
|
|
fn direction_follows_the_picked_side() {
|
|
let up = Vec3::Z;
|
|
// From the top view the ray runs down: the extrude goes up (along the normal).
|
|
assert_eq!(picked_direction(-Vec3::Z, up, false), 0);
|
|
// From the bottom view the ray runs up: the extrude goes down.
|
|
assert_eq!(picked_direction(Vec3::Z, up, false), 1);
|
|
// Edge on: along the normal, as before.
|
|
assert_eq!(picked_direction(Vec3::X, up, false), 0);
|
|
// A body's face goes outward from either side.
|
|
assert_eq!(picked_direction(Vec3::Z, up, true), 0);
|
|
assert_eq!(picked_direction(-Vec3::Z, up, true), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn side_of_the_distance() {
|
|
assert_eq!(side(5.0, 0), 1);
|
|
assert_eq!(side(-5.0, 0), -1);
|
|
assert_eq!(side(5.0, 1), -1);
|
|
assert_eq!(side(-5.0, 1), 1);
|
|
assert_eq!(side(0.0, 0), 0);
|
|
assert_eq!(side(f64::NAN, 0), 0);
|
|
assert_eq!(side(-5.0, 2), 1, "symmetric");
|
|
assert_eq!(side(-5.0, 3), 1, "two sides");
|
|
}
|
|
|
|
/// A drag into a box that goes on past its far side: the geometry alone would say Join
|
|
/// there, the latch keeps the Cut.
|
|
#[test]
|
|
fn a_cut_stays_a_cut_past_the_far_side() {
|
|
let mut latch = None;
|
|
let mut op = JOIN;
|
|
// Into the body: Cut.
|
|
op = operation(CUT, op, -1, &mut latch);
|
|
assert_eq!(op, CUT);
|
|
// Further, then past the far side, where a fresh look would see no material.
|
|
for fresh in [CUT, CUT, JOIN, JOIN] {
|
|
op = operation(fresh, op, -1, &mut latch);
|
|
assert_eq!(op, CUT);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn a_join_does_not_turn_into_a_cut() {
|
|
let mut latch = None;
|
|
let mut op = operation(JOIN, NEW, 1, &mut latch);
|
|
assert_eq!(op, JOIN);
|
|
// The tool reaches another body further out.
|
|
op = operation(CUT, op, 1, &mut latch);
|
|
assert_eq!(op, JOIN);
|
|
}
|
|
|
|
/// Reversing through zero decides afresh for the new side; back again, afresh again.
|
|
#[test]
|
|
fn reversing_through_zero_decides_again() {
|
|
let mut latch = None;
|
|
let mut op = operation(CUT, JOIN, -1, &mut latch);
|
|
assert_eq!(op, CUT);
|
|
// At zero nothing changes.
|
|
op = operation(JOIN, op, 0, &mut latch);
|
|
assert_eq!(op, CUT);
|
|
assert_eq!(latch, Some((CUT, -1)));
|
|
// Out the other side, away from the body: Join, latched there.
|
|
op = operation(JOIN, op, 1, &mut latch);
|
|
assert_eq!(op, JOIN);
|
|
op = operation(CUT, op, 1, &mut latch);
|
|
assert_eq!(op, JOIN);
|
|
// Back into the body: Cut again.
|
|
op = operation(CUT, op, -1, &mut latch);
|
|
assert_eq!(op, CUT);
|
|
}
|
|
|
|
#[test]
|
|
fn view_ray_of_both_projections() {
|
|
let mut cam = solidcraft_engine::render::Camera { fov: 0.0, ..Default::default() };
|
|
assert!((view_ray(&cam, Vec3::ZERO) + cam.back()).len() < 1e-12);
|
|
cam.fov = 0.8;
|
|
let at = Vec3::new(10.0, 0.0, 0.0);
|
|
let r = view_ray(&cam, at);
|
|
assert!((r - (at - cam.eye()).normalized().unwrap()).len() < 1e-12);
|
|
}
|
|
}
|