Skip to content

fix(rendering): map pointer coordinates over the unzoomed design space in screenToWorld - #862

Merged
Exoridus merged 2 commits into
nextfrom
fix/view-screen-to-world-zoom
Oct 22, 2026
Merged

Exoridus merged 2 commits into
nextfrom
fix/view-screen-to-world-zoom

Conversation

@Exoridus

Copy link
Copy Markdown
Owner

View.screenToWorld(x, y) mapped pointer coordinates wrongly whenever the zoom was not 1. The two-argument form used the visible size (view.width/view.height) as the span of the input coordinates, but setZoom() shrinks or grows exactly that size while pointer coordinates stay in the unzoomed logical canvas space. For new View(400, 300, 800, 600) at setZoom(2), the screen centre (400, 300) mapped to (600, 450) instead of (400, 300). The existing zoom round-trip test stayed green because worldToScreen made the same assumption and cancelled the error.

Contract

  • The two-argument screenToWorld/worldToScreen map over the view's design space: the unzoomed size given to the constructor, resize() or reset(). Zooming, or writing size directly, changes how much world is visible, not which coordinates are accepted. A full-canvas view sized to the canvas therefore accepts pointer.x/pointer.y unchanged at every zoom level.
  • The four-argument form maps a coordinate on a surface of the given size and honours the viewport. Its size arguments only need to share the coordinate's unit: app.width/app.height for pointer coordinates, app.canvas.width/app.canvas.height for backing-store pixels. A zero-extent viewport returns a finite point instead of NaN.
  • app.screenToWorld(x, y) and pointer hit-testing now use the four-argument form over the logical canvas, so an active camera that covers only part of the canvas (split screen) or has a custom size maps correctly. This matches how both backends place a view: viewport fraction times the render surface.
  • zoom(factor) no longer resets the design space (it used to go through resize() and reset zoomLevel to 1); it now scales the current size and tracks zoomLevel.
  • RenderingContext.resize() keeps the active camera's zoom across canvas resizes, as its documentation already stated; View.resize() alone still resets zoom.

Tests

Regression tests assert absolute world positions derived from the camera geometry, never round-trips: zoom 2 and 0.5, panned non-square camera, rotation, the backend projection (getTransform onto the design surface), zoom(), direct size writes, View.from({ zoom }), zero-size construction, resize, the four-argument form for full and split viewports, Application.screenToWorld with zoomed and split active cameras, InteractionSystem hits through a zoomed and a split-screen camera, and zoom preservation on RenderingContext.resize. Two existing tests (view-rotation-pivot, view-rotated-cull-bounds) had adapted their screen coordinates to the defect ("zoom shrinks the design space"); they now probe the design space and a unit surface respectively, with unchanged expectations about the rendered area.

The coordinates guide's pointer snippet now converts over app.width/app.height, which is correct for split views too; the pointer guide and API reference describe the contract above.

This is a v0.20 correctness fix: code that compensated for the old mapping (for example by passing view.width as the input span) has to drop that compensation.

@Exoridus
Exoridus enabled auto-merge (squash) October 22, 2026 10:30
@codecov

codecov Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 963 bytes (0.0%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
exo-esm-esm 1.4MB 315 bytes (0.02%) ⬆️
exo-esm-modules-esm 4.0MB 648 bytes (0.02%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: exo-esm-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
exo.esm.js 315 bytes 1.4MB 0.02%

Files in exo.esm.js:

  • ./src/input/InteractionSystem.ts → Total Size: 61.49kB

  • ./src/core/Application.ts → Total Size: 70.27kB

  • ./src/rendering/View.ts → Total Size: 16.71kB

  • ./src/rendering/RenderingContext.ts → Total Size: 21.39kB

  • ./src/input/Pointer.ts → Total Size: 19.73kB

view changes for bundle: site-server-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
_astro/ExampleBrowser.0PCuKEl-.js (New) 46.79kB 46.79kB 100.0% 🚀
_astro/ExampleBrowser.7XjdZhxn.js (Deleted) -46.79kB 0 bytes -100.0% 🗑️
view changes for bundle: exo-esm-modules-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
core/Application.js 125 bytes 73.33kB 0.17%
input/InteractionSystem.js 14 bytes 62.14kB 0.02%
rendering/RenderingContext.js 81 bytes 22.02kB 0.37%
input/Pointer.js -28 bytes 20.02kB -0.14%
rendering/View.js 456 bytes 17.19kB 2.73%

Files in core/Application.js:

  • ./src/core/Application.ts → Total Size: 70.27kB

Files in input/InteractionSystem.js:

  • ./src/input/InteractionSystem.ts → Total Size: 61.49kB

Files in rendering/RenderingContext.js:

  • ./src/rendering/RenderingContext.ts → Total Size: 21.39kB

Files in input/Pointer.js:

  • ./src/input/Pointer.ts → Total Size: 19.73kB

Files in rendering/View.js:

  • ./src/rendering/View.ts → Total Size: 16.71kB

@Exoridus
Exoridus merged commit faa9c61 into next Oct 22, 2026
17 checks passed
@Exoridus
Exoridus deleted the fix/view-screen-to-world-zoom branch October 22, 2026 10:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant