Skip to content

fix: resolve plant elevation through the host floor resolver - #1

Merged
Aymericr merged 1 commit into
mainfrom
fix/inherit-terrain-elevation
Aug 2, 2026
Merged

fix: resolve plant elevation through the host floor resolver#1
Aymericr merged 1 commit into
mainfrom
fix/inherit-terrain-elevation

Conversation

@Aymericr

@Aymericr Aymericr commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Problem

Trees, flowers and grass read node.position[1] raw when writing their transforms, so every plant sat at y = 0 — floating underneath a raised deck, and buried inside a sculpted hillside.

Why the host didn't handle it

Stored plant positions are flat by contract ([x, 0, z]) — the surface a plant stands on is resolved at render time, and the lift is presentation that is never committed.

For an ordinary per-node kind the host does this for free. But these kinds use a collective instanced renderer, and the host's FloorElevationSystem writes only to a node's registered object — which for an instanced kind is the invisible selection proxy, not the instances that are actually drawn. So an instanced kind has to resolve the lift itself, everywhere it writes a transform.

Change

New elevation.ts holds the seam, resolving through the host's getFloorStackedPosition so plants inherit slab election and sculpted terrain without knowing either exists:

  • plantElevation — committed nodes. Used by the instance matrices in instanced.tsx, and by the proxy's box collider. The collider is a positioned sibling of the registered group (so the outline pass traces the plant's true silhouette instead of a box), which puts it outside the host's reach — without this, the hit volume stayed at the storey plane while the plant it stands for rode a deck or a hillside.
  • draftElevation — the placement ghost. A draft is unparented, so the level it will land on has to be named explicitly (the resolver reads parentId first and only falls back to levelId). A ghost that skipped this floated at the storey plane over a deck and sank into every slope, so the plant appeared to jump on click.

Both live in a module free of any Three.js or React import, so the seam is testable without a canvas.

Depends on

Host support landed in pascalorg/editor#568 — the generic levelBaseElevationAt / ctx.levelBaseAt seam, where asking for the ground is what enrolls a kind in following it. This PR is that seam's first plugin consumer, so it doubles as the reference for how a third-party plugin inherits terrain.

Testing

elevation.test.ts (new) — 11 pass, 0 fail against the merged host core. Covers all three kinds resting on bare ground, riding a deck slab, and standing beyond the deck's footprint (the lift is per-position, not global), plus both ghost cases including an unresolvable level, which must stay flat rather than throw.

Worth noting: every assertion in the suite reads 0 under the old code. In a flat test scene the broken and correct implementations agree exactly — which is why this survived so long, and why the fixture builds an explicit deck and lets the spatial grid settle before asserting.

🤖 Generated with Claude Code

Trees, flowers and grass read `node.position[1]` raw when writing their
transforms, so every plant sat at `y = 0` — floating under a raised deck
and buried in a sculpted hillside.

Stored plant positions are flat by contract (`[x, 0, z]`); the surface a
plant stands on is resolved at render time. For an ordinary per-node kind
the host does that for free, but these kinds use a collective instanced
renderer, and the host's `FloorElevationSystem` only writes to a node's
*registered* object — which here is the invisible selection proxy, not
the instances. So an instanced kind has to resolve the lift itself, at
every point it writes a transform.

`elevation.ts` adds that seam, resolving through the host's
`getFloorStackedPosition` so plants inherit slab election and terrain
without knowing either exists:

- `plantElevation` — for committed nodes, used by the instance matrices
  and by the proxy's box collider. The collider is a positioned sibling
  of the registered group (so the outline traces the true silhouette
  rather than a box), which puts it outside the host's reach; without
  this its hit volume stayed at the storey plane while the plant rode a
  deck or a hillside.
- `draftElevation` — for the placement ghost, which is unparented and so
  must name its level explicitly. A ghost that skipped this floated at
  the storey plane, making the plant jump on click.

Kept free of any Three.js or React import so the seam is testable
without a canvas. `elevation.test.ts` covers all three kinds on bare
ground, on a deck, and beyond the deck's footprint, plus both ghost
cases — every assertion reads 0 under the old code, which is why a flat
test scene never caught this.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@Aymericr
Aymericr merged commit f054f88 into main Aug 2, 2026
1 check passed
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