diff --git a/crates/benilla-app/src/ui_script/extract/mod.rs b/crates/benilla-app/src/ui_script/extract/mod.rs index d5c0edef..456f6d52 100644 --- a/crates/benilla-app/src/ui_script/extract/mod.rs +++ b/crates/benilla-app/src/ui_script/extract/mod.rs @@ -1319,6 +1319,8 @@ fn convert_entry( portrait_unit, rotation, desaturated, + mask, + tile, } => { // A live unit portrait (`SetPortraitTexture(region, unit)`): the model bake, or the // reference's 2D TemporaryPortrait while the model streams; no entry yet draws nothing. @@ -1382,8 +1384,15 @@ fn convert_entry( Some(TexCoords::Corners(corners)) => UvRect::from_corners(corners), None => UvRect::FULL, }; - // A slice past the texture tiles (`BonusActionBarFrame.lua:173`), on that axis alone. - let wrap = tiling_axes(&uv); + // A slice past the texture tiles (`BonusActionBarFrame.lua:173`), on that axis alone; + // `SetHorizTile`/`SetVertTile` ask for the same wrap without naming a count, so they + // join it here and the count is computed below, once the art's texel size is known. + let explicit_uv = tex_coords.is_some(); + let mut uv = uv; + let wrap = { + let (h, v) = tiling_axes(&uv); + (h || (tile.0 && !explicit_uv), v || (tile.1 && !explicit_uv)) + }; let handle = match (path.as_deref(), assets.as_mut()) { (Some(p), Some(a)) => { let resolved = if p == benilla_ui::script::nameplate::BORDER_TEXTURE { @@ -1426,6 +1435,36 @@ fn convert_entry( } _ => None, }; + // `SetHorizTile`/`SetVertTile`'s repeat count: the region's own span over the art's, + // which is what "tile at its native size" means. Taken here because the texel size is + // only known once the art has loaded. A missing image, a zero-texel axis or an + // explicit `SetTexCoord` leaves the mapping alone. + if (tile.0 || tile.1) && !explicit_uv { + if let Some(sz) = handle + .as_ref() + .and_then(|h| images.get(h)) + .map(|img| img.texture_descriptor.size) + { + let repeats = |span: f32, texels: u32| { + if texels > 0 { + span / texels as f32 + } else { + 1.0 + } + }; + let u = if tile.0 { + repeats(rect.width(), sz.width) + } else { + 1.0 + }; + let v = if tile.1 { + repeats(rect.height(), sz.height) + } else { + 1.0 + }; + uv = UvRect::from_tex_coords([0.0, u, 0.0, v]); + } + } // A crop's magnified edge would filter in the neighbouring atlas cell. let uv_clamp = handle .as_ref() @@ -1447,6 +1486,18 @@ fn convert_entry( // `SetRotation` is counter-clockwise-positive, `UiQuad::rotation` clockwise. rotation: -rotation, desaturated, + // `AddMaskTexture`: the mask's own art and rect, converted to screen space the + // same way as `rect`. A mask whose art the archives lack drops the mask, not the + // quad — a missing cut-out shows the whole rectangle, which is the same thing the + // caller sees when it never had a mask to add. + mask: mask.and_then(|m| { + let art = assets.as_mut()?.sprite_texture(&m.path, images)?; + let r = m.rect; + Some(crate::ui_pass::UiQuadMask { + texture: art, + rect: Rect::new(r.left * s, h - r.top * s, r.right * s, h - r.bottom * s), + }) + }), ..default() }); } @@ -1831,6 +1882,83 @@ mod clip_plumb_tests { ); } + /// A mask whose art the archives lack drops the **mask**, not the quad. + /// + /// Worth pinning because the other reading is tempting and wrong: a mask that cannot load is + /// zero coverage, and honouring that would erase the art it was meant to shape — a stitched + /// map would go blank wherever a generated mask was missing, which reads as the map's own art + /// failing. Showing the whole rectangle is what the caller sees when it never had a mask. + /// + /// Runs with no `WorldAssets` at all, which is the same path a missing file takes: the quad is + /// a pathless solid so it still draws, and the mask asks for art and gets none. + #[test] + fn a_mask_whose_art_is_missing_drops_the_mask_and_keeps_the_quad() { + let script = UiScript::new().unwrap(); + script + .run( + r#" + local f = CreateFrame("Frame", "MaskPlumb") + f:SetPoint("TOPLEFT", 0, 0) + f:SetWidth(50); f:SetHeight(50) + local m = f:CreateTexture(nil, "ARTWORK") + m:SetTexture(0, 0, 1) + m:SetAllPoints() + local cut = f:CreateMaskTexture(nil, "ARTWORK") + cut:SetTexture("Interface\\AddOns\\NoSuch\\mask") + cut:SetAllPoints() + m:AddMaskTexture(cut) + "#, + ) + .unwrap(); + + let mut app = App::new(); + app.insert_non_send_resource(script); + app.init_resource::(); + app.init_resource::(); + app.init_resource::>(); + app.init_resource::(); + app.init_resource::(); + app.init_resource::(); + app.init_resource::(); + app.init_resource::(); + app.init_resource::(); + app.init_resource::(); + app.init_resource::(); + app.init_resource::(); + app.init_resource::