Skip to content

fix(parity): builder, ground shadow, gizmo wrapper, wordart, doc demos#61

Merged
apresmoi merged 3 commits into
mainfrom
fix/builder-and-gizmo-parity
Jun 4, 2026
Merged

fix(parity): builder, ground shadow, gizmo wrapper, wordart, doc demos#61
apresmoi merged 3 commits into
mainfrom
fix/builder-and-gizmo-parity

Conversation

@apresmoi
Copy link
Copy Markdown
Collaborator

@apresmoi apresmoi commented Jun 4, 2026

Summary

Six interactive surfaces broke during the Three.js parity sweep (#60) — none caught by the test suite. All share the same root cause: the renderers post-parity speak world units in world-axis order (and the camera's zoom is px-per-world-unit), but a handful of call sites still spoke the old CSS-pixel/CSS-axis dialect.

  • Builder placement + zoom. placeMeshOnFloor returned CSS-pixel positions in CSS-axis order; the unitless zoom slider was passed directly to the camera (rendered at 0.006× CSS scale); projectScreenToWorldGround inverted 1/zoom instead of BASE_TILE/zoom. Mirrored gallery's LEGACY_ZOOM_COMPAT pattern, flipped placement to world units in world-axis order, and fixed the inverse scale.
  • React + Vue ground shadow lifted by the mesh. The per-mesh shadow SVG is mounted inside .polycss-mesh, which now translates by position * BASE_TILE. Pre-parity that was effectively a no-op; post-parity it lifted the shadow by the mesh's world Z, so it landed at the cube's vertical midpoint instead of the floor. Compensated by subtracting meshPosZ * BASE_TILE from the projection target's Z.
  • Gizmo wrapper position (all three renderers). The wrapper was emitting translate3d(position[0]px, position[1]px, position[2]px) for what's now a world-unit / world-axis vector — gizmo sat near scene origin instead of on the mesh. Apply the world→CSS axis swap + ×BASE_TILE in all three renderers.
  • Vanilla translate-drag math. applyAxisDelta was adding a CSS-px t directly to a world-unit position at the wrong index. Divide by SCENE_TILE_SIZE and write to WORLD_AXIS_FOR_CSS[cssAxis]. Same fix for the plane-drag callback.
  • Vanilla rotation gizmo. Each ring now rotates around the world axis it visually wraps (was the CSS-axis index, so the wrong axis spun), snapshots the start position, and re-translates each tick so the visible bbox center stays fixed — matches Three.js's pivot-around-center feel without forcing callers to pre-center via loadMesh({ center: true }). gizmoPosition applies the current rotation to centerOffset so the gizmo follows the spinning mesh. Single global sign flip to match the screen-drag direction users expect.
  • WordArt zoom. Same shape as the builder — fitZoom * zoomScale is a unitless CSS-scale value; multiply by BASE_TILE at the camera boundary.
  • Doc PolyDemo lighting. PolyDemo.astro was writing light-direction / light-ambient / light-color attributes on <poly-scene>, but the element reads directional-direction / ambient-intensity / directional-color. The demo's lighting config was silently ignored on every docs page, falling back to renderer default intensity 1 (very dim under the post-parity physical-Lambert /π shading). Renamed the attributes and bumped the default to intensity: 4.5, ambient: 0.55 (gallery's defaults). Generator-mode path also splits state.light into directionalLight + ambientLight for the imperative API.

Test plan

  • /builder — place several primitives, verify they snap to the click point and stack on top faces; floor shadow stays at floor; Z-gizmo sits on the mesh
  • /wordart — renders at the same on-screen size as pre-parity
  • /gallery with Mesh selection + Scene interactive enabled — gizmo centers on the selected mesh; translate arrows track the mouse; rotation rings rotate around the visible center in the dragged direction
  • /guides/projections/ — the tree GLB now renders bright (was very dark)
  • Spot-check the other docs PolyDemos (/quickstart, /core-concepts, /guides/{performance,shapes,textures}/) for the new lighting defaults

