Skip to content

Commit c13a478

Browse files
fix: harden zoom and pan behavior by validating slider inputs, normalizing zoom values, clamping the internal scale range, and preventing non‑finite or zero scale deltas, ensuring stable keyboard/button zoom and scroll‑based panning.
1 parent 4ef87f0 commit c13a478

File tree

1 file changed

+46
-15
lines changed

1 file changed

+46
-15
lines changed

src/simulator/src/listeners.js

Lines changed: 46 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -605,8 +605,19 @@ export default function startListeners() {
605605

606606
if (event.deltaX !== undefined && event.deltaY !== undefined) {
607607
// Modern browsers: event.deltaX, event.deltaY
608-
deltaX = event.deltaX
609-
deltaY = event.deltaY
608+
// Normalize by deltaMode: 0 = pixel, 1 = line, 2 = page
609+
let modeScale = 1
610+
611+
if (event.deltaMode === 1) {
612+
// Lines → approximate pixels
613+
modeScale = 16
614+
} else if (event.deltaMode === 2) {
615+
// Pages → approximate one viewport height
616+
modeScale = window.innerHeight
617+
}
618+
619+
deltaX = event.deltaX * modeScale
620+
deltaY = event.deltaY * modeScale
610621
} else if (event.wheelDeltaX !== undefined && event.wheelDeltaY !== undefined) {
611622
// Webkit browsers: wheelDeltaX, wheelDeltaY (inverted sign)
612623
deltaX = -event.wheelDeltaX
@@ -851,29 +862,42 @@ export function setZoomFromSlider(
851862
minZoom = 0.5 * DPR,
852863
maxZoom = 4 * DPR
853864
) {
854-
if (maxSliderValue === minSliderValue || maxZoom === minZoom) return
865+
// Guard against invalid inputs and degenerate ranges
866+
const inputs = [sliderValue, minSliderValue, maxSliderValue, minZoom, maxZoom]
867+
if (
868+
!inputs.every(Number.isFinite) ||
869+
maxSliderValue === minSliderValue ||
870+
maxZoom === minZoom
871+
) {
872+
return
873+
}
855874

856875
// Normalize slider value to 0-1 range
857-
const normalizedValue = (sliderValue - minSliderValue) / (maxSliderValue - minSliderValue)
858-
876+
const normalizedValue =
877+
(sliderValue - minSliderValue) / (maxSliderValue - minSliderValue)
878+
859879
// Map to zoom scale range
860880
const targetScale = minZoom + normalizedValue * (maxZoom - minZoom)
861-
881+
862882
// Clamp to valid zoom range
863883
const clampedScale = Math.max(minZoom, Math.min(maxZoom, targetScale))
864-
884+
865885
// Calculate delta from current scale
866886
const scaleDelta = clampedScale - globalScope.scale
867-
887+
888+
// Avoid applying invalid or zero delta
889+
if (!Number.isFinite(scaleDelta) || scaleDelta === 0) return
890+
868891
// Apply zoom centered on viewport (method = 3)
869892
changeScale(scaleDelta, 'zoomButton', 'zoomButton', 3)
870-
893+
871894
// Update display
872895
gridUpdateSet(true)
873896
updateCanvasSet(true)
874897
scheduleUpdate()
875898
}
876899

900+
877901
/**
878902
* Get the current zoom level as a slider value
879903
* Inverse of setZoomFromSlider - converts internal scale to slider value
@@ -893,20 +917,27 @@ export function getZoomSliderValue(
893917
minZoom = 0.5 * DPR,
894918
maxZoom = 4 * DPR
895919
) {
896-
if (maxSliderValue === minSliderValue || maxZoom === minZoom) {
920+
const inputs = [minSliderValue, maxSliderValue, minZoom, maxZoom, globalScope.scale]
921+
if (
922+
!inputs.every(Number.isFinite) ||
923+
maxSliderValue === minSliderValue ||
924+
maxZoom === minZoom
925+
) {
897926
return minSliderValue
898927
}
899928

900929
const currentScale = globalScope.scale
901-
930+
902931
// Clamp current scale to valid range
903932
const clampedScale = Math.max(minZoom, Math.min(maxZoom, currentScale))
904-
933+
905934
// Normalize scale to 0-1 range
906935
const normalizedScale = (clampedScale - minZoom) / (maxZoom - minZoom)
907-
936+
908937
// Map to slider value range
909-
const sliderValue = minSliderValue + normalizedScale * (maxSliderValue - minSliderValue)
910-
938+
const sliderValue =
939+
minSliderValue + normalizedScale * (maxSliderValue - minSliderValue)
940+
911941
return sliderValue
912942
}
943+

0 commit comments

Comments
 (0)