snapshot --camera and saved views; grid: false / axes: false for headless renders (#1313)
solidcraft-cli snapshot takes --camera JSON and --view with a saved view name (through view.apply, as the MCP screenshot does), plus --no-grid and --no-axes; the MCP screenshot and ui.render take grid/axes, the headless render skips a hidden origin axis, and the app's ui.render follows the Grid and Origin switches. Closes #1245.
This commit is contained in:
1 parent
d824d36821
commit
b3df2c382b
10 files changed
+569
-54
No files matched your search
+124
-24
@@ -287,7 +287,29 @@ pub fn scene(s: &Session, cam: &Camera) -> Scene {
|
||||
/// the app passes the cut its viewport shows (an open Section Analysis dialog's preview, a
|
||||
/// sketch's Slice), so model renders match the window.
|
||||
pub fn scene_with_section(s: &Session, cam: &Camera, section: Option<(Vec3, Vec3)>) -> Scene {
|
||||
scene_with_section_and_sketch_sampler(s, cam, section, world_curve_parts)
|
||||
scene_with_guides(s, cam, section, Guides::default())
|
||||
}
|
||||
|
||||
/// What a headless render draws besides the model: the ground grid and the origin's axes.
|
||||
/// Both are on by default; the grid lies on Z 0, so it cuts through a model that straddles it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct Guides {
|
||||
/// The ground grid.
|
||||
pub grid: bool,
|
||||
/// The origin's axes. An axis hidden with `browser.visibility` (`X`, `Y`, `Z`, or the whole
|
||||
/// `origin` folder) stays hidden whatever this says, as in the viewport.
|
||||
pub axes: bool,
|
||||
}
|
||||
|
||||
impl Default for Guides {
|
||||
fn default() -> Self {
|
||||
Guides { grid: true, axes: true }
|
||||
}
|
||||
}
|
||||
|
||||
/// [`scene_with_section`] with the grid and the axes shown or left out.
|
||||
pub fn scene_with_guides(s: &Session, cam: &Camera, section: Option<(Vec3, Vec3)>, guides: Guides) -> Scene {
|
||||
scene_with_palette_and_guides(s, cam, section, world_curve_parts, &Palette::DEFAULT, guides)
|
||||
}
|
||||
|
||||
/// A headless scene using supplied display paths for its visible sketch curves.
|
||||
@@ -312,6 +334,29 @@ pub fn scene_with_palette(
|
||||
s: &Session,
|
||||
cam: &Camera,
|
||||
section: Option<(Vec3, Vec3)>,
|
||||
sampler: impl FnMut(&Sketch, usize, &Plane) -> Vec<Vec<Vec3>>,
|
||||
p: &Palette,
|
||||
) -> Scene {
|
||||
build_scene(s, cam, section, Guides::default(), sampler, p)
|
||||
}
|
||||
|
||||
/// [`scene_with_palette`] with the ground grid and the origin's axes shown or left out.
|
||||
pub fn scene_with_palette_and_guides(
|
||||
s: &Session,
|
||||
cam: &Camera,
|
||||
section: Option<(Vec3, Vec3)>,
|
||||
sampler: impl FnMut(&Sketch, usize, &Plane) -> Vec<Vec<Vec3>>,
|
||||
p: &Palette,
|
||||
guides: Guides,
|
||||
) -> Scene {
|
||||
build_scene(s, cam, section, guides, sampler, p)
|
||||
}
|
||||
|
||||
fn build_scene(
|
||||
s: &Session,
|
||||
cam: &Camera,
|
||||
section: Option<(Vec3, Vec3)>,
|
||||
guides: Guides,
|
||||
mut sampler: impl FnMut(&Sketch, usize, &Plane) -> Vec<Vec<Vec3>>,
|
||||
p: &Palette,
|
||||
) -> Scene {
|
||||
@@ -321,31 +366,36 @@ pub fn scene_with_palette(
|
||||
let b = bounds(s);
|
||||
sc.radius = (b.diagonal() * 0.5).max(cam.half_height()).max(10.0) + b.center().dist(cam.target);
|
||||
// Grid on the ground plane (XY with Z up, XZ with Y up).
|
||||
let (minor, major) = grid_step(cam.half_height());
|
||||
let ext = (cam.half_height() * 3.0 / major).ceil() * major;
|
||||
let tf = cam.to_frame(cam.target);
|
||||
let c = Vec3::new((tf.x / major).round() * major, (tf.y / major).round() * major, 0.0);
|
||||
let w = |p: Vec3| cam.from_frame(p);
|
||||
let n = ((ext / minor) as i64).clamp(1, 400);
|
||||
for i in -n..=n {
|
||||
let t = i as f64 * minor;
|
||||
let col = if (t / major).fract().abs() < 1e-9 { p.grid_major } else { p.grid };
|
||||
sc.lines.push(SceneLine {
|
||||
points: vec![w(c + Vec3::new(t, -ext, 0.0)), w(c + Vec3::new(t, ext, 0.0))],
|
||||
color: col,
|
||||
width: 0.6,
|
||||
on_top: false,
|
||||
});
|
||||
sc.lines.push(SceneLine {
|
||||
points: vec![w(c + Vec3::new(-ext, t, 0.0)), w(c + Vec3::new(ext, t, 0.0))],
|
||||
color: col,
|
||||
width: 0.6,
|
||||
on_top: false,
|
||||
});
|
||||
if guides.grid {
|
||||
let (minor, major) = grid_step(cam.half_height());
|
||||
let ext = (cam.half_height() * 3.0 / major).ceil() * major;
|
||||
let tf = cam.to_frame(cam.target);
|
||||
let c = Vec3::new((tf.x / major).round() * major, (tf.y / major).round() * major, 0.0);
|
||||
let w = |p: Vec3| cam.from_frame(p);
|
||||
let n = ((ext / minor) as i64).clamp(1, 400);
|
||||
for i in -n..=n {
|
||||
let t = i as f64 * minor;
|
||||
let col = if (t / major).fract().abs() < 1e-9 { p.grid_major } else { p.grid };
|
||||
sc.lines.push(SceneLine {
|
||||
points: vec![w(c + Vec3::new(t, -ext, 0.0)), w(c + Vec3::new(t, ext, 0.0))],
|
||||
color: col,
|
||||
width: 0.6,
|
||||
on_top: false,
|
||||
});
|
||||
sc.lines.push(SceneLine {
|
||||
points: vec![w(c + Vec3::new(-ext, t, 0.0)), w(c + Vec3::new(ext, t, 0.0))],
|
||||
color: col,
|
||||
width: 0.6,
|
||||
on_top: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
// The origin's axes, each unless hidden one by one (`browser.visibility`), as in the viewport.
|
||||
let axis = cam.half_height() * 0.25;
|
||||
for (d, col) in [Vec3::X, Vec3::Y, Vec3::Z].into_iter().zip(p.axes) {
|
||||
sc.lines.push(SceneLine { points: vec![Vec3::ZERO, d * axis], color: col, width: 1.6, on_top: false });
|
||||
for ((name, d), col) in [("X", Vec3::X), ("Y", Vec3::Y), ("Z", Vec3::Z)].into_iter().zip(p.axes) {
|
||||
if guides.axes && !s.visibility.hidden_origin.iter().any(|h| h == name) {
|
||||
sc.lines.push(SceneLine { points: vec![Vec3::ZERO, d * axis], color: col, width: 1.6, on_top: false });
|
||||
}
|
||||
}
|
||||
let selected_occurrences: Vec<String> = s
|
||||
.selection
|
||||
@@ -551,6 +601,56 @@ mod tests {
|
||||
assert!(img.rgba.chunks(4).any(|p| p[1] > 120 && p[0] < 90 && p[2] < 110), "no green pixels");
|
||||
}
|
||||
|
||||
/// A render can leave out the ground grid and the origin's axes (a box straddling Z 0 is cut
|
||||
/// through by the grid), each on its own; the body's own lines stay.
|
||||
#[test]
|
||||
fn guides_leave_the_grid_and_the_axes_out() {
|
||||
let mut s = Session::default();
|
||||
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20, "corner": [10, -15, -10]})).unwrap();
|
||||
let cam = home_camera(&s);
|
||||
let grid = [colors::GRID, colors::GRID_MAJOR];
|
||||
let axes = [colors::AXIS_X, colors::AXIS_Y, colors::AXIS_Z];
|
||||
let count = |sc: &Scene, colours: &[Rgb]| sc.lines.iter().filter(|l| colours.contains(&l.color)).count();
|
||||
let all = scene(&s, &cam);
|
||||
let (grid_lines, edges) = (count(&all, &grid), count(&all, &[colors::EDGE]));
|
||||
assert!(grid_lines > 20 && edges >= 12, "{grid_lines} grid lines, {edges} edges");
|
||||
assert_eq!(count(&all, &axes), 3);
|
||||
assert_eq!(all.lines.len(), scene_with_guides(&s, &cam, s.section, Guides::default()).lines.len());
|
||||
let render = |g: Guides| scene_with_guides(&s, &cam, s.section, g);
|
||||
let no_grid = render(Guides { grid: false, axes: true });
|
||||
assert_eq!((count(&no_grid, &grid), count(&no_grid, &axes), count(&no_grid, &[colors::EDGE])), (0, 3, edges));
|
||||
let no_axes = render(Guides { grid: true, axes: false });
|
||||
assert_eq!((count(&no_axes, &grid), count(&no_axes, &axes), count(&no_axes, &[colors::EDGE])), (grid_lines, 0, edges));
|
||||
let bare = render(Guides { grid: false, axes: false });
|
||||
assert_eq!(bare.lines.len(), edges);
|
||||
// The picture changes with each.
|
||||
let png = |sc: &Scene| solidcraft_render::render_png(sc, &cam, 160, 100);
|
||||
assert!(png(&all) != png(&no_grid) && png(&all) != png(&no_axes) && png(&no_grid) != png(&bare));
|
||||
}
|
||||
|
||||
/// An origin axis hidden with `browser.visibility` stays out of headless renders, as it does
|
||||
/// out of the viewport, whatever the render's own switch says; the grid is not the origin.
|
||||
#[test]
|
||||
fn hidden_origin_axes_stay_out_of_renders() {
|
||||
let mut s = Session::default();
|
||||
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
|
||||
let cam = home_camera(&s);
|
||||
let axes = [colors::AXIS_X, colors::AXIS_Y, colors::AXIS_Z];
|
||||
let shown = |s: &Session| -> Vec<Rgb> {
|
||||
let sc = scene(s, &cam);
|
||||
axes.into_iter().filter(|c| sc.lines.iter().any(|l| l.color == *c)).collect()
|
||||
};
|
||||
assert_eq!(shown(&s), axes);
|
||||
s.execute("browser.visibility", &json!({"items": ["Y"], "visible": false})).unwrap();
|
||||
assert_eq!(shown(&s), [colors::AXIS_X, colors::AXIS_Z]);
|
||||
s.execute("browser.visibility", &json!({"folder": "origin", "visible": false})).unwrap();
|
||||
assert!(shown(&s).is_empty());
|
||||
let sc = scene_with_guides(&s, &cam, None, Guides { grid: true, axes: true });
|
||||
assert!(sc.lines.iter().any(|l| l.color == colors::GRID), "the grid is not part of the origin");
|
||||
s.execute("browser.visibility", &json!({"folder": "origin", "visible": true})).unwrap();
|
||||
assert_eq!(shown(&s), axes);
|
||||
}
|
||||
|
||||
/// Curved surfaces get a dark silhouette line in snapshots where they turn away.
|
||||
#[test]
|
||||
fn snapshots_draw_silhouettes_on_curved_surfaces() {
|
||||
|
||||
Reference in new issue
Block a user