Merge pull request #125 from CypherNaught-0x/fix/model-render-section
Model renders show an active section with capped cut faces
This commit is contained in:
7 files changed
+217
-46
No files matched your search
@@ -15,7 +15,8 @@
|
||||
//! - `ui.click {x, y, button?, shift?}`, `ui.move {x, y}`, `ui.scroll {x, y, delta}`: real
|
||||
//! pointer input in screen points; `ui.key {key, cmd?, shift?}`, `ui.text {text}`
|
||||
//! - `ui.screenshot {path?}`: PNG of the window; `ui.render {path, width?, height?}`: headless
|
||||
//! CPU render of the model with the current camera (no window needed)
|
||||
//! CPU render of the model with the current camera and the section the viewport shows (no
|
||||
//! window needed)
|
||||
//! - `ui.menu {x?, y?, target?, close?}`: open a context menu at a point (the viewport's marking
|
||||
//! menu, or a browser menu with `target: {type: body|sketch|component, …}`); without a point,
|
||||
//! the open menu and its items. `ui.menuPick {item}` runs an item by id or label;
|
||||
@@ -523,7 +524,9 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
|
||||
"ui.render" => {
|
||||
let w = f("width").unwrap_or(1280.0).clamp(16.0, 8192.0) as usize;
|
||||
let h = f("height").unwrap_or(800.0).clamp(16.0, 8192.0) as usize;
|
||||
let scene = solvecraft_engine::view::scene(&app.session, &app.cam);
|
||||
// Cut by the section the viewport shows (an open Section Analysis dialog's preview,
|
||||
// a sketch's Slice), not only the committed one.
|
||||
let scene = solvecraft_engine::view::scene_with_section(&app.session, &app.cam, crate::viewport::section_plane(app));
|
||||
let Some(png) = render_png(&scene, &app.cam, w, h) else { return err("render failed") };
|
||||
match s("path") {
|
||||
Some(path) => match std::fs::write(path, &png) {
|
||||
@@ -563,3 +566,45 @@ pub fn save_screenshot(image: &egui::ColorImage, path: Option<&str>) -> Value {
|
||||
Err(e) => json!({"ok": false, "error": e.to_string()}),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
use solvecraft_engine::{Sel, Session};
|
||||
|
||||
use super::*;
|
||||
use crate::Services;
|
||||
|
||||
fn render(app: &mut SolveApp, path: &std::path::Path) -> Vec<u8> {
|
||||
let (req, _rx) = ControlRequest::new("ui.render", json!({"path": path.to_string_lossy(), "width": 200, "height": 140}));
|
||||
match handle(app, &egui::Context::default(), &req) {
|
||||
Outcome::Done(v) => assert_eq!(v["ok"], true, "{v}"),
|
||||
Outcome::Screenshot { .. } => panic!("ui.render took a screenshot"),
|
||||
}
|
||||
std::fs::read(path).unwrap()
|
||||
}
|
||||
|
||||
/// A model render shows the section the window shows: the open Section Analysis dialog's
|
||||
/// preview too, not only a committed `inspect.section`.
|
||||
#[test]
|
||||
fn model_render_shows_the_section_dialog_preview() {
|
||||
let mut s = Session::default();
|
||||
s.execute("solid.box", &json!({"length": 40, "width": 30, "height": 20})).unwrap();
|
||||
let mut app = SolveApp::new(s, Services::default());
|
||||
let dir = std::env::temp_dir().join(format!("solvecraft-render-section-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let whole = render(&mut app, &dir.join("whole.png"));
|
||||
app.session.selection = vec![Sel::Plane { name: "XZ".into() }];
|
||||
app.start("inspect.section");
|
||||
let Some(crate::dialogs::Kind::Section { offset, .. }) = app.dialog.as_mut().map(|d| &mut d.kind) else { panic!("no section dialog") };
|
||||
*offset = "15 mm".into();
|
||||
assert!(app.session.section.is_none() && crate::viewport::section_plane(&app).is_some());
|
||||
let preview = render(&mut app, &dir.join("preview.png"));
|
||||
assert!(whole != preview, "the model render ignores the section the window shows");
|
||||
// Committed, it renders the same.
|
||||
app.run("inspect.section", json!({"plane": "XZ", "offset": 15})).unwrap();
|
||||
app.dialog = None;
|
||||
assert!(render(&mut app, &dir.join("committed.png")) == preview, "the committed section renders differently");
|
||||
let _ = std::fs::remove_dir_all(&dir);
|
||||
}
|
||||
}
|
||||
@@ -342,7 +342,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
if let Some((o, n)) = section_plane(app) {
|
||||
let col = c4(tk.text.gamma_multiply(0.9));
|
||||
for b in st.bodies.iter().filter(|b| app.session.body_visible(&b.name)) {
|
||||
for (a, c) in section_segments(&b.mesh(), o, n) {
|
||||
for (a, c) in solvecraft_engine::view::section_segments(&b.mesh(), o, n) {
|
||||
sc.line(a.to_f32(), c.to_f32(), col, 1.8, false);
|
||||
}
|
||||
}
|
||||
@@ -437,28 +437,6 @@ fn ground_shadow(sc: &mut GpuScene, app: &SolveApp, st: &solvecraft_engine::doc:
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the plane through `o` with normal `n` cuts a mesh: one segment per crossed triangle,
|
||||
/// nudged a hair to the kept side (so the clip leaves them drawn).
|
||||
pub(crate) fn section_segments(m: &solvecraft_engine::geom::Mesh, o: Vec3, n: Vec3) -> Vec<(Vec3, Vec3)> {
|
||||
let eps = 1e-4 * (1.0 + m.bounds().diagonal());
|
||||
let mut out = Vec::new();
|
||||
for t in &m.triangles {
|
||||
let Some(p) = m.tri(t) else { continue };
|
||||
let d = p.map(|q| n.dot(q - o));
|
||||
let mut pts = Vec::with_capacity(2);
|
||||
for (i, j) in [(0, 1), (1, 2), (2, 0)] {
|
||||
if (d[i] < 0.0) != (d[j] < 0.0) {
|
||||
let s = d[i] / (d[i] - d[j]);
|
||||
pts.push(p[i] + (p[j] - p[i]) * s - n * eps);
|
||||
}
|
||||
}
|
||||
if let [a, b] = pts[..] {
|
||||
out.push((a, b));
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// The drawn pieces of a polyline in a dash pattern (on, off, on, off… lengths).
|
||||
pub(crate) fn dashes(pts: &[Vec3], pattern: &[f64]) -> Vec<(Vec3, Vec3)> {
|
||||
let mut out = Vec::new();
|
||||
@@ -3016,7 +2994,7 @@ mod tests {
|
||||
let st = s.model.state();
|
||||
let m = st.bodies[0].mesh();
|
||||
let (o, n) = (Vec3::new(0.0, 15.0, 0.0), Vec3::new(0.0, -1.0, 0.0));
|
||||
let segs = section_segments(&m, o, n);
|
||||
let segs = solvecraft_engine::view::section_segments(&m, o, n);
|
||||
assert!(segs.len() >= 4, "{segs:?}");
|
||||
// On the plane (a hair to the kept side) and around the 40 x 20 cut.
|
||||
let mut len = 0.0;
|
||||
@@ -3026,7 +3004,7 @@ mod tests {
|
||||
}
|
||||
assert!((len - 120.0).abs() < 0.5, "outline length {len}");
|
||||
// A plane that misses the body cuts nothing.
|
||||
assert!(section_segments(&m, Vec3::new(0.0, 99.0, 0.0), n).is_empty());
|
||||
assert!(solvecraft_engine::view::section_segments(&m, Vec3::new(0.0, 99.0, 0.0), n).is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in new issue
Block a user