A wheel notch stepped from the easing scale, so notches did not accumulate

`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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-11-04 07:53:50 -07:00
co-authored by Claude Opus 5
parent a32963b3f8
commit c92f078c39
3 changed files with 186 additions and 2 deletions
+18 -2
View File
@@ -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)
@@ -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");
}
+110
View File
@@ -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.