map: clamp the canvas onto whatever zoom ladder is in force

CreateZoomLevels is the extension point a canvas addon replaces
wholesale, and OnLoad seeds a scale (1) before any addon has run.
OpenMapClassic's ladder is 27 steps from 0.0125 to 0.899 -- its whole
range below this file's own 1, because its stitched world is reached by
zooming out past a single zone -- so the canvas sat above its own
ceiling, and ZoomIn, which walks the ladder by value, found no step at
all above it.

ClampZoomToLevels is called from GetCanvasScale rather than from
CreateZoomLevels, because CreateZoomLevels is exactly the function the
addon will not have.

Also records where OpenMapClassic stands, measured: 808 textures in the
canvas subtree, 166 placed, spanning ~4600x3270 units of a 1002x668
canvas, with a ladder floor implying a world ~80000 units wide. The Era
map tree under it answers correctly; GetMapArtLayers answering one
constant for every map is the thread to pull next.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SgstFWtn1RxEJ5kVrpgYwn
This commit is contained in:
2026-11-03 15:05:14 -07:00
co-authored by Claude Opus 5
parent 07dd5ba7e1
commit 3e8c995e27
2 changed files with 111 additions and 20 deletions
+47 -20
View File
@@ -434,7 +434,50 @@ function MapCanvasScrollControllerMixin:GetCanvas()
return self.Child
end
-- The ladder's two ends, or nil before there is a ladder.
function MapCanvasScrollControllerMixin:GetZoomBounds()
local levels = self.zoomLevels
local n = levels and table.getn(levels) or 0
if n == 0 then
return nil
end
return levels[1].scale, levels[n].scale
end
-- Pull every scale the controller holds onto the ladder it currently has.
--
-- `CreateZoomLevels` is the documented extension point and a canvas addon replaces it wholesale,
-- so the ladder can change out from under a scale that was already set -- and `OnLoad` sets one,
-- 1, before any addon has run. OpenMapClassic's ladder is 27 steps from 0.0125 to 0.899, its
-- whole range *below* this file's own 1, because its stitched world is reached by zooming out
-- past a single zone; measured with it loaded, the canvas sat at 1.0, above its own ceiling, and
-- `ZoomIn`/`ZoomOut` walk the ladder **by value** -- they look for the first step above or below
-- the current scale -- so a scale outside it cannot be scrolled back onto at either end.
--
-- Here rather than in `CreateZoomLevels`, because the addon's replacement is the one that will
-- not have it.
function MapCanvasScrollControllerMixin:ClampZoomToLevels()
local floor, ceil = self:GetZoomBounds()
if not floor then
return
end
local function clamp(v)
if not v then
return v
elseif v < floor then
return floor
elseif v > ceil then
return ceil
end
return v
end
self.currentScale = clamp(self.currentScale)
self.targetScale = clamp(self.targetScale)
self.zoomTarget = clamp(self.zoomTarget)
end
function MapCanvasScrollControllerMixin:GetCanvasScale()
self:ClampZoomToLevels()
return self.currentScale or 1
end
@@ -624,26 +667,10 @@ function MapCanvasScrollControllerMixin:CreateZoomLevels()
end
self.zoomLevels = levels
self.baseScale = levels[1].scale
-- A view left outside the new ladder is pulled back onto its nearest end. `ZoomIn`/`ZoomOut`
-- walk the ladder **by value** — the first step above or below the current scale — so a view
-- outside it at either end cannot be climbed back onto by scrolling. Both ends happen: a
-- window that grew raises the floor, and a canvas addon that replaces the ladder wholesale
-- lowers the ceiling, which is OpenMapClassic's case (its stitched world is reached by
-- zooming *out* far past this file's own scale 1, so its whole ladder sits below ours).
local floor, ceil = levels[1].scale, levels[table.getn(levels)].scale
local clamped = self.currentScale
if clamped then
if clamped < floor then
clamped = floor
elseif clamped > ceil then
clamped = ceil
end
end
if clamped and clamped ~= self.currentScale then
self.currentScale = clamped
self.targetScale = clamped
self.zoomTarget = clamped
end
-- A window that grew raises the floor, so a view below it is pulled back on; see
-- `ClampZoomToLevels`, which is where the rule lives because a canvas addon's own
-- `CreateZoomLevels` will not have it.
self:ClampZoomToLevels()
-- The scale itself is only seeded when there is none: `CreateZoomLevels` is called again on
-- every map change and on every container resize, and a view the player has zoomed must not
-- snap back because the window was dragged.
+64
View File
@@ -2728,3 +2728,67 @@ Two consequences had to follow it:
The no-art-layer fallback changed with them: the canvas used to take the **container's** size when
a map had no art layer, which was safe only while the container was 1002x668. It is 1.12's own art
rect now in every case, and the fitted floor is what keeps it visible in a smaller window.
## A canvas addon's zoom ladder, and the scale that was set before it existed
`MapCanvasScrollControllerMixin:OnLoad` seeds `currentScale = 1`, and `CreateZoomLevels` is the
documented extension point a canvas addon replaces **wholesale**. OpenMapClassic's ladder is 27
steps from **0.0125 to 0.899** — its whole range below this file's own 1, because its stitched
world is reached by zooming *out* far past a single zone. Measured with it loaded:
```
zoom: fit=1.0000 min=0.0125 max=0.8990 cur=1.0000 levels=27
```
The canvas sat above its own ceiling, and `ZoomIn`/`ZoomOut` walk the ladder **by value** — the
first step above or below the current scale — so from 1.0 `ZoomIn` found nothing at all. The clamp
is in `ClampZoomToLevels`, called from `GetCanvasScale`, because `CreateZoomLevels` is exactly the
function the addon will have replaced.
### Where OpenMapClassic actually stands, measured
With it enabled and the map open, the canvas subtree holds **808 textures, 263 shown, 166 with a
resolved rect**, spanning roughly 4600x3270 canvas units of a 1002x668 canvas — and that is only
the part built so far, because the stitch is lazy per zone. Its ladder's floor of 0.0125 implies a
world about **80 000 units wide**: 1002 / 0.0125. So what a player sees is a correct crop of a
world far larger than the canvas, at whatever scale the view is on — which is why grom's
screenshot is a band of zone art with grey around it rather than misplaced art.
The Era map tree underneath it answers correctly, which rules out the obvious suspects:
```
children of 947: 2
1414 Kalimdor rect=0.0888,0.4002,0.0791,0.8695 art=1002x668
zone 1440 Ashenvale rectOnParent=0.4176,0.5743,0.3313,0.4879
1415 Eastern Kingdoms rect=0.6238,0.9232,0.0269,0.8713 art=1002x668
```
`C_Map.GetMapArtLayers` answers a constant 1002x668 for **every** map, which is faithful for 1.12
— every zone, continent and world sheet is 1002x668 of art in twelve 256 tiles — but it is the one
number OpenMapClassic's stitch is built on (`Map/Stitch.lua`'s `ArtSize`), together with
`GetMapRectOnMap`. That is where the next session starts.
## Two port edits to BetterCharacterPanel
Mirrored at `Addon_Ports/bettercharacterpanel_port/`, both one-liners, both the addon's own bug:
- **`skins/paperdoll/BetterCharacterPanel-paperdoll-pfUI.lua:127`** — `SetCustomPositionCloseButton`
indexes `BCPPFUIUnifiedBackdrop` unguarded, where line 121 guards the same global three lines
up. FrameXML's own `ToggleCharacter` runs `ShowUIPanel(CharacterFrame)` — and so
`CharacterFrame_OnShow`, and so this — **before** `CharacterFrame_ShowSubFrame`, and the
backdrop is built only in `OnPaperDollShow`. So the first character-panel open of every session
raised, on any 1.12 client; it healed a moment later when `OnPaperDollShow` built the backdrop
and called `ChangeFrameValues` again. It was the only runtime error in grom's whole session log.
- **`BetterCharacterPanel-paperdoll.lua:581`** — `SetMinMaxValues(0, contentH - frameH - 20)`. The
stats content is 699 in a 451 window, so the range has to be 248 and the bar stopped at 228:
twenty units of the last card that no amount of scrolling could reach. The engine's own
`GetVerticalScrollRange()` answers 296 here and is not wrong — it measures the scroll child's
whole **subtree** (`0x786e30` → `0x786f80`), and the cards' pfUI shadows hang past
`contentFrame:SetHeight(yOffset)`. The edit keeps the addon's own measure and drops only the
fudge.
What is left of "the info is bigger than the frame" is the addon's own arithmetic and is not a
client fault: `BCPPFUICharacterInformationFrame` is `PaperDollFrame:GetHeight() + 19 - padding` =
**522**, while `BCPPFUIUnifiedBackdrop.backdrop` is `512 - 10 - 72` = **430**, so the stats column
stands 46 units proud of the panel at the top and 46 at the bottom. `PaperDollFrame` is
`setAllPoints` to `CharacterFrame` in the realm's own XML, so the 512 it is derived from is right.