mirror of
https://github.com/bherbruck/solidcraft.git
synced 2026-11-03 10:35:58 +00:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 80af7bbe3e | |||
| 191e983423 |
Generated
+17
-17
@@ -4078,7 +4078,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
dependencies = [
|
||||
"block2 0.6.2",
|
||||
"eframe",
|
||||
@@ -4109,11 +4109,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-3dx-client"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-cli"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
dependencies = [
|
||||
"image",
|
||||
"serde_json",
|
||||
@@ -4124,7 +4124,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-derive"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -4133,7 +4133,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-doc"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -4150,7 +4150,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -4167,7 +4167,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-geom"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4176,7 +4176,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-gpu"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
dependencies = [
|
||||
"log",
|
||||
"naga",
|
||||
@@ -4193,7 +4193,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-io"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
dependencies = [
|
||||
"cadmpeg-core",
|
||||
"cadmpeg-ir",
|
||||
@@ -4213,7 +4213,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-kernel"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
dependencies = [
|
||||
"log",
|
||||
"proptest",
|
||||
@@ -4233,7 +4233,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-mcp"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
@@ -4242,7 +4242,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-render"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
dependencies = [
|
||||
"image",
|
||||
"moxcms",
|
||||
@@ -4256,7 +4256,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-sketch"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -4267,11 +4267,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-space-mouse"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-egui"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
dependencies = [
|
||||
"arboard",
|
||||
"egui",
|
||||
@@ -4296,7 +4296,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-web"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -5826,7 +5826,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"sha2 0.10.9",
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
exclude = ["vendor"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.16.1"
|
||||
version = "0.16.2"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
|
||||
@@ -91,16 +91,8 @@ fn continuous(mut cam: Camera, p: &Value) -> Result<Value> {
|
||||
cam.pan(delta.x, delta.y, viewport()?.y);
|
||||
} else if p.get("pivot").is_some() {
|
||||
let pivot = req_vec3(cmd, p, "pivot")?;
|
||||
let size = viewport()?;
|
||||
let before = cam.to_screen(pivot, size.x, size.y, 1.0);
|
||||
cam.orbit(delta.x, delta.y);
|
||||
let after = cam.to_screen(pivot, size.x, size.y, 1.0);
|
||||
if let (Some((ax, ay, _)), Some((bx, by, _))) = (before, after) {
|
||||
// Pan operates at target depth. Perspective screen correction must instead
|
||||
// use the pivot's depth after orbiting; lateral pan leaves that depth fixed.
|
||||
let scale = if cam.fov > 0.0 { (cam.eye() - pivot).dot(cam.back()) / cam.distance } else { 1.0 };
|
||||
cam.pan((ax - bx) * scale, (ay - by) * scale, size.y);
|
||||
}
|
||||
viewport()?;
|
||||
cam.orbit_about(delta.x, delta.y, pivot);
|
||||
} else {
|
||||
cam.orbit(delta.x, delta.y);
|
||||
}
|
||||
|
||||
@@ -353,6 +353,25 @@ impl Camera {
|
||||
self.yaw = (self.yaw + dx * 0.008) % std::f64::consts::TAU;
|
||||
self.pitch = (self.pitch + dy * 0.008).clamp(-lim, lim);
|
||||
}
|
||||
/// Orbit by pixel deltas about `pivot`: the camera turns rigidly about it, so the pivot keeps
|
||||
/// its place in the camera's frame and on screen, in perspective as in parallel views.
|
||||
pub fn orbit_about(&mut self, dx: f64, dy: f64, pivot: Vec3) {
|
||||
let keep = self.frame_coords(pivot);
|
||||
self.orbit(dx, dy);
|
||||
self.place(pivot, keep);
|
||||
}
|
||||
/// Where `p` lies from the target along the camera's right, up and back axes.
|
||||
pub fn frame_coords(&self, p: Vec3) -> Vec3 {
|
||||
let (r, u, b) = self.basis();
|
||||
let d = p - self.target;
|
||||
Vec3::new(d.dot(r), d.dot(u), d.dot(b))
|
||||
}
|
||||
/// Move the target so `p` lies at `coords` (from [`Camera::frame_coords`]) in the camera's
|
||||
/// frame: with the distance unchanged, `p` is where it was on screen.
|
||||
pub fn place(&mut self, p: Vec3, coords: Vec3) {
|
||||
let (r, u, b) = self.basis();
|
||||
self.target = p - (r * coords.x + u * coords.y + b * coords.z);
|
||||
}
|
||||
/// Rotate the image about the viewing direction.
|
||||
pub fn roll_by(&mut self, radians: f64) {
|
||||
if radians.is_finite() {
|
||||
@@ -432,6 +451,24 @@ impl Camera {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Orbiting about a point keeps it on its pixel, with and without perspective and roll.
|
||||
#[test]
|
||||
fn orbit_about_keeps_the_pivot_on_screen() {
|
||||
let pivot = Vec3::new(12.0, -7.0, 9.0);
|
||||
for fov in [0.0, 0.6] {
|
||||
for roll in [0.0, 0.4] {
|
||||
let mut cam = Camera { target: Vec3::new(3.0, 4.0, -2.0), distance: 150.0, yaw: 0.3, pitch: 0.5, roll, fov, ..Camera::default() };
|
||||
let before = cam.to_screen(pivot, 800.0, 600.0, 100.0).unwrap();
|
||||
for _ in 0..10 {
|
||||
cam.orbit_about(25.0, -12.0, pivot);
|
||||
}
|
||||
let after = cam.to_screen(pivot, 800.0, 600.0, 100.0).unwrap();
|
||||
assert!((after.0 - before.0).abs() < 1e-6 && (after.1 - before.1).abs() < 1e-6, "fov {fov}, roll {roll}: {before:?} -> {after:?}");
|
||||
assert!((cam.eye() - pivot).len() > 1.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn front_view_projects_target_to_centre() {
|
||||
let mut c = Camera { target: Vec3::new(1.0, 2.0, 3.0), distance: 50.0, ..Default::default() };
|
||||
|
||||
@@ -574,7 +574,8 @@ pub(crate) struct OrbitSnap {
|
||||
pub(crate) target: Camera,
|
||||
pub(crate) yaw_velocity: f64,
|
||||
pub(crate) pitch_velocity: f64,
|
||||
pub(crate) pivot: Option<(Vec3, Pos2)>,
|
||||
/// The pivot and where it lies in the camera's frame ([`Camera::frame_coords`]).
|
||||
pub(crate) pivot: Option<(Vec3, Vec3)>,
|
||||
}
|
||||
|
||||
/// Pointer motion accumulates independently of the spring preview, so many small deltas
|
||||
@@ -610,10 +611,7 @@ pub(crate) fn orbit_drag(app: &mut SolveApp, rect: Rect, dx: f64, dy: f64, press
|
||||
} else {
|
||||
None
|
||||
}
|
||||
.and_then(|p| {
|
||||
app.cam = displayed;
|
||||
projection(app, rect).to_screen(p).map(|at| (p, at))
|
||||
});
|
||||
.map(|p| (p, displayed.frame_coords(p)));
|
||||
let spring = app.viewport.orbit_snap.get_or_insert(OrbitSnap { target, yaw_velocity: 0.0, pitch_velocity: 0.0, pivot });
|
||||
spring.target = target;
|
||||
spring.pivot = pivot;
|
||||
@@ -623,7 +621,7 @@ pub(crate) fn orbit_drag(app: &mut SolveApp, rect: Rect, dx: f64, dy: f64, press
|
||||
|
||||
/// A lightly damped spring: mostly settled in about a third of a second, with a small
|
||||
/// elastic finish. Bounded substeps keep the behavior consistent across frame rates.
|
||||
pub(crate) fn step_orbit_snap(app: &mut SolveApp, rect: Rect, dt: f64) {
|
||||
pub(crate) fn step_orbit_snap(app: &mut SolveApp, _rect: Rect, dt: f64) {
|
||||
let Some(mut spring) = app.viewport.orbit_snap.take() else { return };
|
||||
let dt = if dt.is_finite() { dt.clamp(0.0, 0.05) } else { 1.0 / 60.0 };
|
||||
let steps = (dt * 240.0).ceil().max(1.0) as usize;
|
||||
@@ -645,10 +643,8 @@ pub(crate) fn step_orbit_snap(app: &mut SolveApp, rect: Rect, dt: f64) {
|
||||
if done {
|
||||
app.cam = spring.target;
|
||||
}
|
||||
if let Some((pivot, at)) = spring.pivot
|
||||
&& let Some(after) = projection(app, rect).to_screen(pivot)
|
||||
{
|
||||
app.cam.pan(f64::from(at.x - after.x), f64::from(at.y - after.y), f64::from(rect.height().max(1.0)));
|
||||
if let Some((pivot, coords)) = spring.pivot {
|
||||
app.cam.place(pivot, coords);
|
||||
}
|
||||
if !done {
|
||||
app.viewport.orbit_snap = Some(spring);
|
||||
@@ -707,18 +703,7 @@ pub fn orbit(app: &mut SolveApp, rect: egui::Rect, dx: f64, dy: f64, press: Opti
|
||||
app.cam.orbit(dx, dy);
|
||||
return;
|
||||
};
|
||||
orbit_about(app, rect, dx, dy, pivot);
|
||||
}
|
||||
|
||||
/// Orbit by pixel deltas, keeping `pivot` still on screen: the camera turns about it and a pan
|
||||
/// correction puts it back where it was.
|
||||
pub(crate) fn orbit_about(app: &mut SolveApp, rect: egui::Rect, dx: f64, dy: f64, pivot: Vec3) {
|
||||
let before = crate::viewport::projection(app, rect).to_screen(pivot);
|
||||
app.cam.orbit(dx, dy);
|
||||
let after = crate::viewport::projection(app, rect).to_screen(pivot);
|
||||
if let (Some(a), Some(b)) = (before, after) {
|
||||
app.cam.pan(f64::from(a.x - b.x), f64::from(a.y - b.y), f64::from(rect.height().max(1.0)));
|
||||
}
|
||||
app.cam.orbit_about(dx, dy, pivot);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -5911,6 +5911,33 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// An orbit drag that starts on the model turns about the point under the cursor, which
|
||||
/// stays on its pixel, in perspective as in parallel views. The perspective view used to
|
||||
/// correct with a pan at the target's depth, so the point slid away and the view swung
|
||||
/// about the camera instead.
|
||||
#[test]
|
||||
fn cursor_orbit_keeps_the_picked_point_on_its_pixel() {
|
||||
for perspective in [false, true] {
|
||||
for offset in [vec2(30.0, 20.0), vec2(-60.0, 10.0), vec2(120.0, -40.0)] {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.preferences.orbit_around = crate::prefs::OrbitAround::Cursor;
|
||||
h.app.ui.perspective = perspective;
|
||||
h.app.run("solid.box", json!({"length":80,"width":50,"height":20})).unwrap();
|
||||
h.app.fit_view();
|
||||
let rect = h.app.viewport.rect.unwrap();
|
||||
let at = rect.center() + offset;
|
||||
let proj = projection(&h.app, rect);
|
||||
assert!(pick(&h.app, &proj, at).iter().any(|hit| matches!(hit, Hit::Face { .. })), "the drag starts on the box");
|
||||
let pivot = crate::prefs::pivot_under(&h.app, &proj, at);
|
||||
let before = h.app.cam;
|
||||
h.call("ui.drag", json!({"x0":at.x,"y0":at.y,"x1":at.x+150.0,"y1":at.y+60.0,"button":"middle","shift":true,"steps":20}));
|
||||
assert!((h.app.cam.yaw - before.yaw).abs() > 0.5, "the view turned");
|
||||
let after = projection(&h.app, rect).to_screen(pivot).unwrap();
|
||||
assert!(after.distance(at) < 0.5, "perspective={perspective}: {at:?} -> {after:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shift_orbit_keeps_cursor_pivot_and_default_middle_pan() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
|
||||
Reference in New Issue
Block a user