This repository was archived by the owner on May 19, 2026. It is now read-only.
Builder workbench + dock primitives + FPV refinements#65
Merged
Conversation
…FPV spike - DebugWorkbench.tsx (5977 LOC) → GalleryWorkbench.tsx (702 LOC) plus scoped subfolders: types.ts, presets/, helpers/, hooks/. - New components/ folders: AttributionCredit, Dock, DropOverlay, Inspector, ModelsSidebar, ReactScene, StatsOverlay, VanillaScene, GalleryWorkbench — each Folder/Folder.tsx + index.ts barrel. - Drop deprecated src/debug/ tree and src/pages/debug/ subroutes; gallery is the only debug surface now. - New createPolyFirstPersonControls in @layoutit/polycss: WASD/arrow walk, pointer-lock mouselook, Space jump, Ctrl crouch, per-axis enable flags. Wired into the gallery as a third dragMode option. Walking + mouselook still need calibration — that lands in a follow-up.
…text The polycss scene element owns its own `perspective` CSS property, but `.dn-vanilla-host` is `transform-style: flat` — so when target translates in Z, the scene wrapper moves invisibly and polygons inside never get re-foreshortened. Adding `perspective: 2000px; transform-style: preserve-3d` to the host (scoped to FPV via `data-camera-mode="fpv"` on .dn-root) unlocks real 3D walking: WASD now actually approaches/recedes from the model, not just slides the scene in 2D. Also: spawn FPV camera one mesh-span behind the model (instead of inside it) along the orbit's previous look direction.
The FPV controls now maintain an internal `cameraOrigin` (camera world position) and DERIVE the scene's `target` as target = cameraOrigin + lookDir * (perspective / tile) so the polycss perspective viewer mathematically coincides with the camera origin in world space. Mouselook rotates `target` around the FIXED `cameraOrigin` (in-place head rotation), instead of orbiting some point in front of the camera. WASD moves `cameraOrigin` (target follows via the same offset). This matches three.js PointerLockControls semantics exactly. Also expose getOrigin/setOrigin on the FPV handle so callers (tests, spawn hooks, teleporters) can read/write the camera position directly without having to invert the derived-target math. Bonus: expose more perspective values in the gallery dropdown (500 / 16k / 32k / 64k px) so it's easier to find a comfortable foreshortening.
FPV's lookOffset = perspective / tile assumes the scene-element perspective and the host-element perspective are the same number. With the host CSS hardcoded to 2000px, raising sceneOptions.perspective (e.g. to 64000) pushed target onto a 1280-world-unit lever arm while the host viewer plane stayed at 2000px. Result: the scene flew past the camera in Z, and pitching on mouselook caused dramatic content drift instead of in-place rotation. Fix: expose --fpv-perspective on .dn-root (driven by sceneOptions.perspective) and have the FPV-scoped host rule read it. Now host and scene agree on where the eye is, so mouselook stays in-place at any perspective value.
Native implementations of the FPV controls in @layoutit/polycss-react and @layoutit/polycss-vue, matching the vanilla createPolyFirstPersonControls behavior: - cameraOrigin maintained internally; target derived as origin + lookDir * (perspective/tile) so the perspective viewer sits at origin in world space (in-place mouselook). - WASD walks the origin; jump/crouch shift origin.z; mouselook on pointer- lock rotates target around the fixed origin. - Same prop names, defaults, and handle shape (getOrigin/setOrigin/lock/ unlock/update/etc.) across vanilla / react / vue. React: 17 new tests, total 329 pass. Vue: 17 new tests, total 329 pass. Both packages depend only on @layoutit/polycss-core, matching the existing controls pattern.
Add direct assertions for the core FPV invariant target = origin + lookDir(rotX, rotY) * (perspective / BASE_TILE) plus the mouselook-keeps-origin-fixed property (in-place rotation, not orbit). The behavior tests prior covered movement direction but didn't encode the math identity as a unit-test failure — these do. Coverage: - polycss: +5 (numerical identity under attach, yaw sweep, pitch sweep, origin-fixed during a 30-step mouselook, lookOffset scales with sceneOptions.perspective). - react: +2 (setOrigin matches getOrigin, target sits at perspective/tile). - vue: +2 (same shape, plus checking |target - origin| across 500/2000/16k perspective values). Totals: polycss 370 (+5), react 331 (+2), vue 331 (+2). Build green.
A new gallery-style workbench where users add presets from a sidebar,
drag them around with a transform gizmo, and remove them with Delete.
Reuses the gallery's components (ModelsSidebar, StatsOverlay) and
preset library verbatim.
State shape: placedItems[] = {id, preset, polygons, position, rotation,
scale}. Sidebar click = load preset + push. Click mesh or outliner row
to select. Selected item gets a PolyTransformControls in translate
mode; onObjectChange flows through into placedItems.
Known v1 limitations (follow-ups):
- Per-mesh scale isn't normalized; raw OBJ vertex extents differ wildly
across presets so the user may need to scale them manually.
- 'Focus on selected' (re-frame the orbit target onto a clicked item)
isn't wired up yet.
- Only translate mode; rotate/scale gizmo toggle TBD.
- Layout serialization (URL/localStorage) TBD.
Two bugs in v1: 1. Mesh sizing — parser `targetSize` isn't applied consistently across OBJ/GLB/VOX, so Cat dwarfed Apple. Now every loaded mesh is rebbox'd and scaled to NORMALIZED_MAX_DIM world units after parse, regardless of how the source was authored. 2. Placement coordinate system — PolyMesh.position is raw CSS-px (translate3d) while the orbit target was passed in world units, so the spawn was mixing scales. Now placements happen on a 3-wide world XY grid, with worldToCssPosition() applying polycss's world→CSS axis swap (world Y → CSS X, world X → CSS Y, *BASE_TILE) before handing off to PolyMesh. Result: adding Apple/Bread/Cat produces a neat row of equally-sized meshes instead of a pile of vastly different scales.
Layout: - Scene outliner moved from bottom-left to top-right. - Selection panel split into its own block below the outliner. Shows gizmo-mode toggle (Translate / Rotate), scale slider (0.1×–5×, written directly to PolyMesh.scale prop), and position/rotation readouts. - Camera-mode pill at bottom-center: Orbit / Pan / FPV. Conditionally renders PolyOrbitControls / PolyMapControls / PolyFirstPersonControls. Scale: PolyTransformControls only supports translate + rotate, so scale isn't part of the gizmo — it's a slider that writes the mesh's scale prop directly. Range 0.1–5×, step 0.05. The selection panel only renders when an item is selected. Outliner remains visible at all times (or shows an empty-state hint).
Pass selected.scale through to PolyTransformControls.size so the translate-arrows / rotate-rings track the visible footprint of the mesh. Without this a scaled-up mesh hides the gizmo inside it, and a scaled-down mesh has a gizmo bigger than the mesh itself. size is a raw multiplier on gizmo geometry — same axis-length unit the mesh uses for its scale prop, so 1:1 keeps them in sync.
The user reported textured meshes 'not loading textures'. Debugging via Playwright showed 30 <s> sprite tags with valid blob URLs in the DOM — textures DO load. The issue was that NORMALIZED_MAX_DIM=4 world units plus DEFAULT_ZOOM=0.18 gave roughly 36 CSS-px of mesh on screen, where bitmap textures degrade to a muddy color blob. - Normalize meshes to 8 world units (was 4) — 2× bigger render. - DEFAULT_ZOOM 0.3 (was 0.18). - Grid step bumped to 10 to keep the 25% gap around larger meshes. Also: textures work fine when polygons have polygon.texture set (which loadPresetModel does); no rendering pipeline change was needed. Replace vertex-rewrite normalization with PolyMesh.scale composition (fitScale × user scale) so texture triangles / atlas refs stay attached to their original vertex positions — the earlier vertex-rewrite path would have desynced atlas UVs from vertices.
DocsHeader hides the sidebar-search-dock on /gallery (workbench layout) because Starlight's floating search icon collides with the workbench's own search input. /builder uses the same layout pattern — extend the guard to also match /builder, matching the gallery's behavior exactly.
Builder now uses the same Dock component as the gallery, with all its Model / Rendering / Interaction / Camera / Lighting folders. State shape is a complete SceneOptionsState wired through to the React renderer: - Camera mode (orbit/pan/fpv) lives in the Dock's Camera folder; the custom builder pill is gone. - Lighting (azimuth, elevation, color, intensity, ambient) goes through directionalFromOptions / ambientFromOptions like the gallery. - Texture lighting, quality, edge-repair, strategies all controllable. - Auto-center, show axes, show light helper all work. - FPV with proper origin-tracking, perspective, and the rest still work via the Dock's FPV sub-folder. The only builder-specific UI: - Scene outliner at the very top-right (above the Dock) — list of placed items + delete-on-click. - Selection panel inline inside the outliner — Translate/Rotate gizmo toggle + per-mesh scale slider (PolyTransformControls doesn't support scale gizmo, so it's a slider). Stubs supplied to Dock for animation / loaded-model / metrics / preset fields that don't apply to the multi-mesh builder workflow.
Dock now accepts an optional topSlot ReactNode that renders INSIDE the .dn-floating-controls container, above the lil-gui folders. The builder passes its Scene outliner (placed-items list + per-mesh gizmo toggle + scale slider) via this slot. Result: one unified right-column panel with Scene → Model → Rendering → Interaction → Camera → Lighting, sharing the same chrome / scroll context. No more visually-different secondary panel floating above the Dock; the builder reads like the gallery with an extra section on top. Gallery doesn't pass topSlot — its layout is unchanged.
…ng/Animation/Interaction/Camera/Lighting/Scene)
…bench-and-fpv-controls # Conflicts: # website/src/components/Dock/Dock.tsx # website/src/components/Dock/index.ts # website/src/components/GalleryWorkbench/GalleryWorkbench.tsx # website/src/components/GalleryWorkbench/helpers/cssValues.ts # website/src/components/GalleryWorkbench/helpers/domMetrics.ts # website/src/components/GalleryWorkbench/helpers/interiorFill.ts # website/src/components/GalleryWorkbench/hooks/useScenePolygons.ts # website/src/components/ReactScene/ReactScene.tsx # website/src/components/VanillaScene/VanillaScene.tsx # website/src/components/types.ts
…fpv-controls # Conflicts: # website/src/components/Dock/Dock.tsx
…ring/Animation/Interaction/Camera/Lighting/Scene children
…n-slab homography
…o's working pattern)
…, matches gizmo handle approach
…der in any orientation
…sis chooser keeps it 3D
… flattened the 3D context
… + add tool palette UI
… (moved to geometry/ and slots/)
…-workbench-and-fpv-controls # Conflicts: # website/src/components/BuilderWorkbench/BuilderWorkbench.tsx # website/src/components/BuilderWorkbench/builder-workbench.css
…nd visible elevated cells
…, neighbouring cells form ramps
…ar tris close gaps)
…e-runs don't reset them
… terrain, not below it
…ed vertices, drop separate fill mesh
…er/snap respect it
…round bbox centre
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 subscribe to this conversation on GitHub.
Already have an account?
Sign in.
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
Dockprimitives (useGui,useFolder,addToggle/Slider/Color/Option/Button) and lifted each domain (camera, lighting, rendering, model, animation) into its own folder hook. Per-page dock composition now picks the slots it needs — Gallery and Builder consume the same primitives./builder): click-to-place from a kits sidebar (City Kit / Urban Pack / Medieval Village) with cursor-tracked dotted ghost wireframe, composable scene presets (City Block, City Roads), floating tool palette (Pointer / Raise / Lower / Smooth), Vertex/Face target toggle, and an Orbit/Pan camera-mode pill (Cmd-hold to temporarily pan on Mac)..polycss-fpv-hostperspective injection (React + Vue + vanilla mirrors).Coverage
core/polycss/react/vue) — full vitest suites pass; 7 of the 35 package files in this PR are tests (camera, cull, parser, dock helpers)./builderwork is exercised by hand.pnpm test && pnpm buildare both green.Test plan
/builderloads, kits sidebar populated; click-to-place lands a ghost-previewed mesh on the floor./builder*.