feat(Sprite3d): anchor support via settings.anchorPoint + named presets engine-wide#1538
Conversation
Adds a settings.originPoint option (preset string or {x,y}) so the
quad's pivot can be placed anywhere relative to pos, e.g. bottom-center
for ground-anchored 2.5D characters. Baked as a constant local offset
applied after flips, so it composes with billboarding and
trimmed/rotated atlas frames. Default stays centered — no behavior
change for existing code.
Updated the billboard example to anchor the character at its feet
instead of using a manual GY offset fudge factor.
Closes melonjs#1514
|
Hi, Thanks for the PR, before merging I'm gonna make some further changes however that I think makes it a better fit with the legacy/existing API :
|
|
@obiot thanks for the detailed feedback! Happy to implement this myself, just let me know if you'd rather I push these changes, or if you're planning to handle it on your end. Either way works for me. |
no worries, I will do it, already on it :) |
…ine-wide
Rework of the originPoint proposal into the hybrid agreed in review:
Sprite3d anchors through the SAME settings.anchorPoint key, convention
and centered default as the 2D Sprite — no second anchor API — while
keeping the (verified-correct) vertex-bake mechanism from the original
commit.
- new shared resolver (renderable/anchorPoint.ts): the named presets
("center", "top", "bottom", ..., "bottom-right") now work everywhere
settings.anchorPoint does — Sprite, Entity, Collectable, ImageLayer,
Text, BitmapText (and subclasses), plus Sprite3d. On the 2D classes
invalid values keep their historical silent (0, 0) outcome for full
backward compatibility, but log a console warning; Sprite3d (a new
surface) throws. Values are never clamped; Vector2d instances and
bare {x, y} objects pass through unchanged.
- Sprite resolves the anchor once, up front, and forwards the RESOLVED
pair into the texture-cache descriptor (spritesheet atlases store it
as the cached per-frame pivot), so presets and {x, y} behave
identically on both paths and a raw string or aliased Vector2d never
lands in the shared cache.
- ImageLayer normalizes its bare-number shorthand pre-super (its Sprite
base consumes the same setting and the strict resolver rejects bare
numbers).
- Sprite3d wires the inherited ObservablePoint as the LIVE anchor:
setMuted + setCallback post-super; runtime anchorPoint.set() re-bakes
the quad (region path via _applyFrame, plain-quad path via
bakeQuadVertices) and re-derives the cull bounds (direct point
mutation — the Rect width setter's recalc() walks Mesh's repurposed
normals array and would throw).
- Mesh gains the _anchorBaked opt-out consulted by preDraw's per-frame
applyAnchorTransform recompute, so the anchor never leaks into the
renderer transform on either camera path (plain Mesh behavior
unchanged, webgl_mesh_anchor.spec.js untouched and green).
- cull bounds fixed: side = max(max(w,h), sqrt2*hypot(hw+|ox|, hh+|oy|))
— the sphere exactly encloses the anchored quad in any billboard
orientation (a bottom-anchored 130x225 character was ~47% under-sized
and popped out on screen); centered default byte-identical.
- billboard example: anchorPoint: "bottom", camera-target framing
restored (GY * 0.6 became dead code aiming at the floor), stale
comment fixed.
- tests: new tests/anchorpoint-presets.spec.js (cross-renderable
contract, 72 tests, failing-first) + 9 geometry/runtime/cull tests in
sprite3d.spec.js (failing-first). Full suite 4698 passed / 0 failed.
Master merged in (CHANGELOG + the melonjs#1539-melonjs#1541 fixes).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A
|
Thanks again @asadullahbro — the vertex-bake mechanism you built was verified correct end-to-end and is kept as-is. I've pushed the rework we discussed on top of your commit ( API change: Your presets went engine-wide: the names now work everywhere Also fixed along the way:
Tests: new 🤖 Generated with Claude Code |
…mpat pins
Sprite3d (9 new adversarial cases in sprite3d.spec.js):
- anchor survives animation frame changes
- combined torture: anchor + flipX + frame change + runtime re-anchor,
then unflip — exact vertex assertions at every step
- anchor on a BOTH rotated and trimmed atlas region
- anchor round-trip (bottom → arbitrary → out-of-range → center)
restores geometry AND cull bounds byte-exact vs a fresh sprite
- single-axis anchorPoint.y = ... property writes re-bake
- out-of-range anchors: cull sphere encloses every projected corner
across billboard yaw/pitch (property invariant, 3 anchors × 3 poses)
- spherical billboard: corner distances from pos are rotation-invariant
- frame-derived sizing (no explicit width) computes offsets from the
frame grid
Sprite 2D backward compatibility (7 pins in anchorpoint-presets.spec.js):
- spritesheet preset ≡ {x, y} equivalence
- PIN of the pre-existing shared-atlas pivot inheritance (first
sprite's anchor becomes the cached pivot for later same-image
sprites) — documents the wart, guards against accidental change
- caller Vector2d mutation no longer retro-poisons the sprite or the
cache (the one deliberate safety divergence from master)
- garbage on a shared sheet keeps legacy (0, 0) for both sprites
- anchor survives 2D animation frames and flips (untrimmed pivot rule)
- Entity preset reaches entity AND child sprite; NineSliceSprite
inherits presets
Master-parity proof: the whole spec was run against master's engine
src — all 31 pure back-compat pins pass there; the 48 failures are
exactly the new-feature surface (presets, warn-on-garbage, Sprite3d's
new API) plus the documented aliasing fix. Full suite 4713 / 0.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A
An honest audit of the anchor coverage found five holes; all closed:
- FULL preset-table validation: only "bottom"/"top-left" were
value-verified — a typo'd pair in any of the other 7 presets would
have passed every test (same failure class as the CSS color table).
All 9 names now assert their documented values.
- Tiled property chain: applyTMXProperties keeps a string anchorPoint
("bottom") intact through coercion and it resolves correctly in a
renderable constructor; the legacy numeric expansion ("0.5" →
{x, y}) is pinned too — the CHANGELOG's Tiled claim is now tested.
- pool recycling: a pooled Text resolves presets on pull AND
re-resolves new settings on recycle (onResetEvent path).
- Sprite3d flipY + anchor (only flipX was covered): bottom anchor
stays pinned, unflip restores byte-exact.
- REGRESSION GUARD for the cull fix: pins that the legacy max(w, h)
bounds would NOT enclose a bottom-anchored 130×225 quad (farthest
corner 234.2 vs old radius 159.1) while the shipped bounds do —
the pop-in bug can't silently return.
anchorpoint-presets.spec.js: 93 tests; sprite3d.spec.js: 49. Full
suite green. All sprite/anchor-heavy examples verified running clean
in-browser (platformer, sprite, aseprite, text, isometric-rpg, ui,
tiled-map-loader, texture-packer — zero console/page errors).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A
|
Follow-up hardening pushed (
Full suite: 4727 passed / 0 failed. 🤖 Generated with Claude Code |
- Mesh class doc + preDraw JSDoc: note the _anchorBaked subclass opt-out (Sprite3d) — "the 2D path keeps the anchor" was no longer the whole truth - Renderable#anchorPoint property doc: document the named presets accepted by settings.anchorPoint across all consumers, and that a plain string preset now works as a Tiled property (the old note implied the json: expression was the only way) Audited: zero originPoint references remain anywhere; all 8 settings.anchorPoint @param lines carry the widened type + presets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019t8kP8vp58ZrvaD2FqJU6A
|
all done, thanks again for the PR ! |
Adds a settings.originPoint option (preset string or {x,y}) so the quad's pivot can be placed anywhere relative to pos, e.g. bottom-center for ground-anchored 2.5D characters. Baked as a constant local offset applied after flips, so it composes with billboarding and trimmed/rotated atlas frames. Default stays centered — no behavior change for existing code.
Updated the billboard example to anchor the character at its feet instead of using a manual GY offset fudge factor.
Closes #1514
Description
Type of change
Checklist
pnpm lintpasses)pnpm testpasses)pnpm build)Related issues