View cube with chamfered corners: its only targets are the 6 faces (orthographic views) and the 8 corner facets (iso views); edges are not targets, a point on one goes to a neighbouring face or facet, and hover lights exactly the face or facet under the pointer. Design decision.

Fixes #15

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-09 20:28:16 +00:00
1 parent eef2430bc7
commit 0a1ab40ea0
4 files changed
+202 -247

No files matched your search

+15
View File
@@ -548,6 +548,21 @@ pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> {
return Err(format!("handle order: got {xs:?}")); return Err(format!("handle order: got {xs:?}"));
} }
} }
"camera" => {
// {"camera": {"yaw_deg": y, "pitch_deg": p}}: where the camera looks from (±0.5°),
// once any view animation has finished.
for _ in 0..240 {
if h.app.cam_anim.is_none() {
break;
}
h.frame();
}
let (yaw, pitch) = (h.app.cam.yaw.to_degrees(), h.app.cam.pitch.to_degrees());
let near = |got: f64, want: &Value| want.as_f64().is_none_or(|w| ((got - w + 540.0).rem_euclid(360.0) - 180.0).abs() < 0.5);
if !near(yaw, &v["yaw_deg"]) || !near(pitch, &v["pitch_deg"]) {
return Err(format!("camera yaw {yaw:.2}°, pitch {pitch:.2}°, want {v}"));
}
}
"count" => { "count" => {
// {"count": {"name": n, "value": x}}: a published count. // {"count": {"name": n, "value": x}}: a published count.
let n = v["name"].as_str().unwrap_or_default(); let n = v["name"].as_str().unwrap_or_default();
+165 -138
View File
@@ -5,7 +5,7 @@ use egui::{Align2, Color32, FontId, Pos2, Rect, Sense, Shape, Stroke, pos2, vec2
use serde_json::json; use serde_json::json;
use solvecraft_engine::Sel; use solvecraft_engine::Sel;
use solvecraft_engine::geom::{Vec2, Vec3}; use solvecraft_engine::geom::{Vec2, Vec3};
use solvecraft_engine::render::{Camera, Mat4, Rgb, StandardView}; use solvecraft_engine::render::{Camera, Mat4, Rgb};
use solvecraft_engine::sketch::{ConstraintKind, CurveKind}; use solvecraft_engine::sketch::{ConstraintKind, CurveKind};
use solvecraft_engine::view::{colors, grid_step, sketch_consumed, sketch_lines}; use solvecraft_engine::view::{colors, grid_step, sketch_consumed, sketch_lines};
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
@@ -1880,74 +1880,46 @@ pub(crate) fn cube_point(app: &SolveApp, rect: Rect, p: Vec3) -> Pos2 {
/// Half a view cube face, in pixels. /// Half a view cube face, in pixels.
const CUBE_SCALE: f64 = 32.0; const CUBE_SCALE: f64 = 32.0;
/// The view cube (top right): click a face for a standard view; the house resets. /// The view cube (top right): a cube with chamfered corners. Its 14 targets are the 6 faces
/// (orthographic views) and the 8 corner facets (iso views); the edges between them are not
/// targets. Hover lights the face or facet under the pointer, a click turns the view to it, the
/// house goes home.
fn view_cube(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) { fn view_cube(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
let t = Tokens::get(); let t = Tokens::get();
let c = pos2(rect.right() - 80.0, rect.top() + 80.0); let c = pos2(rect.right() - 80.0, rect.top() + 80.0);
let s = CUBE_SCALE;
let mut lit = 0;
let (r, u, b) = app.cam.basis(); let (r, u, b) = app.cam.basis();
let to2 = |p: Vec3| -> (Pos2, f64) { (pos2(c.x + (p.dot(r) * s) as f32, c.y - (p.dot(u) * s) as f32), p.dot(b)) };
let faces: [(Vec3, &str, StandardView); 6] = [
(Vec3::new(0.0, -1.0, 0.0), "FRONT", StandardView::Front),
(Vec3::new(0.0, 1.0, 0.0), "BACK", StandardView::Back),
(Vec3::new(1.0, 0.0, 0.0), "RIGHT", StandardView::Right),
(Vec3::new(-1.0, 0.0, 0.0), "LEFT", StandardView::Left),
(Vec3::Z, "TOP", StandardView::Top),
(Vec3::new(0.0, 0.0, -1.0), "BOTTOM", StandardView::Bottom),
];
let painter = ui.painter_at(rect); let painter = ui.painter_at(rect);
let hover = ui.input(|i| i.pointer.hover_pos()); let hover = ui.input(|i| i.pointer.hover_pos());
let mut order: Vec<usize> = (0..6).collect(); let targets = cube_targets();
order.sort_by(|a, b2| faces[*a].0.dot(b).total_cmp(&faces[*b2].0.dot(b))); let shapes = cube_projected(&targets, (r, u, b), c);
let visible: Vec<usize> = order.into_iter().filter(|i| faces[*i].0.dot(b) > 1e-3).collect(); let hovered = hover.and_then(|h| cube_hit(&shapes, h));
let outline = |n: Vec3| -> Vec<Pos2> { // Back to front: the faces and facets turned toward the camera.
let (e1, e2) = cube_face_axes(n); for (i, pts) in shapes.iter().rev() {
[n - e1 - e2, n + e1 - e2, n + e1 + e2, n - e1 + e2].iter().map(|p| to2(*p).0).collect() let tg = &targets[*i];
}; let n = tg.normal();
// The region under the cursor (a face, edge or corner, as the view direction a click
// turns to): found on the frontmost face under it, the same zones a click uses.
let region: Option<Vec3> = hover.and_then(|h| {
visible.iter().rev().find_map(|i| {
let n = faces[*i].0;
let pts = outline(n);
let (e1, e2) = cube_face_axes(n);
point_in_poly(h, &pts).then(|| cube_zone(&pts, h)).flatten().map(|(zi, zj)| n + e1 * f64::from(zi) + e2 * f64::from(zj))
})
});
for i in &visible {
let (n, label, _) = faces[*i];
let pts = outline(n);
let shade = (0.75 + 0.25 * n.dot(b)) as f32; let shade = (0.75 + 0.25 * n.dot(b)) as f32;
painter.add(Shape::convex_polygon(pts, t.cube_face.gamma_multiply(shade).to_opaque(), Stroke::new(1.0, t.cube_edge))); let fill = if tg.corner { t.cube_face.gamma_multiply(shade * 0.92) } else { t.cube_face.gamma_multiply(shade) };
// Every patch of this face that belongs to the hovered region: a corner lights its painter.add(Shape::convex_polygon(pts.clone(), fill.to_opaque(), Stroke::new(1.0, t.cube_edge)));
// three patches, an edge its two strips, a face its middle. if hovered == Some(*i) {
let (e1, e2) = cube_face_axes(n); painter.add(Shape::convex_polygon(pts.clone(), t.accent_soft, Stroke::new(1.0, t.accent)));
for (zi, zj) in region.map(|d| cube_patches_on(n, d)).unwrap_or_default() {
let lo = |k: i32| match k {
-1 => (-1.0, -1.0 + 2.0 * CUBE_BAND),
0 => (-1.0 + 2.0 * CUBE_BAND, 1.0 - 2.0 * CUBE_BAND),
_ => (1.0 - 2.0 * CUBE_BAND, 1.0),
};
let ((a0, a1), (b0, b1)) = (lo(zi), lo(zj));
let zp: Vec<Pos2> = [(a0, b0), (a1, b0), (a1, b1), (a0, b1)].iter().map(|(x, y)| to2(n + e1 * *x + e2 * *y).0).collect();
painter.add(Shape::convex_polygon(zp, t.accent_soft, Stroke::NONE));
lit += 1;
} }
let center = to2(n).0; let centre = cube_centroid(pts);
if n.dot(b) > 0.35 { crate::scenario::publish_handle(&format!("cube:{}", tg.name), centre);
painter.text(center, Align2::CENTER_CENTER, label, FontId::proportional(9.5), Color32::from_rgb(40, 44, 52)); if !tg.corner && n.dot(b) > 0.35 {
painter.text(centre, Align2::CENTER_CENTER, tg.name.to_uppercase(), FontId::proportional(9.5), Color32::from_rgb(40, 44, 52));
} }
} }
// Clicked view direction (target → eye): the hovered region. crate::scenario::publish_count("cube_highlights", f64::from(u8::from(hovered.is_some())));
let clicked = region.filter(|_| ui.input(|i| i.pointer.primary_clicked())); if let Some(i) = hovered {
crate::scenario::publish_count("cube_highlights", f64::from(lit)); crate::scenario::publish_handle(&format!("cube_hover:{}", targets[i].name), hover.unwrap_or(c));
}
// Clicked: the view from the face's or facet's direction (target → eye).
let clicked = hovered.filter(|_| ui.input(|i| i.pointer.primary_clicked())).map(|i| targets[i].dir);
// Axis triad at the cube's corner. // Axis triad at the cube's corner.
let to2 = |p: Vec3| pos2(c.x + (p.dot(r) * CUBE_SCALE) as f32, c.y - (p.dot(u) * CUBE_SCALE) as f32);
let o = Vec3::new(-1.0, -1.0, -1.0); let o = Vec3::new(-1.0, -1.0, -1.0);
for (d, col) in [(Vec3::X, colors::AXIS_X), (Vec3::Y, colors::AXIS_Y), (Vec3::Z, colors::AXIS_Z)] { for (d, col) in [(Vec3::X, colors::AXIS_X), (Vec3::Y, colors::AXIS_Y), (Vec3::Z, colors::AXIS_Z)] {
let (a, _) = to2(o); painter.line_segment([to2(o), to2(o + d * 2.6)], Stroke::new(2.0, Color32::from_rgb(col.0, col.1, col.2)));
let (bb, _) = to2(o + d * 2.6);
painter.line_segment([a, bb], Stroke::new(2.0, Color32::from_rgb(col.0, col.1, col.2)));
} }
let home = Rect::from_center_size(pos2(c.x - 52.0, c.y - 48.0), vec2(18.0, 18.0)); let home = Rect::from_center_size(pos2(c.x - 52.0, c.y - 48.0), vec2(18.0, 18.0));
let hr = ui.interact(home, ui.id().with("vc_home"), Sense::click()); let hr = ui.interact(home, ui.id().with("vc_home"), Sense::click());
@@ -1961,7 +1933,101 @@ fn view_cube(app: &mut SolveApp, ui: &mut egui::Ui, rect: Rect) {
} }
} }
/// The in-face axes of a view cube face (the zone grid's u and v). /// How far along each edge a view cube corner is cut off (the cube's faces are at ±1).
const CUBE_CHAMFER: f64 = 0.38;
/// A view cube target: a face or a corner facet, its view direction (target → eye) and outline
/// (cube units).
#[derive(Clone, Debug)]
pub(crate) struct CubeTarget {
pub name: String,
pub dir: Vec3,
pub corner: bool,
pub outline: Vec<Vec3>,
}
impl CubeTarget {
fn normal(&self) -> Vec3 {
self.dir.normalized().unwrap_or(Vec3::Z)
}
}
/// The 14 targets: 6 octagonal faces and 8 triangular corner facets.
pub(crate) fn cube_targets() -> Vec<CubeTarget> {
let k = CUBE_CHAMFER;
let mut out = Vec::new();
for (n, name) in [
(Vec3::new(0.0, -1.0, 0.0), "front"),
(Vec3::new(0.0, 1.0, 0.0), "back"),
(Vec3::new(1.0, 0.0, 0.0), "right"),
(Vec3::new(-1.0, 0.0, 0.0), "left"),
(Vec3::Z, "top"),
(Vec3::new(0.0, 0.0, -1.0), "bottom"),
] {
let (e1, e2) = cube_face_axes(n);
let uv =
[(-1.0 + k, -1.0), (1.0 - k, -1.0), (1.0, -1.0 + k), (1.0, 1.0 - k), (1.0 - k, 1.0), (-1.0 + k, 1.0), (-1.0, 1.0 - k), (-1.0, -1.0 + k)];
out.push(CubeTarget { name: name.into(), dir: n, corner: false, outline: uv.iter().map(|(a, b2)| n + e1 * *a + e2 * *b2).collect() });
}
for sz in [1.0, -1.0] {
for sy in [-1.0, 1.0] {
for sx in [1.0, -1.0] {
let corner = Vec3::new(sx, sy, sz);
let name = format!(
"{}-{}-{}",
if sz > 0.0 { "top" } else { "bottom" },
if sy < 0.0 { "front" } else { "back" },
if sx > 0.0 { "right" } else { "left" }
);
let outline = vec![corner - Vec3::X * (sx * k), corner - Vec3::Y * (sy * k), corner - Vec3::Z * (sz * k)];
out.push(CubeTarget { name, dir: corner, corner: true, outline });
}
}
}
out
}
/// The targets turned toward the camera (basis `rub`), front first, as screen polygons around
/// the cube's centre `c`.
pub(crate) fn cube_projected(targets: &[CubeTarget], rub: (Vec3, Vec3, Vec3), c: Pos2) -> Vec<(usize, Vec<Pos2>)> {
let (r, u, b) = rub;
let to2 = |p: Vec3| pos2(c.x + (p.dot(r) * CUBE_SCALE) as f32, c.y - (p.dot(u) * CUBE_SCALE) as f32);
let mut v: Vec<(usize, f64, Vec<Pos2>)> = targets
.iter()
.enumerate()
.filter(|(_, tg)| tg.normal().dot(b) > 1e-3)
.map(|(i, tg)| (i, tg.normal().dot(b), tg.outline.iter().map(|p| to2(*p)).collect()))
.collect();
v.sort_by(|a, b2| b2.1.total_cmp(&a.1));
v.into_iter().map(|(i, _, p)| (i, p)).collect()
}
/// The target under `p`. The faces and facets tile the cube, so a point on an edge between two
/// of them (where neither polygon claims it) goes to the nearer one, within a few pixels.
pub(crate) fn cube_hit(shapes: &[(usize, Vec<Pos2>)], p: Pos2) -> Option<usize> {
if let Some((i, _)) = shapes.iter().find(|(_, pts)| point_in_poly(p, pts)) {
return Some(*i);
}
shapes.iter().map(|(i, pts)| (*i, poly_distance(p, pts))).filter(|(_, d)| *d <= 3.0).min_by(|a, b| a.1.total_cmp(&b.1)).map(|(i, _)| i)
}
fn poly_distance(p: Pos2, pts: &[Pos2]) -> f32 {
(0..pts.len())
.map(|i| {
let (a, b) = (pts[i], pts[(i + 1) % pts.len()]);
let ab = b - a;
let t = ((p - a).dot(ab) / ab.length_sq().max(1e-6)).clamp(0.0, 1.0);
p.distance(a + ab * t)
})
.fold(f32::INFINITY, f32::min)
}
fn cube_centroid(pts: &[Pos2]) -> Pos2 {
let n = pts.len().max(1) as f32;
pos2(pts.iter().map(|p| p.x).sum::<f32>() / n, pts.iter().map(|p| p.y).sum::<f32>() / n)
}
/// The in-face axes of a view cube face.
fn cube_face_axes(n: Vec3) -> (Vec3, Vec3) { fn cube_face_axes(n: Vec3) -> (Vec3, Vec3) {
if n.z.abs() > 0.5 { if n.z.abs() > 0.5 {
(Vec3::X, Vec3::Y) (Vec3::X, Vec3::Y)
@@ -1972,52 +2038,6 @@ fn cube_face_axes(n: Vec3) -> (Vec3, Vec3) {
} }
} }
/// The patches (zones) of face `n` that belong to region `d` (a face, edge or corner direction
/// with components −1, 0 or 1): the zones a click on would turn to `d`.
pub(crate) fn cube_patches_on(n: Vec3, d: Vec3) -> Vec<(i32, i32)> {
let (e1, e2) = cube_face_axes(n);
let mut out = Vec::new();
for zi in -1..=1 {
for zj in -1..=1 {
if (n + e1 * f64::from(zi) + e2 * f64::from(zj)).dist(d) < 1e-9 {
out.push((zi, zj));
}
}
}
out
}
/// Width of the edge/corner band of a view cube face, as a fraction of the face.
const CUBE_BAND: f64 = 0.2;
/// Which of the 3×3 zones of a projected cube face (corners in order −−, +−, ++, −+) the point
/// is over: (−1|0|1, −1|0|1).
pub(crate) fn cube_zone(pts: &[Pos2], p: Pos2) -> Option<(i32, i32)> {
let [c0, c1, _, c3] = pts else { return None };
let (ux, uy) = ((c1.x - c0.x) as f64, (c1.y - c0.y) as f64);
let (vx, vy) = ((c3.x - c0.x) as f64, (c3.y - c0.y) as f64);
let det = ux * vy - uy * vx;
if det.abs() < 1e-9 {
return None;
}
let (px, py) = ((p.x - c0.x) as f64, (p.y - c0.y) as f64);
let a = (px * vy - py * vx) / det;
let b = (ux * py - uy * px) / det;
if !(-1e-6..=1.0 + 1e-6).contains(&a) || !(-1e-6..=1.0 + 1e-6).contains(&b) {
return None;
}
let k = |t: f64| {
if t < CUBE_BAND {
-1
} else if t > 1.0 - CUBE_BAND {
1
} else {
0
}
};
Some((k(a), k(b)))
}
fn point_in_poly(p: Pos2, poly: &[Pos2]) -> bool { fn point_in_poly(p: Pos2, poly: &[Pos2]) -> bool {
let n = poly.len(); let n = poly.len();
let mut inside = false; let mut inside = false;
@@ -2152,32 +2172,50 @@ fn view_menu(app: &mut SolveApp, ui: &mut egui::Ui, button: Rect) -> bool {
mod tests { mod tests {
use super::*; use super::*;
/// A camera basis looking from `dir` (target → eye), as the view cube draws with.
fn basis_from(dir: Vec3) -> (Vec3, Vec3, Vec3) {
let cam = solvecraft_engine::render::Camera::default().looking_from(dir);
cam.basis()
}
#[test] #[test]
fn view_cube_regions_light_their_patches() { fn view_cube_has_14_targets_each_hit_where_drawn() {
let faces = [Vec3::X, -Vec3::X, Vec3::Y, -Vec3::Y, Vec3::Z, -Vec3::Z]; let targets = cube_targets();
let mut regions = 0; assert_eq!(targets.len(), 14);
for x in -1i32..=1 { assert_eq!(targets.iter().filter(|t| t.corner).count(), 8);
for y in -1i32..=1 { let c = pos2(100.0, 100.0);
for z in -1i32..=1 { for (i, tg) in targets.iter().enumerate() {
let d = Vec3::new(f64::from(x), f64::from(y), f64::from(z)); // Seen from a little off its own direction, its middle hits it.
let k = x.abs() + y.abs() + z.abs(); let shapes = cube_projected(&targets, basis_from(tg.dir + Vec3::new(0.11, 0.07, 0.05)), c);
if k == 0 { let pts = &shapes.iter().find(|(j, _)| *j == i).expect("visible").1;
continue; assert_eq!(cube_hit(&shapes, cube_centroid(pts)), Some(i), "{}", tg.name);
} }
regions += 1; // From an iso view: 3 faces and the 4 facets turned toward it.
let lit: Vec<(Vec3, (i32, i32))> = faces.iter().flat_map(|n| cube_patches_on(*n, d).into_iter().map(move |z| (*n, z))).collect(); let shapes = cube_projected(&targets, basis_from(Vec3::new(1.0, -1.0, 1.0)), c);
// A face lights 1 patch, an edge 2, a corner 3, each on a different face. assert_eq!(shapes.len(), 3 + 4);
assert_eq!(lit.len() as i32, k, "{d:?}: {lit:?}"); assert_eq!(cube_hit(&shapes, pos2(400.0, 400.0)), None);
// Each lit patch is one a click turns to this same region. }
for (n, (zi, zj)) in lit {
let (e1, e2) = cube_face_axes(n); #[test]
assert!((n + e1 * f64::from(zi) + e2 * f64::from(zj)).dist(d) < 1e-12); fn view_cube_edges_are_not_targets() {
assert!(n.dot(d) > 0.5, "a patch on a face away from {d:?}"); let targets = cube_targets();
} let c = pos2(100.0, 100.0);
} let shapes = cube_projected(&targets, basis_from(Vec3::new(1.0, -1.2, 0.9)), c);
} let (r, u, _) = basis_from(Vec3::new(1.0, -1.2, 0.9));
let to2 = |p: Vec3| pos2(c.x + (p.dot(r) * CUBE_SCALE) as f32, c.y - (p.dot(u) * CUBE_SCALE) as f32);
let name = |i: Option<usize>| i.map(|i| targets[i].name.clone()).unwrap_or_default();
// On the top-front edge, its middle and near its ends: a neighbouring face (or facet).
for x in [-0.5, 0.0, 0.5] {
let hit = name(cube_hit(&shapes, to2(Vec3::new(x, -1.0, 1.0))));
assert!(hit == "top" || hit == "front", "edge point {x}: {hit}");
}
let end = name(cube_hit(&shapes, to2(Vec3::new(0.9, -1.0, 1.0))));
assert!(["top", "front", "top-front-right"].contains(&end.as_str()), "{end}");
// Every direction a click turns to is a face's or a corner's: none along an edge.
for tg in &targets {
let zeros = [tg.dir.x, tg.dir.y, tg.dir.z].iter().filter(|v| v.abs() < 1e-9).count();
assert!(zeros == 2 || zeros == 0, "{}: {:?}", tg.name, tg.dir);
} }
assert_eq!(regions, 26);
} }
#[test] #[test]
@@ -2203,15 +2241,4 @@ mod tests {
app.ui.hidden_origin.push(name); app.ui.hidden_origin.push(name);
assert!(construction_axis_segments(&app).is_empty()); assert!(construction_axis_segments(&app).is_empty());
} }
#[test]
fn view_cube_zones() {
// A square face 100 px wide, corners in order −−, +−, ++, −+ (screen y down).
let pts = [pos2(0.0, 100.0), pos2(100.0, 100.0), pos2(100.0, 0.0), pos2(0.0, 0.0)];
assert_eq!(cube_zone(&pts, pos2(50.0, 50.0)), Some((0, 0)));
assert_eq!(cube_zone(&pts, pos2(5.0, 95.0)), Some((-1, -1)));
assert_eq!(cube_zone(&pts, pos2(95.0, 50.0)), Some((1, 0)));
assert_eq!(cube_zone(&pts, pos2(50.0, 5.0)), Some((0, 1)));
assert_eq!(cube_zone(&pts, pos2(150.0, 50.0)), None);
}
} }
@@ -1,110 +1,22 @@
[ [
{ {"note": "The view cube is a cube with chamfered corners. Its targets are the 6 faces and the 8 corner facets; hover lights exactly the one under the pointer (edges are not targets: a point on an edge goes to a neighbour), and a click turns the view to it."},
"note": "View cube hover from the home view: a face lights one patch, an edge the 2 patches on its faces, a corner the 3 around it (Fusion)." {"call": "ui.view", "params": {"view": "home"}},
}, {"move": {"cube": [0, 0, 1]}},
{ {"expect": {"count": {"name": "cube_highlights", "value": 1}, "shown_handles": ["cube_hover:top"]}},
"call": "ui.view", {"move": {"cube": [0.3, -1, 0.2]}},
"params": { {"expect": {"count": {"name": "cube_highlights", "value": 1}, "shown_handles": ["cube_hover:front"]}},
"view": "home" {"note": "On the top-front edge: one target, the top or the front face."},
} {"move": {"cube": [0, -1, 1]}},
}, {"expect": {"count": {"name": "cube_highlights", "value": 1}}},
{ {"move": {"cube": [0.873, -0.873, 0.873]}},
"move": { {"expect": {"count": {"name": "cube_highlights", "value": 1}, "shown_handles": ["cube_hover:top-front-right"]}},
"cube": [ {"click": {"handle": "cube:top-front-right"}},
0, {"expect": {"camera": {"yaw_deg": -45, "pitch_deg": 35.26}}},
0, {"click": {"handle": "cube:front"}},
1 {"expect": {"camera": {"yaw_deg": 0, "pitch_deg": 0}}},
] {"call": "ui.view", "params": {"view": "home"}},
} {"click": {"handle": "cube:top-back-right"}},
}, {"expect": {"camera": {"yaw_deg": -135, "pitch_deg": 35.26}}},
{ {"move": [800, 500]},
"expect": { {"expect": {"count": {"name": "cube_highlights", "value": 0}}}
"count": {
"name": "cube_highlights",
"value": 1
}
}
},
{
"move": {
"cube": [
0,
-0.9,
1
]
}
},
{
"expect": {
"count": {
"name": "cube_highlights",
"value": 2
}
}
},
{
"move": {
"cube": [
1,
-0.9,
0
]
}
},
{
"expect": {
"count": {
"name": "cube_highlights",
"value": 2
}
}
},
{
"move": {
"cube": [
0.9,
-0.9,
1
]
}
},
{
"expect": {
"count": {
"name": "cube_highlights",
"value": 3
}
}
},
{
"move": {
"cube": [
0.9,
-1,
0.9
]
}
},
{
"expect": {
"count": {
"name": "cube_highlights",
"value": 3
}
}
},
{
"move": [
800,
500
]
},
{
"expect": {
"count": {
"name": "cube_highlights",
"value": 0
}
}
}
] ]
+2 -1
View File
@@ -15,7 +15,8 @@
- **Viewport.** Click to select, Shift or Ctrl to add, drag for a window or crossing box. The - **Viewport.** Click to select, Shift or Ctrl to add, drag for a window or crossing box. The
wheel zooms at the cursor, the middle button pans and Shift+middle orbits (the mouse preset in wheel zooms at the cursor, the middle button pans and Shift+middle orbits (the mouse preset in
Preferences switches to SolidWorks or Inventor style). The view cube and the navigation bar Preferences switches to SolidWorks or Inventor style). The view cube and the navigation bar
change the view. Right-click for the marking menu: eight commands around the cursor, plus a change the view: a cube face turns to its standard view, a chamfered corner to that corner's
iso view, and the house goes home. Right-click for the marking menu: eight commands around the cursor, plus a
menu for whatever is under it. menu for whatever is under it.
- **Dialogs** dock on the right with a live preview. Most features also show drag handles and a - **Dialogs** dock on the right with a live preview. Most features also show drag handles and a
value box in the viewport. value box in the viewport.