feat(tv): per-preset FPV player scale + camera handle fix + retro/pixel polish#64
Merged
Conversation
Adds a new public element across all three renderers for placing live iframes in 3D space, plus a five-TV demo page. - packages/polycss: PolyIframeElement (vanilla custom element). Mounts an iframe wrapper inside the parent <poly-scene>'s .polycss-scene so the CSS camera transform composes naturally with surrounding meshes. Same position/rotation/scale conventions as <poly-mesh> post-parity (world units, world-axis order, rotation conjugation rotateY(-rx) rotateX(-ry) rotateZ(-rz)). Width/height in world units; iframe centred at the wrapper's local origin so rotation/scale pivot at the visible centre. - packages/react: <PolyIframe> mirror with the same props. - packages/vue: <PolyIframe> mirror with the same props. - 14/8/7 tests added per package; transform math + attribute forwarding pinned. AGENTS.md updated with the new element + naming entry. Bug fixes shipped alongside: - PolyPerspectiveCameraElement / PolyOrthographicCameraElement runtime attribute updates now mutate the camera handle in place and re-apply the scene transform. Before, the elements either recreated the handle (orphaning the scene's pointer) or updated but never called applyCamera, so rot-x/rot-y/zoom changes were ignored at runtime. - DocsHeader hides the floating search dock on /television (matches /gallery, /builder, /wordart). /television demo: - Vertical-rail TV picker (5 sets, hand-drawn SVG art per tile). ?tv=id query param + popstate sync so individual TVs are linkable. - Per-TV polygon indices identify which face(s) are the screen; the page derives the iframe's world-unit position, rotation, width, and height from those polygons' vertices at load time (area-weighted normal, bbox in the in-plane right/up basis, small lift along the normal so the iframe doesn't z-fight with the screen polygon itself). Per-TV rotY + zoom remain hand-tuned because GLB authoring orientations differ. - Retro Stack uses multiple screen entries — one <poly-iframe> per CRT is mounted dynamically and each is placed independently.
- Rename /television route + public assets to /tv (shorter, link to it
from the docs header next to WordArt).
- Query param renamed to ?model=<id> (was ?tv=<id> — reads better
alongside /tv).
- Brighter scene lighting (directional 4.5→6.5, ambient 0.55→0.9) plus
a ground <poly-plane> below each TV with cast-shadow on the mesh, so
the bottom of every TV sits visibly on the floor.
Element changes to support the floor:
- PolyShapeElement now forwards `cast-shadow` / `receive-shadow` to
scene.add (mirrors <poly-mesh>) and `exclude-from-auto-center` so
ground planes don't skew the scene's auto-center calculation.
- PolyShapeElement gains an attributeChangedCallback so transform attrs
(position/scale/rotation/shadow) propagate after mount — the floor is
re-positioned per TV from the script side once the mesh has loaded.
- PolyPlaneElement: new `offset` attribute, defaulting to 0 so the
plane is centered at the element's local origin. The underlying
planePolygons helper defaulted to size*2 because it was authored as
a transform-control drag handle.
Camera-element runtime fix:
- PolyPerspectiveCameraElement / PolyOrthographicCameraElement
attributeChangedCallback now correctly locates the scene as a
descendant (`this.querySelector('poly-scene')`) rather than an
ancestor. The previous walk-up version always found null and
applyCamera was never called, so runtime rot-x/rot-y/zoom changes
were no-ops in vanilla.
Tuning:
- TV mesh gets `auto-center` so its bbox center IS mesh-local (0,0,0).
- Floor's per-preset Z is derived from the mesh's minZ on load.
- Per-TV cam.rotY/zoom dialed in by hand for each set (pixel TV at
rotY 245 / zoom 10 for a 3/4 view that doesn't crop).
- Retro stack screen 1: append poly 216 to the polyIndices array (user identified it as part of that CRT's bezel strip; was missing). - Floor shadow attempt: receiver-shadow on the floor + scene shadow config wired up cleanly, but the projected shadow SVG lands at wrong scene coords for our auto-centered TV + excludeFromAutoCenter floor combination. Vanilla dropped the legacy ground-shadow fallback for three.js parity, so a caster with no receiver draws nothing. Leaving the floor as a non-receiver for now with a TODO; lighting + floor geometry still ship.
The receiver-shadow SVG sits at the floor plane plus `shadow.lift` (default 0.05 world units = 2.5 CSS px). On a big 400-world-unit floor viewed at a normal-angle camera (rotX 70), that razor-thin gap z-fought with the floor itself and the shadow only showed from straight overhead. A 1 world unit (50 CSS px) lift puts the shadow clearly in front at every angle the camera reaches.
The first CRT's actual screen polygons are 212–216 — 211 is an adjacent bezel face the iframe-placement code was averaging into the plane, shifting the iframe off the screen. Restricting to 212–216 lands the iframe on the visible glass.
placementFromPolygons now takes an optional liftOverride; the TvScreen config exposes it as `lift`. The retro stack's CRT screens already model the front glass surface, so a small negative lift (−0.6 world units) seats the iframe inside the bezel instead of in front of it. The default lift formula (0.2 + 0.01 * max(w,h)) still applies for every other TV — those polygons mark the screen plane itself, so the iframe sitting slightly in front of them reads as the picture surface.
The 'floating quad' I attributed to receive-shadow earlier turned out to be the monitor.glb's back panel authored at an offset position — present even without receive-shadow. Self-shadows on textured receivers are computed correctly but the texturedReceiver code reduces opacity to mimic three.js's 'darken to ambient-only' (`effOp = opacity * (1 - (ambient/total)^(1/2.4))`). With our bright light (dir 6.5, ambient 0.9) that pushed shadow alpha to ~0.32, which read as nearly-invisible 'white-ish' on textured TVs. Bumping the input opacity to 0.95 lands the effective alpha at ~0.55 and the self-shadows now read as actually dark.
…ead dark - Lift 1 → 0.1 world units. The bigger value was detaching shadows visibly from their receivers; 0.1 (5 CSS px) still clears z-fighting but reads as flush with the floor / bezel. - Ambient intensity 0.9 → 0.4. The texture-receiver code caps shadow alpha at `opacity * (1 - (ambient/total)^(1/2.4))` to mimic three.js's 'darken to ambient-only' lighting model. With ambient=0.9 the cap pushed effective shadow alpha to ~0.32 — visibly faint / 'white-ish' on the TV's own faces. Lower ambient = darker possible shadows; 0.4 hits the polycss default and lets self-shadows read as properly dark.
- Bottom-centre toggle switches between <poly-orbit-controls> and <poly-first-person-controls>. FPV mounts with WASD/jump/look and a hint label appears on the right side of the pill while active. - Orbit zoom range widened to [1, 200] so users can zoom out far past each TV's default zoom.
- Drop receive-shadow from the TV mesh: the texture-receiver darkening reads as 'white-ish' (capped at ambient brightness) and visible self-shadows weren't worth the cost. - FPV mode now matches the gallery's defaults: move-speed 30, jump-velocity 25, gravity 60, eye-height 6, look-sensitivity 0.15. Also switches camera perspective to 2000 while FPV is active (the same FPV_PERSPECTIVE the gallery and builder use) for a wider FOV; restores to 32000 when going back to Orbit.
…pawn) Entering FPV used to drop the camera at scene origin (where the TV sits, since the scene is auto-centered) — so the player was INSIDE the TV with a face full of bezel. Now matching gallery's spawn behaviour: - Save + drop scene's auto-center attribute on entry, restore on exit. - Compute TV bbox from the loaded mesh handle, derive a spawn target one mesh-span behind the TV along the current rotY look direction, set it on the camera so the controls' initializeOriginFromTarget uses that as the seed. - Remove target on exit so orbit returns to its scene-origin pivot.
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.
Summary
Post-merge polish for the
/tvshowcase (PR #62 landed the page itself). Adds the FPV camera that makes each TV preset feel right and fixes a latent bug in<poly-perspective-camera>that was hiding the issue.<poly-perspective-camera>fix: mutating theperspectiveattribute at runtime was recreating the camera handle, orphaning the scene's captured reference. Every subsequentrot-x/rot-y/zoom/targetupdate silently no-op'd because they were writing to a dead handle. Now updates the wrapper's CSSperspectivein place. Pinned by 5 new tests (725 total polycss tests pass).1/2and pixel-tv uses1/3so those TVs read as bigger from the player's POV, without scaling the mesh DOM (large CSS scales caused compositor flicker). Cabinet / electronic / monitor keepplayerScale = 1and the prior framing.rot-xis computed from look geometry (atan2(midZ - cameraZ, back)) — small players look horizontally, tall players tilt down. Replaces the hardcoded 75° that missed the TV when the camera was short.<poly-first-person-controls>so it re-seedscameraOriginfrom the new mesh's bbox instead of staying anchored to the previous TV.target="0,0,0"on exit;removeAttribute("target")alone left the camera handle anchored to the FPV position (parseVec3(null)→undefinedis ignored by the update path).1(turn-into-mouse).Also includes the squashed batch of TV-page tweaks since #62 merged: receive-shadow exploration, shadow tuning, floor color match to /gallery, per-screen
liftoverride for the retro stack's recessed bezels, polygon-index fixes for the retro stack screens, per-CRT YouTube videos, and the Orbit/FPV camera-mode pill.Test plan
pnpm test(725 polycss + react/vue/core all green)pnpm build(all packages + website)/tvorbit view renders each preset with iframe on the screen polygon