215 lines
9.1 KiB
Rust
215 lines
9.1 KiB
Rust
//! Dragging a part that joints place moves it as far as its joints allow: each frame runs
|
|
//! `joint.reach`, taking the point grabbed toward the pointer in the plane through it that faces
|
|
//! the view. The whole drag is one undo step.
|
|
|
|
use egui::Pos2;
|
|
use serde_json::json;
|
|
use solidcraft_engine::doc::{apply_point, joints, mat_inverse};
|
|
use solidcraft_engine::geom::Vec3;
|
|
|
|
use crate::SolveApp;
|
|
use crate::viewport::{Hit, Proj};
|
|
|
|
/// A drag under way.
|
|
#[derive(Clone, Debug)]
|
|
pub struct Drag {
|
|
occurrence: u64,
|
|
/// The point grabbed, in the occurrence's own coordinates.
|
|
local: Vec3,
|
|
/// The plane the pointer moves the point in: through where it was grabbed, facing the view.
|
|
origin: Vec3,
|
|
normal: Vec3,
|
|
/// The drag's undo step is on top of the history (it went in with the first frame that moved).
|
|
pushed: bool,
|
|
}
|
|
|
|
/// Begin a drag on the part under `p` when joints place it and leave it free to move (it is
|
|
/// neither grounded nor fixed to what is). True when one began.
|
|
pub fn start(app: &mut SolveApp, proj: &Proj, p: Pos2) -> bool {
|
|
// The face under the pointer, past the part's own edges and vertices (not past sketches or
|
|
// construction geometry in front of it).
|
|
let hits = crate::viewport::pick(app, proj, p);
|
|
let Some((body, at)) =
|
|
hits.into_iter().take_while(|h| matches!(h, Hit::Face { .. } | Hit::Edge { .. } | Hit::Vertex { .. })).find_map(|h| match h {
|
|
Hit::Face { body, point, .. } => Some((body, point)),
|
|
_ => None,
|
|
})
|
|
else {
|
|
return false;
|
|
};
|
|
let s = &app.session;
|
|
let Some(occurrence) = crate::dialogs_assembly::occurrence_of_body(s, &body).filter(|o| *o != 0) else { return false };
|
|
if !s.doc.assembly.joints.iter().any(|j| !j.suppressed && (j.a.occurrence == occurrence || j.b.occurrence == occurrence)) {
|
|
return false;
|
|
}
|
|
let sol = joints::solve(&s.doc, &s.model.state());
|
|
if !sol.world.contains_key(&occurrence) || sol.dof.get(&occurrence).is_none_or(|d| *d == 0) {
|
|
return false;
|
|
}
|
|
let Some(local) = mat_inverse(&joints::occurrence_world(&s.doc, occurrence)).map(|inv| apply_point(&inv, at)) else { return false };
|
|
let normal = proj.ray(p).1;
|
|
app.viewport.joint_drag = Some(Drag { occurrence, local, origin: at, normal, pushed: false });
|
|
true
|
|
}
|
|
|
|
pub fn active(app: &SolveApp) -> bool {
|
|
app.viewport.joint_drag.is_some()
|
|
}
|
|
|
|
/// Reach for the point under `p`.
|
|
pub fn to(app: &mut SolveApp, proj: &Proj, p: Pos2) {
|
|
let Some(d) = app.viewport.joint_drag.clone() else { return };
|
|
let (o, dir) = proj.ray(p);
|
|
let facing = dir.dot(d.normal);
|
|
if facing.abs() < 1e-9 {
|
|
return;
|
|
}
|
|
let target = o + dir * ((d.origin - o).dot(d.normal) / facing);
|
|
if !target.is_finite() {
|
|
return;
|
|
}
|
|
let at = apply_point(&joints::occurrence_world(&app.session.doc, d.occurrence), d.local);
|
|
let params =
|
|
json!({"occurrence": d.occurrence, "at": [at.x, at.y, at.z], "point": [target.x, target.y, target.z], "quiet": true, "continuous": true});
|
|
// One undo step for the whole drag: the first frame's, kept on top while the others run (not
|
|
// truncated by depth, which a full history would make lose its oldest steps frame by frame).
|
|
let own = if d.pushed { app.session.undo.pop() } else { None };
|
|
// A frame that changes the design adds an undo step (a full history drops its oldest for it,
|
|
// so its length says nothing).
|
|
let before = app.session.doc.clone();
|
|
let pushed = app.run("joint.reach", params).is_ok() && !std::sync::Arc::ptr_eq(&before, &app.session.doc);
|
|
match own {
|
|
Some(step) => {
|
|
if pushed {
|
|
app.session.undo.pop();
|
|
}
|
|
app.session.undo.push(step);
|
|
}
|
|
None if pushed => {
|
|
if let Some(d) = app.viewport.joint_drag.as_mut() {
|
|
d.pushed = true;
|
|
}
|
|
}
|
|
None => {}
|
|
}
|
|
}
|
|
|
|
pub fn end(app: &mut SolveApp) {
|
|
app.viewport.joint_drag = None;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use serde_json::{Value, json};
|
|
use solidcraft_engine::doc::{apply_point, joints};
|
|
use solidcraft_engine::geom::Vec3;
|
|
|
|
use crate::scenario::Harness;
|
|
|
|
fn run(h: &mut Harness, id: &str, p: Value) -> Value {
|
|
h.app.run(id, p.clone()).unwrap_or_else(|e| panic!("{id} {p}: {e}"))
|
|
}
|
|
|
|
fn screen(h: &mut Harness, p: Vec3) -> egui::Pos2 {
|
|
let [x, y] = h.at(&json!({"world": [p.x, p.y, p.z]})).expect("on screen");
|
|
egui::pos2(x as f32, y as f32)
|
|
}
|
|
|
|
/// Where the arm's point `local` (its own coordinates) is.
|
|
fn arm_at(h: &Harness, local: Vec3) -> Vec3 {
|
|
let s = &h.app.session;
|
|
let c = s.doc.find_component("Arm").unwrap_or(0);
|
|
let o = s.doc.occurrence_of(c).map(|o| o.id).unwrap_or(0);
|
|
apply_point(&joints::occurrence_world(&s.doc, o), local)
|
|
}
|
|
|
|
/// An arm hinged about +Z on a grounded base (seen from the top): its far end, and the screen
|
|
/// points of that end and of the end turned a quarter round.
|
|
fn hinged_arm() -> (Harness, Vec3, egui::Pos2, egui::Pos2) {
|
|
let mut h = Harness::new();
|
|
h.app.home.open = false;
|
|
for (name, corner, size) in [("Base", [-5, -5, 0], [10, 10, 5]), ("Arm", [-3, -3, 5], [63, 6, 5])] {
|
|
run(&mut h, "component.activate", json!({"component": "root"}));
|
|
run(&mut h, "component.create", json!({"name": name}));
|
|
run(&mut h, "solid.box", json!({"length": size[0], "width": size[1], "height": size[2], "corner": corner}));
|
|
}
|
|
run(&mut h, "component.activate", json!({"component": "root"}));
|
|
let base = h.app.session.doc.find_component("Base").and_then(|c| h.app.session.doc.occurrence_of(c)).map(|o| o.id).unwrap_or(0);
|
|
run(&mut h, "occurrence.ground", json!({"occurrence": base, "grounded": true}));
|
|
let at = |o: &str| json!({"occurrence": o, "point": [0, 0, 5], "z": [0, 0, 1]});
|
|
run(&mut h, "joint.create", json!({"type": "revolute", "a": at("Base"), "b": at("Arm"), "flip": true, "name": "Hinge"}));
|
|
h.call("ui.view", json!({"view": "top"}));
|
|
h.app.cam.distance = 200.0;
|
|
h.frames(3);
|
|
let end = Vec3::new(55.0, 0.0, 10.0);
|
|
let (a, b) = (screen(&mut h, end), screen(&mut h, Vec3::new(0.0, 55.0, 10.0)));
|
|
(h, end, a, b)
|
|
}
|
|
|
|
fn play(h: &mut Harness) {
|
|
while !h.app.synthetic.is_empty() {
|
|
h.frame();
|
|
}
|
|
h.frames(2);
|
|
}
|
|
|
|
/// Press at `a` and move to `b` in 12 frames, holding the button there.
|
|
fn press_and_move(h: &mut Harness, a: egui::Pos2, b: egui::Pos2) {
|
|
h.app.synthetic.push(egui::Event::PointerMoved(a));
|
|
h.app.synthetic.push(egui::Event::PointerButton {
|
|
pos: a,
|
|
button: egui::PointerButton::Primary,
|
|
pressed: true,
|
|
modifiers: Default::default(),
|
|
});
|
|
h.app.synthetic.extend((1..=12).map(|k| egui::Event::PointerMoved(a + (b - a) * (k as f32 / 12.0))));
|
|
play(h);
|
|
}
|
|
|
|
fn release(h: &mut Harness, b: egui::Pos2) {
|
|
h.app.synthetic.push(egui::Event::PointerButton {
|
|
pos: b,
|
|
button: egui::PointerButton::Primary,
|
|
pressed: false,
|
|
modifiers: Default::default(),
|
|
});
|
|
play(h);
|
|
}
|
|
|
|
/// Dragged by its far end a quarter turn round, the arm turns there in one undo step, and undo
|
|
/// turns it back.
|
|
#[test]
|
|
fn dragging_a_jointed_part_turns_it_about_its_hinge() {
|
|
let (mut h, end, a, b) = hinged_arm();
|
|
let undo = h.app.session.undo.len();
|
|
press_and_move(&mut h, a, b);
|
|
assert!(h.app.viewport.joint_drag.is_some() && h.app.viewport.boxsel.is_none(), "the drag moves the arm, not a selection box");
|
|
release(&mut h, b);
|
|
assert!(h.app.viewport.joint_drag.is_none());
|
|
assert!(arm_at(&h, end).dist(Vec3::new(0.0, 55.0, 10.0)) < 0.5, "{:?}", arm_at(&h, end));
|
|
assert_eq!(h.app.session.undo.len(), undo + 1, "one undo step for the drag");
|
|
assert!(!h.app.session.log.iter().any(|l| l.contains("Reach")), "frames are not echoed: {:?}", h.app.session.log);
|
|
run(&mut h, "edit.undo", json!({}));
|
|
assert!(arm_at(&h, end).dist(end) < 1e-9, "{:?}", arm_at(&h, end));
|
|
}
|
|
|
|
/// With the undo history full, a drag still adds one step: it drops the oldest once, not one
|
|
/// per frame.
|
|
#[test]
|
|
fn a_drag_on_a_full_undo_history_drops_one_step() {
|
|
let (mut h, _, a, b) = hinged_arm();
|
|
let step = h.app.session.undo.last().cloned().expect("an undo step");
|
|
while h.app.session.undo.len() < 200 {
|
|
let mut older = step.clone();
|
|
older.label = format!("step {}", h.app.session.undo.len());
|
|
h.app.session.undo.insert(0, older);
|
|
}
|
|
let second = h.app.session.undo.get(1).map(|s| s.label.clone());
|
|
press_and_move(&mut h, a, b);
|
|
release(&mut h, b);
|
|
assert_eq!(h.app.session.undo.len(), 200);
|
|
assert_eq!(h.app.session.undo.first().map(|s| s.label.clone()), second, "only the oldest step went");
|
|
assert_eq!(h.app.session.undo.last().map(|s| s.label.as_str()), Some("Reach Point"), "the drag's step is on top");
|
|
}
|
|
}
|