apresmoi added 3 commits June 4, 2026 02:15
…sition

Three follow-up bugs from the three.js parity sweep:

1. Builder zoom + placement. The builder still spoke pre-parity
   semantics: passed the unitless slider zoom directly to the camera
   (rendered the scene at 0.006× CSS scale), `placeMeshOnFloor`
   returned CSS-pixel positions in CSS-axis order, and
   `projectScreenToWorldGround` inverted with `1/zoom` instead of
   `BASE_TILE/zoom`. Mirrored gallery's `LEGACY_ZOOM_COMPAT` pattern,
   flipped placement to world units in world-axis order, and fixed the
   inverse scale.

2. React + Vue ground shadow lifted by the mesh. The per-mesh ground
   shadow SVG is rendered inside the `.polycss-mesh` wrapper, which
   translates by `position * BASE_TILE`. Pre-parity that was a no-op
   (position was near zero); post-parity the wrapper now lifts the
   shadow by the mesh's world Z, so it landed at the cube's vertical
   midpoint instead of the floor. Subtract `meshPosZ * BASE_TILE`
   from the projection target's Z so the wrapper translate cancels.

3. TransformControls wrapper placement + drag math. The gizmo wrapper
   was emitting `translate3d(position[0]px, position[1]px,
   position[2]px)` for a position that's now world units in world-axis
   order — so the gizmo sat near scene origin instead of on the mesh.
   Apply the world→CSS axis swap + ×BASE_TILE in all three renderers.
   Vanilla's axis-drag math also added a CSS-px `t` directly to a
   world-unit position with the wrong axis index; divide by
   `SCENE_TILE_SIZE` and write to `WORLD_AXIS_FOR_CSS[cssAxis]`.
   Vanilla rotation gizmo now uses the world axis the ring visually
   wraps, snapshots the start position, and re-translates each tick so
   the visible bbox center stays put (matches three.js's pivot-around-
   center feel without requiring callers to pre-center via
   `loadMesh({ center: true })`). `gizmoPosition` now applies the
   current rotation to `centerOffset` so the gizmo follows the
   spinning mesh.
WordArt passed the legacy unitless zoom slider value (range ~0.001–1.2)
directly to the camera, which post-parity is px-per-world-unit — scenes
rendered tiny. Multiply by BASE_TILE at the camera boundary so the
existing slider range produces the same on-screen scale as before.

PolyDemo was writing `light-direction` / `light-ambient` / `light-color`
attributes on `<poly-scene>`, but the element reads `directional-direction`
/ `ambient-intensity` / `directional-color` (etc.) — the demo's lighting
config was silently ignored on every docs page, falling back to the
renderer default intensity 1, which is very dim under the post-parity
physical-Lambert (/π) shading. Rename the attributes and bump the default
when callers don't override (4.5 directional, 0.55 ambient — same shape
as Gallery DEFAULT_SCENE). Generator-mode path also splits `state.light`
into `directionalLight` + `ambientLight` for the imperative API.
Six test cases were pinned to the pre-parity drag math and bbox-center
formula:

- Vanilla `applyAxisDelta` now divides the CSS-px `t` by SCENE_TILE_SIZE
  and writes to WORLD_AXIS_FOR_CSS[cssAxis] (world axis), so dragging
  the X arrow moves `position[1]` (world Y) by t/50 — not `position[0]`
  by t CSS px. Three position-assertion tests updated.

- Vanilla rotation: the X ring is built around WORLD_AXIS_FOR_CSS[0] =
  world Y, so the test now expects `rotation[1]` (not [0]) to be the
  changed component, with X and Z magnitudes near zero. Renamed from
  '(X-axis inverted)' to '(around world Y)' to reflect the new behavior.

- React TransformControls wrapper now applies worldPositionToCss before
  adding the CSS-pixel bbox center. Two wrapper-transform assertions
  updated for the new pixel-space output.
@apresmoi apresmoi merged commit e253797 into main Jun 4, 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