Compare commits
2
Commits
main
...
release-0.10.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
069ba1223f | ||
|
|
4c09fa5355 |
No files matched your search
Generated
+14
-14
@@ -3281,7 +3281,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -3299,7 +3299,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-cli"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
@@ -3308,7 +3308,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-doc"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -3323,7 +3323,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-engine"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"serde",
|
||||
@@ -3339,7 +3339,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-geom"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3348,7 +3348,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-gpu"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"pollster 0.4.0",
|
||||
@@ -3360,7 +3360,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-io"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"quick-xml",
|
||||
"serde",
|
||||
@@ -3375,7 +3375,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-kernel"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"log",
|
||||
"rayon",
|
||||
@@ -3393,7 +3393,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-mcp"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"solidcraft-engine",
|
||||
@@ -3401,7 +3401,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-render"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"image",
|
||||
"serde",
|
||||
@@ -3411,7 +3411,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-sketch"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -3422,7 +3422,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-ui-egui"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"egui",
|
||||
"egui-wgpu",
|
||||
@@ -3439,7 +3439,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "solidcraft-web"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"eframe",
|
||||
"egui",
|
||||
@@ -4862,7 +4862,7 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
|
||||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"sha2",
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
|
||||
exclude = ["vendor"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.10.0"
|
||||
version = "0.10.1"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.95"
|
||||
|
||||
@@ -31,6 +31,9 @@ pub struct ModelBody {
|
||||
/// The display plan (none when the body has no plan: a mesh body, or one the planner
|
||||
/// refuses), shared like the mesh. A GPU viewport draws it.
|
||||
plan: Arc<OnceLock<Option<Arc<TessPlan>>>>,
|
||||
/// The plan evaluated on the CPU (none without a plan), shared like the mesh: the triangles a
|
||||
/// GPU viewport draws, so a highlight over a face lies exactly on it.
|
||||
plan_mesh: Arc<OnceLock<Option<Arc<Mesh>>>>,
|
||||
/// Identity of this body's content for the evaluation cache: the key of the evaluation
|
||||
/// that produced it (kept while later features pass it through unchanged).
|
||||
key: u64,
|
||||
@@ -59,6 +62,7 @@ impl ModelBody {
|
||||
feature,
|
||||
mesh: Arc::new(OnceLock::new()),
|
||||
plan: Arc::new(OnceLock::new()),
|
||||
plan_mesh: Arc::new(OnceLock::new()),
|
||||
key: 0,
|
||||
names: Default::default(),
|
||||
made: Default::default(),
|
||||
@@ -98,6 +102,14 @@ impl ModelBody {
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// The display plan's mesh, evaluated on the CPU once: the same triangles a GPU viewport
|
||||
/// evaluates and draws for this body. Highlights over a face drawn from the plan use it, since
|
||||
/// the reference mesh ([`ModelBody::mesh`]) triangulates the face differently and the two
|
||||
/// would fight at equal depth. `None` when the body has no plan.
|
||||
pub fn plan_mesh(&self) -> Option<Arc<Mesh>> {
|
||||
self.plan_mesh.get_or_init(|| self.display_plan().and_then(|plan| self.body.display_mesh_of(&plan).ok()).map(Arc::new)).clone()
|
||||
}
|
||||
|
||||
/// The display mesh's chord tolerance: 1/1000 of the body's size.
|
||||
fn display_tol(&self) -> f64 {
|
||||
(self.body.size() * 1e-3).max(1e-3)
|
||||
|
||||
@@ -642,7 +642,7 @@ impl Resources {
|
||||
access_bgl,
|
||||
access_bind,
|
||||
access_version: 0,
|
||||
tri_hl: pipeline("sc_tris_hl", "vs_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
|
||||
tri_hl: pipeline("sc_tris_hl", "vs_tri", "fs_tri", tri_layout(), highlight_depth(t.depth), alpha),
|
||||
trans: pipeline("sc_trans", "vs_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::LessEqual, false), alpha),
|
||||
glass_far: glass("sc_glass_far", wgpu::Face::Front),
|
||||
glass_near: glass("sc_glass_near", wgpu::Face::Back),
|
||||
@@ -662,6 +662,24 @@ impl Resources {
|
||||
}
|
||||
}
|
||||
|
||||
/// Depth state of faces lit over the model (hover, selection): tested, not written, and pulled a
|
||||
/// little toward the eye. They lie on the model's own faces, but the GPU evaluates a body's plan
|
||||
/// in f32 while the highlight's copy of the same triangles is evaluated on the CPU, so the two
|
||||
/// depths differ by rounding and an unbiased equal test lights the face in speckles and wedges.
|
||||
/// The bias is a few depth steps plus the face's own slope: far less than any real gap.
|
||||
fn highlight_depth(format: Option<wgpu::TextureFormat>) -> Option<wgpu::DepthStencilState> {
|
||||
format.map(|format| wgpu::DepthStencilState {
|
||||
format,
|
||||
depth_write_enabled: Some(false),
|
||||
depth_compare: Some(wgpu::CompareFunction::LessEqual),
|
||||
stencil: Default::default(),
|
||||
bias: HIGHLIGHT_BIAS,
|
||||
})
|
||||
}
|
||||
|
||||
/// See [`highlight_depth`]. Negative: toward the eye (depth grows away from it).
|
||||
const HIGHLIGHT_BIAS: wgpu::DepthBiasState = wgpu::DepthBiasState { constant: -8, slope_scale: -1.0, clamp: 0.0 };
|
||||
|
||||
fn upload(device: &wgpu::Device, label: &str, bytes: &[u8], stride: usize) -> Option<Batch> {
|
||||
upload_with(device, label, bytes, stride, wgpu::BufferUsages::VERTEX)
|
||||
}
|
||||
@@ -895,8 +913,17 @@ impl egui_wgpu::CallbackTrait for ViewportCallback {
|
||||
/// Render `scene` offscreen on a device of its own, through the viewport's real resources and
|
||||
/// frame callback (`prepare`, then `paint`), and read the pixels back: straight RGBA, `size` ×
|
||||
/// `size`. `None` without a GPU adapter. With `tess`, planned bodies are evaluated on the GPU.
|
||||
/// `highlight` is drawn as the viewport's highlight scene (hover and selection).
|
||||
#[cfg(all(test, not(target_arch = "wasm32")))]
|
||||
pub(crate) fn render_offscreen(scene: GpuScene, view_proj: [[f32; 4]; 4], back: [f32; 3], eye: [f32; 4], size: u32, tess: bool) -> Option<Vec<u8>> {
|
||||
pub(crate) fn render_offscreen(
|
||||
scene: GpuScene,
|
||||
highlight: Option<GpuScene>,
|
||||
view_proj: [[f32; 4]; 4],
|
||||
back: [f32; 3],
|
||||
eye: [f32; 4],
|
||||
size: u32,
|
||||
tess: bool,
|
||||
) -> Option<Vec<u8>> {
|
||||
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
|
||||
let adapter = pollster::block_on(instance.request_adapter(&Default::default())).ok()?;
|
||||
let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
|
||||
@@ -915,8 +942,8 @@ pub(crate) fn render_offscreen(scene: GpuScene, view_proj: [[f32; 4]; 4], back:
|
||||
slot: Arc::new(Mutex::new(Some(scene))),
|
||||
sketch_key: 0,
|
||||
sketch_slot: none(),
|
||||
hl_key: 0,
|
||||
hl_slot: none(),
|
||||
hl_key: u64::from(highlight.is_some()),
|
||||
hl_slot: Arc::new(Mutex::new(highlight)),
|
||||
pv_key: 0,
|
||||
pv_slot: none(),
|
||||
view_proj,
|
||||
|
||||
+119
-18
@@ -305,6 +305,51 @@ fn sketch_color(t: &Tokens, c: Rgb) -> [u8; 4] {
|
||||
}
|
||||
}
|
||||
|
||||
/// With a component active, bodies outside it are drawn faded.
|
||||
fn body_faded(app: &SolveApp, b: &solidcraft_engine::doc::ModelBody, active_bodies: &Option<Vec<String>>) -> bool {
|
||||
let s = &app.session;
|
||||
if let Some(names) = active_bodies {
|
||||
!names.contains(&b.name)
|
||||
} else {
|
||||
s.active_component != 0 && !s.doc.component_within(b.component, s.active_component)
|
||||
}
|
||||
}
|
||||
|
||||
/// The display plan the model scene draws `b` from on the GPU, or `None` when its faces are built
|
||||
/// here from the reference mesh. Opaque shaded bodies (`opaque`: not faded, and no see-through
|
||||
/// body or face) are drawn from their plans where the device evaluates them; faded and
|
||||
/// see-through ones (sorted and blended per triangle) are built here.
|
||||
fn drawn_plan(
|
||||
app: &SolveApp,
|
||||
b: &solidcraft_engine::doc::ModelBody,
|
||||
opaque: bool,
|
||||
) -> Option<std::sync::Arc<solidcraft_engine::geom::tess::TessPlan>> {
|
||||
let shaded = app.ui.visual_style != 2;
|
||||
(shaded && opaque && app.viewport.gpu.is_some_and(|g| g.tess))
|
||||
.then(|| b.display_plan())
|
||||
.flatten()
|
||||
.filter(|plan| plan.vertex_count() <= solidcraft_gpu::MAX_BATCH)
|
||||
}
|
||||
|
||||
/// The mesh whose triangles the model scene shows for `b`'s faces: the plan's (evaluated on the
|
||||
/// CPU, the same triangles the GPU draws) or the reference mesh. A face highlighted over the model
|
||||
/// must use the same triangles: another triangulation of the same face fights it at equal depth
|
||||
/// and shimmers.
|
||||
fn shown_mesh(
|
||||
app: &SolveApp,
|
||||
b: &solidcraft_engine::doc::ModelBody,
|
||||
active_bodies: &Option<Vec<String>>,
|
||||
) -> std::sync::Arc<solidcraft_engine::geom::Mesh> {
|
||||
let doc = &app.session.doc;
|
||||
let opaque =
|
||||
|| !body_faded(app, b, active_bodies) && doc.model_body_opacity(b) >= 0.999 && doc.face_colors(b).iter().all(|(_, l)| l.opacity >= 0.999);
|
||||
(app.viewport.gpu.is_some_and(|g| g.tess) && opaque())
|
||||
.then(|| drawn_plan(app, b, true))
|
||||
.flatten()
|
||||
.and_then(|_| b.plan_mesh())
|
||||
.unwrap_or_else(|| b.mesh())
|
||||
}
|
||||
|
||||
fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
let tk = Tokens::get();
|
||||
let mut sc = GpuScene::default();
|
||||
@@ -316,12 +361,7 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
if !app.session.body_visible(&b.name) || app.preview.replaced.contains(&b.name) {
|
||||
continue;
|
||||
}
|
||||
// With a component active, bodies outside it are drawn faded.
|
||||
let faded = if let Some(names) = &active_bodies {
|
||||
!names.contains(&b.name)
|
||||
} else {
|
||||
s.active_component != 0 && !s.doc.component_within(b.component, s.active_component)
|
||||
};
|
||||
let faded = body_faded(app, b, &active_bodies);
|
||||
let base = body_color(&s.doc, b).unwrap_or(tk.body);
|
||||
let col = if faded { c4(base.gamma_multiply(0.35)) } else { c4(base) };
|
||||
let m = b.mesh();
|
||||
@@ -338,13 +378,8 @@ fn build_scene(app: &SolveApp) -> GpuScene {
|
||||
let opacity = s.doc.model_body_opacity(b);
|
||||
let face_looks: std::collections::HashMap<usize, solidcraft_engine::doc::appearance::Look> =
|
||||
if shaded { s.doc.face_colors(b).into_iter().collect() } else { Default::default() };
|
||||
// Opaque bodies are drawn from their display plans on the GPU where the device evaluates
|
||||
// them; faded and see-through ones (sorted and blended per triangle) are built here.
|
||||
let opaque = !faded && opacity >= 0.999 && face_looks.values().all(|l| l.opacity >= 0.999);
|
||||
let planned = (shaded && opaque && app.viewport.gpu.is_some_and(|g| g.tess))
|
||||
.then(|| b.display_plan())
|
||||
.flatten()
|
||||
.filter(|plan| plan.vertex_count() <= solidcraft_gpu::MAX_BATCH);
|
||||
let planned = drawn_plan(app, b, opaque);
|
||||
let on_gpu = planned.is_some();
|
||||
if let Some(plan) = planned {
|
||||
let face_colors = (0..b.body.face_count())
|
||||
@@ -1400,6 +1435,10 @@ fn highlight_key(app: &SolveApp) -> u64 {
|
||||
format!("{:?}", app.viewport.hover).hash(&mut h);
|
||||
app.viewport.hover_feature.hash(&mut h);
|
||||
app.viewport.hover_bodies.hash(&mut h);
|
||||
// Which mesh lights a face follows how the model draws its body (see `shown_mesh`).
|
||||
app.session.active_component.hash(&mut h);
|
||||
app.session.active_occurrence_path.hash(&mut h);
|
||||
app.ui.visual_style.hash(&mut h);
|
||||
app.ui.pick_bodies.hash(&mut h);
|
||||
crate::theme::is_dark().hash(&mut h);
|
||||
app.preview.replaced.hash(&mut h);
|
||||
@@ -1418,6 +1457,10 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
let t = Tokens::get();
|
||||
let mut sc = GpuScene::default();
|
||||
let st = app.session.world_state();
|
||||
let s = &app.session;
|
||||
let active_bodies = s.active_occurrence_path.as_ref().and_then(|p| s.occurrence_bodies(p, true).ok());
|
||||
// Faces lit over the model use the triangles the model shows them with.
|
||||
let shown = |b: &solidcraft_engine::doc::ModelBody| shown_mesh(app, b, &active_bodies);
|
||||
let mut sel = Vec::new();
|
||||
for item in app.highlighted() {
|
||||
for piece in displayed_selection(app, &item) {
|
||||
@@ -1491,7 +1534,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
Sel::Face { body, index, .. } => {
|
||||
// A live preview stands in for the body: its old faces would fight with it.
|
||||
if let Some(b) = st.body(body).filter(|_| app.session.body_visible(body) && !app.preview.replaced.contains(body)) {
|
||||
face_tris(&mut sc, &b.mesh(), *index, c4(t.sel_face), false);
|
||||
face_tris(&mut sc, &shown(b), *index, c4(t.sel_face), false);
|
||||
hidden_face_outline(&mut sc, &b.mesh(), *index, t.sel_edge);
|
||||
}
|
||||
}
|
||||
@@ -1499,7 +1542,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
if let Some(b) = st.body(name)
|
||||
&& app.session.body_visible(name)
|
||||
{
|
||||
let m = b.mesh();
|
||||
let m = shown(b);
|
||||
for f in 0..b.body.face_count() {
|
||||
face_tris(&mut sc, &m, f, c4(t.body_selected), true);
|
||||
}
|
||||
@@ -1573,11 +1616,12 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
let k = t.hover_face_lift;
|
||||
let c = body_rgb(app, &b.name);
|
||||
let m = b.mesh();
|
||||
let lit = shown(b);
|
||||
for &f in &faces {
|
||||
if !sel.iter().any(|x| {
|
||||
matches!(x, Sel::Face { body, index, .. } if body == &b.name && *index == f) || matches!(x, Sel::Body { name } if name == &b.name)
|
||||
}) {
|
||||
face_tris(&mut sc, &m, f, [c.0.saturating_add(k), c.1.saturating_add(k), c.2.saturating_add(k), 255], true);
|
||||
face_tris(&mut sc, &lit, f, [c.0.saturating_add(k), c.1.saturating_add(k), c.2.saturating_add(k), 255], true);
|
||||
}
|
||||
}
|
||||
for (ei, e) in m.edges.iter().enumerate() {
|
||||
@@ -1593,7 +1637,7 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
if let Some(b) = st.body(body) {
|
||||
let k = t.hover_face_lift;
|
||||
let c = body_rgb(app, body);
|
||||
let m = b.mesh();
|
||||
let m = shown(b);
|
||||
for f in 0..b.body.face_count() {
|
||||
face_tris(&mut sc, &m, f, [c.0.saturating_add(k), c.1.saturating_add(k), c.2.saturating_add(k), 255], true);
|
||||
}
|
||||
@@ -1608,8 +1652,9 @@ fn build_highlight(app: &SolveApp) -> GpuScene {
|
||||
let c = body_rgb(app, body);
|
||||
let m = b.mesh();
|
||||
let patch = m.surface_patch(*index);
|
||||
let lit = shown(b);
|
||||
for &f in &patch {
|
||||
face_tris(&mut sc, &m, f, [c.0.saturating_add(k), c.1.saturating_add(k), c.2.saturating_add(k), 255], true);
|
||||
face_tris(&mut sc, &lit, f, [c.0.saturating_add(k), c.1.saturating_add(k), c.2.saturating_add(k), 255], true);
|
||||
}
|
||||
for (e, _) in m.edge_faces.iter().enumerate().filter(|(_, fs)| fs.iter().any(|f| patch.contains(&(*f as usize)))) {
|
||||
if let Some(p) = m.edges.get(e)
|
||||
@@ -3272,7 +3317,7 @@ mod tests {
|
||||
let e = proj.cam.eye().to_f32();
|
||||
let eye = [e[0], e[1], e[2], if proj.cam.fov > 0.0 { 1.0 } else { 0.0 }];
|
||||
let planned = scene.planned.len();
|
||||
(planned, scene.tris.len(), crate::gpu::render_offscreen(scene, proj.vp.to_f32(), proj.cam.back().to_f32(), eye, SIZE, tess))
|
||||
(planned, scene.tris.len(), crate::gpu::render_offscreen(scene, None, proj.vp.to_f32(), proj.cam.back().to_f32(), eye, SIZE, tess))
|
||||
};
|
||||
let (planned, gpu_tris, gpu) = render(true);
|
||||
let (none, cpu_tris, cpu) = render(false);
|
||||
@@ -3290,6 +3335,62 @@ mod tests {
|
||||
assert!(differ * 100 < drawn, "{differ} of {drawn} model pixels differ");
|
||||
}
|
||||
|
||||
/// A hovered face lights solidly over a body drawn from its display plan on the GPU: the
|
||||
/// highlight uses the plan's triangles, not the reference mesh's other triangulation of the
|
||||
/// same face, which fought the model at equal depth and shimmered in wedges. Every face of a
|
||||
/// box with a pocket and a round boss, hovered, changes the same pixels as on the CPU path (where model and
|
||||
/// highlight share the reference mesh). Skipped without a GPU adapter.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
#[test]
|
||||
fn a_hovered_face_lights_whole_over_a_body_drawn_from_its_plan() {
|
||||
let mut s = solidcraft_engine::Session::default();
|
||||
s.execute("solid.box", &json!({"length": 40, "width": 40, "height": 40, "corner": [0, 0, 0], "operation": "new"})).unwrap();
|
||||
s.execute("solid.box", &json!({"length": 16, "width": 16, "height": 20, "corner": [12, 12, 20], "operation": "cut"})).unwrap();
|
||||
s.execute("solid.cylinder", &json!({"base": [33, 33, 40], "diameter": 10, "height": 10, "operation": "join"})).unwrap();
|
||||
let mut app = SolveApp::new(s, crate::Services::default());
|
||||
app.viewport.no_pixels = true;
|
||||
app.fit_view();
|
||||
const SIZE: u32 = 256;
|
||||
let rect = Rect::from_min_size(Pos2::ZERO, vec2(SIZE as f32, SIZE as f32));
|
||||
let body = app.session.world_state().bodies[0].clone();
|
||||
let faces = body.body.face_count();
|
||||
let mut render = |tess: bool, hover: Option<usize>| {
|
||||
app.viewport.gpu = Some(GpuTarget { format: egui_wgpu::wgpu::TextureFormat::Rgba8Unorm, depth: None, samples: 1, tess });
|
||||
app.viewport.hover = hover.map(|index| Hit::Face { body: body.name.clone(), index, point: Vec3::ZERO });
|
||||
let (scene, highlight) = (build_scene(&app), build_highlight(&app));
|
||||
assert_eq!(!scene.planned.is_empty(), tess, "the box is drawn from its plan exactly on the GPU path");
|
||||
let proj = projection(&app, rect);
|
||||
let e = proj.cam.eye().to_f32();
|
||||
let eye = [e[0], e[1], e[2], if proj.cam.fov > 0.0 { 1.0 } else { 0.0 }];
|
||||
crate::gpu::render_offscreen(scene, Some(highlight), proj.vp.to_f32(), proj.cam.back().to_f32(), eye, SIZE, tess)
|
||||
};
|
||||
let lit = |plain: &[u8], hovered: &[u8]| -> Vec<bool> { plain.chunks(4).zip(hovered.chunks(4)).map(|(a, b)| a != b).collect() };
|
||||
let (Some(gpu_plain), Some(cpu_plain)) = (render(true, None), render(false, None)) else {
|
||||
eprintln!("skipping: no GPU adapter");
|
||||
return;
|
||||
};
|
||||
let mut checked = 0;
|
||||
for f in 0..faces {
|
||||
let (Some(gpu), Some(cpu)) = (render(true, Some(f)), render(false, Some(f))) else { return };
|
||||
let (on_gpu, on_cpu) = (lit(&gpu_plain, &gpu), lit(&cpu_plain, &cpu));
|
||||
let area = on_cpu.iter().filter(|x| **x).count();
|
||||
if area < 400 {
|
||||
continue; // facing away or hidden
|
||||
}
|
||||
// Inside the face, two pixels in from its outline: a curved face's outline follows
|
||||
// its own facets, which the plan and the reference mesh cut differently.
|
||||
let n = SIZE as i32;
|
||||
let inside = |i: usize| {
|
||||
let (x, y) = (i as i32 % n, i as i32 / n);
|
||||
(-2..=2).all(|dy| (-2..=2).all(|dx| on_cpu.get(((y + dy).clamp(0, n - 1) * n + (x + dx).clamp(0, n - 1)) as usize) == Some(&true)))
|
||||
};
|
||||
let missed = (0..on_cpu.len()).filter(|&i| inside(i) && !on_gpu[i]).count();
|
||||
assert!(missed * 200 < area, "face {f}: {missed} of its {area} hovered pixels do not light (the highlight fights the model)");
|
||||
checked += 1;
|
||||
}
|
||||
assert!(checked >= 3, "{checked} faces seen lit");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn construction_drawing_and_hover_overlays_preserve_visible_gaps() {
|
||||
fn length(shape: &Shape) -> f32 {
|
||||
|
||||
Reference in new issue
Block a user