From c92f078c39ec88f91d7fa213fe495030c9b8672e Mon Sep 17 00:00:00 2001 From: grom Date: Wed, 4 Nov 2026 07:53:50 -0700 Subject: [PATCH] A wheel notch stepped from the easing scale, so notches did not accumulate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `ZoomIn`/`ZoomOut` walked the zoom ladder from `GetCanvasScale()` — the live value the smooth zoom is still easing — so the notch after the first re-read a scale that had barely moved, found the same rung still under it, and re-set the target it already had. The ease closes about 63% of the gap a second, which is slow enough that a player never gets a second rung. Driven at grom's own 2560x1440 with his addons loaded, over OpenMapClassic's 27-step ladder (0.0330 .. 2.3734): four wheel-down notches at the map's centre all set target 0.9030 — one rung — and two more at the far right set nothing. That is his "it will only scroll very little out … and will only very little scroll in period". `GetZoomStepScale()` steps from `targetScale` instead, so notches accumulate and either end of a long ladder is reachable. The same measurement rules out the position dependence he saw: the wheel's target over the centre and 19 px from the right edge is `WorldMapButton` both times, because `wheel_target` walks past frames that take no wheel the way `0x7664f0` does. `docs/ADDON_COMPAT.md` also records the two addon-side causes of "I can't see any buttons but I can use them", both fixed in `Addon_Ports/` rather than here: OpenMapClassic's rail tabs are never dressed because `mod.FaceSideTab` ships in a file the release omits, and the OpenMap tab's own icon is absent from this patch chain. And pfQuest's minimap icons now fade with the minimap. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn --- crates/benilla-app/assets/ui/MapCanvas.lua | 20 +++- .../src/ui_script/map_canvas_tests.rs | 58 +++++++++ docs/ADDON_COMPAT.md | 110 ++++++++++++++++++ 3 files changed, 186 insertions(+), 2 deletions(-) diff --git a/crates/benilla-app/assets/ui/MapCanvas.lua b/crates/benilla-app/assets/ui/MapCanvas.lua index 8dd87468..9b9b3a19 100644 --- a/crates/benilla-app/assets/ui/MapCanvas.lua +++ b/crates/benilla-app/assets/ui/MapCanvas.lua @@ -753,9 +753,25 @@ function MapCanvasScrollControllerMixin:SetZoomTarget(scale, x, y) end end +-- Which rung a wheel notch steps FROM: where the view is heading, not where the ease has got +-- to. `currentScale` closes about 63% of the gap a second (`SCALE_RATE`), so a second notch +-- arriving a frame after the first re-reads a scale that has barely moved, finds the same rung +-- under it, and re-sets the target it already has. Measured at the player's own window size +-- with his addons loaded: four wheel-down notches over the map moved the ladder ONE step +-- (1.0000 -> 0.9030) and two more moved it none. Stepping from `targetScale` makes notches +-- accumulate, which is both how it feels in Era and the only way either end of a 27-step +-- ladder is reachable at all -- OpenMapClassic's runs 0.033 to 2.373, and its bottom rung is +-- the whole stitched world. +function MapCanvasScrollControllerMixin:GetZoomStepScale() + -- Through `GetCanvasScale`, because it clamps every held scale onto the current ladder as a + -- side effect, and a canvas addon can have replaced the ladder since the last notch. + local live = self:GetCanvasScale() + return self.targetScale or live +end + function MapCanvasScrollControllerMixin:ZoomIn() local n = table.getn(self.zoomLevels) - local current = self:GetCanvasScale() + local current = self:GetZoomStepScale() for i = 1, n do if self.zoomLevels[i].scale > current + 0.0001 then self:SetZoomTarget(self.zoomLevels[i].scale) @@ -765,7 +781,7 @@ function MapCanvasScrollControllerMixin:ZoomIn() end function MapCanvasScrollControllerMixin:ZoomOut() - local current = self:GetCanvasScale() + local current = self:GetZoomStepScale() for i = table.getn(self.zoomLevels), 1, -1 do if self.zoomLevels[i].scale < current - 0.0001 then self:SetZoomTarget(self.zoomLevels[i].scale) diff --git a/crates/benilla-app/src/ui_script/map_canvas_tests.rs b/crates/benilla-app/src/ui_script/map_canvas_tests.rs index 6232d1f1..74195374 100644 --- a/crates/benilla-app/src/ui_script/map_canvas_tests.rs +++ b/crates/benilla-app/src/ui_script/map_canvas_tests.rs @@ -480,3 +480,61 @@ fn the_canvas_container_is_the_windows_content_area_in_both_map_modes() { ); } } + +/// A wheel notch steps from where the view is **heading**, so notches accumulate while the +/// smooth zoom is still easing toward the last one. +/// +/// Before this, `ZoomIn`/`ZoomOut` read `GetCanvasScale()` — the live, easing value. The ease +/// closes about 63% of the gap a second, so the notch after the first found the same rung still +/// under it and re-set the target it already had: measured live at grom's window size with his +/// addons, four wheel-down notches moved the ladder one step and two more moved it none, which +/// is what "scroll only goes a little way out" is. The ladder here is synthetic because the +/// rule has to hold for a canvas addon's own ladder, which is the only kind with enough rungs +/// for the defect to show — `CreateZoomLevels` is the documented place to replace it. +#[test] +fn wheel_notches_accumulate_while_the_zoom_is_still_easing() { + let Some(s) = harness() else { return }; + let seat = "local sc = WorldMapFrame.ScrollContainer \ + sc.zoomLevels = {} \ + for i = 1, 10 do sc.zoomLevels[i] = { scale = i, layerIndex = 1 } end \ + sc.currentScale, sc.targetScale, sc.zoomTarget = 5, 5, 5 "; + let (a, b, c, live): (f64, f64, f64, f64) = s + .eval(&format!( + "{seat} \ + sc:ZoomOut() local a = sc.targetScale \ + sc:ZoomOut() local b = sc.targetScale \ + sc:ZoomOut() \ + return a, b, sc.targetScale, sc.currentScale" + )) + .unwrap(); + assert_eq!( + (a, b, c), + (4.0, 3.0, 2.0), + "three notches down the ladder must be three rungs, not one repeated" + ); + assert_eq!( + live, 5.0, + "and none of it touched the easing scale — the OnUpdate owns that" + ); + let (a, b, c): (f64, f64, f64) = s + .eval(&format!( + "{seat} \ + sc:ZoomIn() local a = sc.targetScale \ + sc:ZoomIn() local b = sc.targetScale \ + sc:ZoomIn() \ + return a, b, sc.targetScale" + )) + .unwrap(); + assert_eq!((a, b, c), (6.0, 7.0, 8.0), "and the same going in"); + // The step is still clamped to the ladder's own ends, which is what stops a held wheel + // walking the target past a rung that exists. + let (lo, hi): (f64, f64) = s + .eval(&format!( + "{seat} \ + for _ = 1, 20 do sc:ZoomOut() end local lo = sc.targetScale \ + for _ = 1, 40 do sc:ZoomIn() end \ + return lo, sc.targetScale" + )) + .unwrap(); + assert_eq!((lo, hi), (1.0, 10.0), "the ladder's own ends hold"); +} diff --git a/docs/ADDON_COMPAT.md b/docs/ADDON_COMPAT.md index f04212dd..f3b87a46 100644 --- a/docs/ADDON_COMPAT.md +++ b/docs/ADDON_COMPAT.md @@ -3262,3 +3262,113 @@ reads the live cursor. With a tick per move the same drag walks `0.5000,0.5000 0.7582,0.3073 → 0.5080,0.5161`, and `0.7582,0.3073` is exactly what hand-driving produced. A gesture instrument that does not tick measures nothing: **the frame between the moves is the gesture.** + +## The buttons you could click but not see, and the wheel that moved one rung + +grom, 2026-11-04: *"Some of the buttons are still not working … I still can't see any buttons or +settings, but I can find some of them invisible and use them … scroll is not working correctly, +like it will only scroll very little out unless I point my mouse on the far right side of the map, +and will only very little scroll in period."* + +Three separate causes, two of them in the addon rather than here. + +### The rail tabs were never dressed, because the file that dresses them is not in the release + +`Map/Panel.lua`'s `RailTab()` builds a tab out of four textures — `bg`, `hover`, `selected` and +`icon` — and gives none of them an anchor or a texture. The function that does both, +`mod.FaceSideTab`, ships in `Map/Dungeons.lua`: a file this release of OpenMap Classic does not +include and whose name does not appear in its `.toc`. Panel.lua guards the call +(`if mod.FaceSideTab then`), so with `tabRail` on — which is what grom's saved settings carry — +the rail is built, its tabs are sized, placed on the map's edge and take clicks and tooltips, and +**draw nothing at all**. + +Measured on the live interface, with a probe that prints each region's anchor count beside its +rect: + +``` +btn1 … size=40x42 points=1 RIGHT->WorldMapScrollContainer.BOTTOMRIGHT(+0,+65) + r1 Texture layer=BACKGROUND shown=1 NIL-RECT points=0 tex=nil + r2 Texture layer=HIGHLIGHT shown=1 NIL-RECT points=0 tex=nil + r3 Texture layer=OVERLAY shown=0 NIL-RECT points=0 tex=nil + r4 Texture layer=ARTWORK shown=1 NIL-RECT points=0 tex=nil +``` + +`points=0` is the whole diagnosis: not an anchor that failed, an anchor that was never set. The +button's own rect resolves, which is why the hit test finds it — **a frame can be in the hit chain +and emit no quad at all**, and that pair is what "invisible but I can use it" means. A region dump +that prints only `IsShown()` and the texture path cannot tell the two apart; print +`GetNumPoints()` beside the rect. + +The port supplies the missing function as `Map/SideTabs.lua` (one `.toc` line after +`Map\Panel.lua`), in the addon's own visual language: `Tile()`'s boxed face, a one-pixel edge that +`SetLit` tints, the action button's square highlight and the checked glow. `b.boxed` is set there +rather than in `RailTab` so `SetLit` only reaches for a backdrop once there is one. + +### An icon the client does not have reads back nil, and that is the feature test + +The OpenMap tab itself stayed a blank box after that, because it asks for +`Interface\Icons\Achievement_Zone_EasternKingdoms_01` — a retail icon, **ABSENT from this whole +patch chain** (`chain_report`: 0 copies). The addon's own `Look()` has a fallback path, but it +keys on `SetTexture` answering `false`, which is retail's contract; ours answers `1` or **nil**, +which is 1.12's own (`0x79bb40`, `(nil) | (number)`, and `reference/1.12-shapes.tsv` agrees). + +What settles it without a list of which icons exist: **a texture whose file did not load reads +back `GetTexture() == nil`**, live, because `Model::texture_probe` judges the path at +`SetTexture` time. So `if not b.icon:GetTexture() then b.icon:SetTexture(FALLBACK) end` is a tab +asking about itself, and it covers any favourite whose icon is as retail as the OpenMap tab's. +Note that this only holds where a probe is installed: offline (the harness) `SetTexture` answers +nil for everything and the read-back keeps the path, so a fallback driven off *the return value* +would replace every icon on the rail. The read-back does not have that failure mode. + +### The wheel: a notch stepped from the easing scale, so notches did not accumulate + +`ZoomIn`/`ZoomOut` walked the ladder from `GetCanvasScale()` — the **live, easing** value. +`currentScale` closes about 63% of the gap a second (`SCALE_RATE`), so the notch after the first +re-read a scale that had barely moved, found the same rung still under it, and re-set the target +it already had. Driven at grom's own window size with his addons loaded: + +``` +ladder steps=27 lo=0.0330 hi=2.3734 current=1.0000 target=1.0000 +wheel -1 ×4 at the map's centre → target 0.9030, 0.9030, 0.9030, 0.9030 (ONE rung) +wheel -1 ×2 at the far right → target 0.9030, 0.9030 (none) +``` + +`GetZoomStepScale()` now steps from `targetScale`, so notches accumulate and either end of a +27-step ladder is reachable. Covered by +`map_canvas_tests::wheel_notches_accumulate_while_the_zoom_is_still_easing`, which reads +`(4.0, 4.0, 4.0)` without the fix and `(4.0, 3.0, 2.0)` with it. + +The same measurement **rules out the position dependence**: the wheel's target over the map's +centre and 19 px from its right edge is the same frame both times (`WorldMapButton`, level 3, +topmost wheel-enabled hit), because `wheel_target` walks past frames that take no wheel the way +the reference's `0x7664f0` does. What looked like "it only works on the far right" was the one +rung arriving late. + +### pfQuest's minimap icons did not fade with the minimap + +1.12's `SetAlpha` (`0x76a690`) writes the raw value to the frame **and every descendant**, with no +draw-time product, so pfUI's fade on the minimap holder does reach pfQuest's pins — they are +children of `Minimap`, which `pfQuest/map.lua:232` takes as its draw layer. But pfQuest reasserts +its own alpha on every pin on every update (`NodeAnimate` eases each toward `defalpha`), so the +fade is overwritten within a frame. + +`pfUI/modules/autofade.lua` now ties them the way it already ties the unit frames — *through* the +addon rather than against it. A pin's `SetAlpha` records what pfQuest asked for and applies +`asked × factor`; its `GetAlpha` answers what pfQuest asked for, so NodeAnimate's easing runs +entirely in pfQuest's own numbers and never sees the factor. The factor is `pfUI.minimap`'s own +live alpha, which is what makes the tie to the minimap structural rather than configured: + +``` +minimap 1.00 → pin wants 1.00 raw 1.00 +minimap 0.25 → pin wants 1.00 raw 0.25 + … 40 rounds of pfQuest's own Animate(.., 1, ..) → wants 1.000 raw 0.250 (no fight, no drift) +minimap 1.00 → pin wants 1.00 raw 1.00 +``` + +### Still open: the zone art at middle zoom + +Not reproduced. `unexploredMode = "reveal"` is **not** the cause — the same view captured with +the fog `default` and `reveal` is pixel-indistinguishable — and switching `lod`, `close` and +`zoneart` off one at a time changed nothing in a capture either (those take a rebuild the slash +command alone does not force). What a capture cannot reproduce is which zones the player has +actually explored, and that is the one input the complaint turns on.