Skip to content

Fix isometric collision bounds, anchored raycasts and zoomed cameras - #1762

Merged
obiot merged 1 commit into
masterfrom
fix/iso-collision-raycast-camera-zoom
Oct 23, 2026
Merged

obiot merged 1 commit into
masterfrom
fix/iso-collision-raycast-camera-zoom

Conversation

@obiot

@obiot obiot commented Oct 23, 2026

Copy link
Copy Markdown
Member

Engine fixes found while building a new isometric example (coming in its own PR), plus the skill updates and a Line of Sight example that shows the raycast fix.

Tiled

  • Collision objects whose shape reaches left of or above their position, like every rectangle on an isometric map, now have bounds covering their shape. The shape object kept its bounds at pos .. pos + size, so getBounds() (what the quadtree files it by, and what queryAABB and raycast prefilter on) sat beside the drawn wall, and a character could walk through it wherever the quadtree split differently. The object now starts at its shapes' top left corner and the shapes are translated back, so the vertices are unchanged. Such an object's pos changes; point objects keep theirs.
  • Tile objects on isometric maps collide over their image, as on other maps. parseTMXShapes ran the isometric projection on a tile object's image box, which Tiled already draws upright, giving a slanted body hanging off the image.

Physics

  • The builtin raycast honours the anchor, as collisions have since 20.7 (Physics: collision shapes follow the renderable, and segments collide as segments #1693). It placed each shape at pos + shape.pos, so on a body anchored anywhere but its top left (a Sprite defaults to its centre) a ray hit half the body's size away from where it collides, or missed it. It now uses the same anchor offsets as SAT (anchorOffsetX / anchorOffsetY, exported from sat.js), honouring applyAnchorTransform = false. The 3D raycast already did this.

Camera

  • A zoomed camera follows, clamps and focuses in world units. follow (deadzone and clamps), moveTo's bounds clamp and focusOn used the on-screen width / height, while a zoomed camera shows width / zoom of the world: zoomed out, a followed target sat off centre and the view scrolled past its bounds; zoomed in, it stopped short of them.
  • localToWorld / worldToLocal take zoom and screenX / screenY into account, so a pointer on a zoomed camera, or on a second camera drawn in a corner, hits what it points at. world = pos + (screen - screenOffset) / zoom, then the existing world offset and camera transform steps; worldToLocal is the exact inverse.

Also

  • Text: an empty or blank label gets a canvas at least one 32 px bucket wide. A zero-wide canvas made WebGPU refuse the upload and lose the frame.
  • Typings: TextureAtlas#getAnimationSettings() is typed as the settings it returns, and Sprite3d accepts named animations (as an aseprite atlas carries them).
  • Skills: tilemaps (what sortOn = "y" can and cannot sort, and the default bodies of class-less tile objects), drawing (create a CanvasRenderTarget once, since its GPU texture lives as long as the renderer, and invalidate() it after repainting), lighting (a sprite without a normal map is not shaded at all, not "lit flatly").
  • Line of Sight example: round pillars with Ellipse bodies anchored at their centre, and patrolling intruders the cone spots with a raycast check.

Verification

  • New specs in tmx-shape-factory, tmxtilemap, raycast, camera, pointer-world-offset and text_bucket; every one fails with its fix reverted, and each half of the conversion fix (zoom, screen offset) is caught separately.
  • Full suite: 8,304 passed, 10 skipped. Lint clean.
  • The isometric example that surfaced these runs on WebGL, Canvas and WebGPU: a wall probe walking into every wall from 8 directions finds no clipping, and its minimap camera now uses follow() with a zoom of 0.1.

🤖 Generated with Claude Code

https://claude.ai/code/session_01SeWCkETjW35ZbXDRswV1hx

Tiled:
- a collision object whose shape reaches left of or above its position,
  like every rectangle on an isometric map, now has bounds covering its
  shape, so collision checks, queryAABB and raycast find it wherever the
  shape is. Its pos becomes the top left corner of the shape (point
  objects keep theirs). Parts of it were missed, so a character could
  walk through walls
- a tile object on an isometric map collides over its image; its default
  body was the image box projected onto the map grid

Physics:
- the builtin raycast measures a body from the frame its renderable draws
  in, using the same anchor offsets as SAT (now exported from sat.js). On
  a body anchored anywhere but its top left, such as a Sprite's default
  centre, a ray hit half the body's size away from it, or missed it

Camera:
- a zoomed camera follows its target, honours its deadzone, stays within
  its bounds and centres on focusOn in world units
- localToWorld and worldToLocal take zoom and screenX/screenY into
  account, so a pointer on a zoomed camera, or on one drawn in a corner
  of the screen, hits what it points at

Text: an empty or blank label gets a canvas at least one bucket wide,
since WebGPU refuses a zero-sized upload and blanked the whole frame.

Typings: TextureAtlas#getAnimationSettings() is typed as the settings it
returns, and Sprite3d accepts named animations.

Skills: the tilemaps skill covers what the y-sort can and cannot sort and
the default bodies of class-less tile objects; the drawing skill says to
create a CanvasRenderTarget once and invalidate() it after repainting;
the lighting skill no longer claims an unmapped sprite is lit flatly.

Line of Sight example: round pillars as Ellipse bodies anchored at their
centre, and patrolling intruders the cone spots with a raycast check.

Verified: every new spec fails with its fix reverted (each half of the
conversion fix caught separately); full suite 8,304 passed; lint clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SeWCkETjW35ZbXDRswV1hx
Copilot AI balanced review requested due to automatic review settings October 23, 2026 07:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@obiot
obiot merged commit 988ab1f into master Oct 23, 2026
6 checks passed
@obiot
obiot deleted the fix/iso-collision-raycast-camera-zoom branch October 23, 2026 07:26
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.

2 participants