Squashed from work; the fine-grained history is under tag archive/work-2026-08-29: - mp* wave: mpwm window manager + the mp app family, WM API, theme bridge, PDF engine fix - mpwm polish wave: terminal key focus, focus-history close order, pop-back-to-origin, occupied-workspace cycling, demo - mpwm: warm-instance pool, flat-luminance opens, flicker-free CEF resize - work: land the sources the last commits reference - kenney: catalogue all 50 free 3D kits; Modal dismissed() never fired - platform: windows check green again — SetWindowTextW binding - map: exact warp-aware inverse projections — pointer ops work folded - mpwm: quick-look gap fixes; image cache eviction on preview unload - tweaker: material thumbnails + vibecode popup + ctrl-space notes, undo/redo over the edit ledger, capture-semantics pi - tweaker: vibe popup card chrome + dispatch order, ctrl-space notes verified, sploded design v2 chapter - tweaker: tabbed side panel (Props/Shader/Tree) - shader tab with checkerboard material well + prompt, complete widget- - sploded v2: nesting-depth z, hairline scope frames, body pass - sploded: pin the depth convention with a test, kill the draw_depth residue - sploded: real body-pass split (scene-pass capture, panel flat) + y-convention source of truth with anti-flip gate test - sploded: hollow outlines, flat-band input, ray-pick unprojection - tweaker: shader tab defaults to the selection's first draw layer, stale hint trimmed - sploded: outlines become clipped, antialiased strips; tighter deck - sploded: merge the lane's v2 (nesting-depth z, clipped AA strip outlines, flat-band input, unproject, SplodedStack bod - sploded: the exploded view is a LIVE view — pointer events route through the inverse explode transform (ray -> plane - - tweaker: tabs are real widgets (uid, tree node under the dock, own plane in 3D) and pickable; navigation-class clicks - sploded: pinned/hover outlines render on the widget's own plane in 3D — per-widget nesting depth lives on the platform - tweaker: the material well renders the pinned widget's actual shader — the swatch byte-copies the widget's live draw c - tweaker: the Shader tab shows the shader as written — the layer's pixel/vertex fn source (nearest definition up the co - sploded: I = true isometric preset (yaw 45°, pitch atan(1/√2)) - tweaker: eyedropper — the colour popover's pick button arms a pixel probe; the next press in the app samples that devi - tweaker: the shader loop closes — /tweak/apply resolves the pinned widget by uid (anonymous path segments never round- - vj: responsive DJ mixer + Windows drag-and-drop, cherry-picked from PR #1199 (vjroger) - tweaker: the material well is a magnifier — the mirrored instance draws at the widget's native size in the well's own - tweaker: per-layer material thumbnails — the Widget derive emits WidgetNode::layer_areas() (every #[live] Draw… field - tweaker: the shader source view is the real CodeView (syntax highlighting, selection, editing) when the app registers - tweaker: Ctrl+Enter sends on every platform (TextInput treated only Cmd as primary on macOS, so Ctrl+Enter inserted a - Modal claims no layout slot: the DJ page fills its window again - tweaker: every fn apply recompiles (eval_chunk ran every chunk under ONE synthetic callsite, so the script body — and - tweaker: an apply whose draw shader fails to compile is rejected — the layer goes back (last live fns / the fn as writ - tweaker: the Shader tab's source view owns its scrolling (the ScrollYView around the CodeView double-scrolled the care - widgets: set_visible belongs to every widget, not just View (#1194) - script: a dead heap's resource handles must not outlive it (#1195) - Resources: search the executable's directory, not only the working directory (#1196) - Windows: fit a restored window to the displays that are actually attached (#1197) - d3d11: a failing GPU call reports the loss instead of killing the process (#1198) Co-authored-by: Kevin Boos <1139460+kevinaboos@users.noreply.github.com>
168 lines
5.3 KiB
Rust
168 lines
5.3 KiB
Rust
//! Working-tree sprite-pass lab test (not for commit): screenshot the row of
|
|
//! six billboard recipes and say, per recipe, whether pixels arrived.
|
|
|
|
use makepad_test::{makepad_test, Selector, TestApp};
|
|
use makepad_zune_png::makepad_zune_core::bytestream::ZCursor;
|
|
use makepad_zune_png::PngDecoder;
|
|
|
|
struct Image {
|
|
width: usize,
|
|
height: usize,
|
|
rgba: Vec<u8>,
|
|
}
|
|
|
|
impl Image {
|
|
fn read(path: &std::path::Path) -> Image {
|
|
let bytes = std::fs::read(path)
|
|
.unwrap_or_else(|err| panic!("cannot read grab {}: {err}", path.display()));
|
|
let mut decoder = PngDecoder::new(ZCursor::new(&bytes));
|
|
let pixels = decoder
|
|
.decode_raw()
|
|
.unwrap_or_else(|err| panic!("cannot decode grab {}: {err:?}", path.display()));
|
|
let (width, height) = decoder.dimensions().expect("grab has no dimensions");
|
|
let components = decoder
|
|
.colorspace()
|
|
.expect("grab has no colorspace")
|
|
.num_components();
|
|
let mut rgba = vec![0u8; width * height * 4];
|
|
for i in 0..width * height {
|
|
let src = i * components;
|
|
rgba[i * 4] = pixels[src];
|
|
rgba[i * 4 + 1] = pixels[src + 1];
|
|
rgba[i * 4 + 2] = pixels[src + 2];
|
|
rgba[i * 4 + 3] = if components == 4 { pixels[src + 3] } else { 255 };
|
|
}
|
|
Image {
|
|
width,
|
|
height,
|
|
rgba,
|
|
}
|
|
}
|
|
|
|
fn pixel(&self, x: usize, y: usize) -> [u8; 3] {
|
|
let p = (y.min(self.height - 1) * self.width + x.min(self.width - 1)) * 4;
|
|
[self.rgba[p], self.rgba[p + 1], self.rgba[p + 2]]
|
|
}
|
|
}
|
|
|
|
fn is_red(p: [u8; 3]) -> bool {
|
|
p[0] > 150 && p[1] < 90 && p[2] < 90
|
|
}
|
|
fn is_yellow(p: [u8; 3]) -> bool {
|
|
p[0] > 150 && p[1] > 150 && p[2] < 90
|
|
}
|
|
fn is_green(p: [u8; 3]) -> bool {
|
|
p[1] > 120 && p[0] < 100 && p[2] < 100
|
|
}
|
|
fn is_magenta(p: [u8; 3]) -> bool {
|
|
p[0] > 150 && p[1] < 90 && p[2] > 150
|
|
}
|
|
fn is_blue(p: [u8; 3]) -> bool {
|
|
p[2] > 150 && p[0] < 100 && p[1] < 150
|
|
}
|
|
|
|
#[makepad_test]
|
|
fn each_billboard_recipe_puts_pixels_on_screen(app: TestApp) {
|
|
app.locator(Selector::id("lab")).wait_visible();
|
|
// Give the pass a couple of frames to settle, then grab.
|
|
std::thread::sleep(std::time::Duration::from_millis(600));
|
|
let path = app.screenshot();
|
|
println!("[spritelab] grab: {}", path.display());
|
|
let img = Image::read(&path);
|
|
|
|
// Six equal column bands, one per case, in submission order.
|
|
let band_w = img.width / 6;
|
|
let names = [
|
|
"0 asset-ui recipe (whole uv, zw=0) ",
|
|
"1 sandbox barrel (half uv, zw=46x32) ",
|
|
"2 sandbox troo (window uv, zw=472x434) ",
|
|
"3 troo mirrored (u0>u1, zw=472x434) ",
|
|
"4 troo window, ramp OFF (zw=0) ",
|
|
"5 barrel recipe facing AWAY (yaw+pi) ",
|
|
];
|
|
let mut counts = [[0usize; 5]; 6];
|
|
for band in 0..6 {
|
|
let x0 = band * band_w;
|
|
let x1 = (band + 1) * band_w;
|
|
for y in 0..img.height {
|
|
for x in x0..x1 {
|
|
let p = img.pixel(x, y);
|
|
if is_red(p) {
|
|
counts[band][0] += 1;
|
|
}
|
|
if is_yellow(p) {
|
|
counts[band][1] += 1;
|
|
}
|
|
if is_green(p) {
|
|
counts[band][2] += 1;
|
|
}
|
|
if is_magenta(p) {
|
|
counts[band][3] += 1;
|
|
}
|
|
if is_blue(p) {
|
|
counts[band][4] += 1;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
println!("[spritelab] band red yellow green magenta blue");
|
|
for band in 0..6 {
|
|
println!(
|
|
"[spritelab] {} {:>7} {:>7} {:>7} {:>7} {:>7}",
|
|
names[band],
|
|
counts[band][0],
|
|
counts[band][1],
|
|
counts[band][2],
|
|
counts[band][3],
|
|
counts[band][4]
|
|
);
|
|
}
|
|
|
|
let mut failures: Vec<String> = Vec::new();
|
|
if counts[0][0] < 100 || counts[0][1] < 100 {
|
|
failures.push(format!(
|
|
"case 0 (asset-ui recipe) missing: red {} yellow {}",
|
|
counts[0][0], counts[0][1]
|
|
));
|
|
}
|
|
if counts[1][0] < 100 {
|
|
failures.push(format!(
|
|
"case 1 (sandbox barrel recipe) missing: red {}",
|
|
counts[1][0]
|
|
));
|
|
}
|
|
if counts[2][2] < 100 {
|
|
failures.push(format!(
|
|
"case 2 (SANDBOX TROO RECIPE) missing: green {}",
|
|
counts[2][2]
|
|
));
|
|
}
|
|
if counts[3][2] < 100 {
|
|
failures.push(format!(
|
|
"case 3 (troo mirrored) missing: green {}",
|
|
counts[3][2]
|
|
));
|
|
}
|
|
if counts[4][2] < 100 {
|
|
failures.push(format!(
|
|
"case 4 (troo, ramp off) missing: green {}",
|
|
counts[4][2]
|
|
));
|
|
}
|
|
// Case 5 (facing away) is report-only: culling it or showing it are both
|
|
// defensible; the row above says which one this build does.
|
|
println!(
|
|
"[spritelab] case 5 (facing away) red pixels: {} -> {}",
|
|
counts[5][0],
|
|
if counts[5][0] > 100 {
|
|
"drawn from behind (no backface cull)"
|
|
} else {
|
|
"NOT drawn (culled or degenerate when facing away)"
|
|
}
|
|
);
|
|
assert!(
|
|
failures.is_empty(),
|
|
"invisible billboard recipes:\n{}",
|
|
failures.join("\n")
|
|
);
|
|
}
|