feat(export): baked GLB export with identity, clips, cutout fix (phases 0-1)
Promote the client GLB export into the baked-artifact format from
plans/editor-baked-glb-export.md (phases 0 and 1).
- NodeMaterial -> classic MeshStandardMaterial conversion at export. The
viewer's MeshStandard/LambertNodeMaterial set isNodeMaterial, not
isMeshStandardMaterial, so GLTFExporter would otherwise drop every
surface to a blank default. KTX2 (compressed) maps are decompressed via
WebGPUTextureUtils so the exporter can embed them (PNG for now; KTX2
re-encode is deferred to the phase-3 bake worker).
- Identity stamping from sceneRegistry: node.name = pascalId and
extras = { pascalId, kind, label?, openable?, clips? }; all other
userData stripped so editor/runtime ephemera never reach glTF extras.
- Door/window open clips baked from the build-once + pose-at-t primitives
(door pascalSwingLeaf marker, window poseWindowMovingParts). Clips named
by label ("Door 1: open"), carry extras.loop = false (consumers play
once and hold; dumb glTF players still loop).
- Cutout fix: door/window selection hitboxes hide via material.visible,
which onlyVisible misses, so the hitbox box plugged the wall opening.
Non-renderable container meshes now keep their node but lose geometry;
childless ones are removed.
- Editor-overlay stripping mirrors the thumbnail capture: emit
thumbnail:before/after-capture so scene-layer affordances (handles,
ceiling/site brackets) self-hide, and drop anything off SCENE_LAYER
(gizmos, grid, zone fills).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
b2f1a8432e
commit
06e4cd7748
@@ -157,6 +157,9 @@ export { getVisibleWallMaterials } from './systems/wall/wall-materials'
|
||||
// 800+ lines of CSG / mitering logic during Phase 3. These exports are
|
||||
// removed in Phase 6 when the legacy mount points are deleted.
|
||||
export { WallSystem } from './systems/wall/wall-system'
|
||||
export { WindowAnimationSystem } from './systems/window/window-animation-system'
|
||||
export {
|
||||
poseWindowMovingParts,
|
||||
WindowAnimationSystem,
|
||||
} from './systems/window/window-animation-system'
|
||||
export { buildWindowPreviewMesh, WindowSystem } from './systems/window/window-system'
|
||||
export { ZoneSystem } from './systems/zone/zone-system'
|
||||
|
||||
@@ -1092,6 +1092,7 @@ function addDoorLeaf(
|
||||
hingeX,
|
||||
hingeSide,
|
||||
swingRotation,
|
||||
openRotationY,
|
||||
segments,
|
||||
contentPadding,
|
||||
handle,
|
||||
@@ -1117,6 +1118,10 @@ function addDoorLeaf(
|
||||
hingeX: number
|
||||
hingeSide: 'left' | 'right'
|
||||
swingRotation: number
|
||||
// Leaf rotation (radians, about the hinge Y axis) at fully-open. The GLB
|
||||
// exporter reads this off the leaf group to bake an open/close clip; it is
|
||||
// the kinematic endpoint, independent of the current `swingRotation`.
|
||||
openRotationY: number
|
||||
segments: DoorNode['segments']
|
||||
contentPadding: DoorNode['contentPadding']
|
||||
handle: boolean
|
||||
@@ -1147,6 +1152,10 @@ function addDoorLeaf(
|
||||
const leafGroup = new THREE.Group()
|
||||
leafGroup.position.set(hingeX, 0, 0)
|
||||
leafGroup.rotation.y = swingRotation
|
||||
// Marks this group as the swing leaf and records its fully-open angle so the
|
||||
// GLB exporter can bake an open/close animation clip from a single pose. The
|
||||
// exporter strips this marker before writing the file.
|
||||
leafGroup.userData.pascalSwingLeaf = { axis: 'y', openRotationY }
|
||||
mesh.add(leafGroup)
|
||||
|
||||
const addLeafBox = (
|
||||
@@ -2461,6 +2470,7 @@ function updateDoorMesh(rawNode: DoorNode, mesh: THREE.Mesh) {
|
||||
hingeX: -insideWidth / 2,
|
||||
hingeSide: 'left',
|
||||
swingRotation: -clampedSwingAngle * swingDirectionSign,
|
||||
openRotationY: (-Math.PI / 2) * swingDirectionSign,
|
||||
segments,
|
||||
contentPadding,
|
||||
handle,
|
||||
@@ -2489,6 +2499,7 @@ function updateDoorMesh(rawNode: DoorNode, mesh: THREE.Mesh) {
|
||||
hingeX: insideWidth / 2,
|
||||
hingeSide: 'right',
|
||||
swingRotation: clampedSwingAngle * swingDirectionSign,
|
||||
openRotationY: (Math.PI / 2) * swingDirectionSign,
|
||||
segments,
|
||||
contentPadding,
|
||||
handle,
|
||||
@@ -2520,6 +2531,7 @@ function updateDoorMesh(rawNode: DoorNode, mesh: THREE.Mesh) {
|
||||
hingeX,
|
||||
hingeSide: hingesSide,
|
||||
swingRotation: clampedSwingAngle * swingDirectionSign * hingeDirectionSign,
|
||||
openRotationY: (Math.PI / 2) * swingDirectionSign * hingeDirectionSign,
|
||||
segments,
|
||||
contentPadding,
|
||||
handle,
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
type WindowNode,
|
||||
} from '@pascal-app/core'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import type { Object3D } from 'three'
|
||||
import {
|
||||
AWNING_WINDOW_SASH_NAME,
|
||||
CASEMENT_WINDOW_SASH_NAME,
|
||||
@@ -28,12 +29,20 @@ function markWindowDirty(windowId: AnyNodeId) {
|
||||
scene.dirtyNodes.add(windowId)
|
||||
}
|
||||
|
||||
function applyDirectWindowAnimation(windowId: AnyNodeId, value: number) {
|
||||
const node = useScene.getState().nodes[windowId]
|
||||
if (node?.type !== 'window') return false
|
||||
|
||||
const mesh = sceneRegistry.nodes.get(windowId)
|
||||
|
||||
/**
|
||||
* Pose a window's moving parts (sash/panel/slats) at `value` (0 = closed,
|
||||
* 1 = open) by mutating the named child groups under `mesh`. Returns true when
|
||||
* the window type has a direct pose path and the named parts were found.
|
||||
*
|
||||
* This is the single source of truth for window kinematics: the live animation
|
||||
* system poses the registered scene mesh, and the GLB exporter poses an export
|
||||
* clone to sample the open/close keyframes for a baked animation clip.
|
||||
*/
|
||||
export function poseWindowMovingParts(
|
||||
node: WindowNode,
|
||||
mesh: Object3D | undefined,
|
||||
value: number,
|
||||
): boolean {
|
||||
if (node.windowType === 'sliding') {
|
||||
const activePanel = mesh?.getObjectByName(SLIDING_WINDOW_ACTIVE_PANEL_NAME)
|
||||
if (!activePanel) return false
|
||||
@@ -120,6 +129,12 @@ function applyDirectWindowAnimation(windowId: AnyNodeId, value: number) {
|
||||
return false
|
||||
}
|
||||
|
||||
function applyDirectWindowAnimation(windowId: AnyNodeId, value: number) {
|
||||
const node = useScene.getState().nodes[windowId]
|
||||
if (node?.type !== 'window') return false
|
||||
return poseWindowMovingParts(node, sceneRegistry.nodes.get(windowId), value)
|
||||
}
|
||||
|
||||
export const WindowAnimationSystem = () => {
|
||||
useFrame(({ clock }) => {
|
||||
const interactive = useInteractive.getState()
|
||||
|
||||
Reference in New Issue
Block a user