fix: resolve plant elevation through the host floor resolver - #1
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Trees, flowers and grass read
node.position[1]raw when writing their transforms, so every plant sat aty = 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
FloorElevationSystemwrites 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.tsholds the seam, resolving through the host'sgetFloorStackedPositionso plants inherit slab election and sculpted terrain without knowing either exists:plantElevation— committed nodes. Used by the instance matrices ininstanced.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 readsparentIdfirst and only falls back tolevelId). 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.levelBaseAtseam, 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
0under 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