2026-04-05 02:25:08 +02:00
import { jsx as _jsx , jsxs as _jsxs , Fragment as _Fragment } from "react/jsx-runtime" ;
import { useEffect , useRef , useState } from "react" ;
import { createCreateBoxBrushCommand } from "../commands/create-box-brush-command" ;
import { createDeleteBoxBrushCommand } from "../commands/delete-box-brush-command" ;
import { createDeleteEntityCommand } from "../commands/delete-entity-command" ;
2026-04-05 04:23:39 +02:00
import { createDuplicateSelectionCommand } from "../commands/duplicate-selection-command" ;
2026-04-05 02:25:08 +02:00
import { createImportAudioAssetCommand } from "../commands/import-audio-asset-command" ;
import { createImportBackgroundImageAssetCommand } from "../commands/import-background-image-asset-command" ;
import { createImportModelAssetCommand } from "../commands/import-model-asset-command" ;
import { createDeleteModelInstanceCommand } from "../commands/delete-model-instance-command" ;
import { createCommitTransformSessionCommand } from "../commands/commit-transform-session-command" ;
import { createMoveBoxBrushCommand } from "../commands/move-box-brush-command" ;
import { createRotateBoxBrushCommand } from "../commands/rotate-box-brush-command" ;
import { createResizeBoxBrushCommand } from "../commands/resize-box-brush-command" ;
import { createSetBoxBrushFaceMaterialCommand } from "../commands/set-box-brush-face-material-command" ;
import { createSetBoxBrushNameCommand } from "../commands/set-box-brush-name-command" ;
import { createSetEntityNameCommand } from "../commands/set-entity-name-command" ;
import { createSetBoxBrushFaceUvStateCommand } from "../commands/set-box-brush-face-uv-state-command" ;
import { createDeleteInteractionLinkCommand } from "../commands/delete-interaction-link-command" ;
import { createSetModelInstanceNameCommand } from "../commands/set-model-instance-name-command" ;
import { createSetSceneNameCommand } from "../commands/set-scene-name-command" ;
import { createSetWorldSettingsCommand } from "../commands/set-world-settings-command" ;
import { createUpsertEntityCommand } from "../commands/upsert-entity-command" ;
import { createUpsertModelInstanceCommand } from "../commands/upsert-model-instance-command" ;
import { createUpsertInteractionLinkCommand } from "../commands/upsert-interaction-link-command" ;
import { getSelectedBrushEdgeId , getSelectedBrushFaceId , getSelectedBrushVertexId , getSingleSelectedBrushId , getSingleSelectedEntityId , getSingleSelectedModelInstanceId , isBrushFaceSelected , isBrushSelected } from "../core/selection" ;
import { createTransformSession , doesTransformSessionChangeTarget , getTransformOperationLabel , getTransformTargetLabel , resolveTransformTarget , supportsTransformAxisConstraint , supportsTransformOperation } from "../core/transform-session" ;
import { MODEL _INSTANCE _COLLISION _MODES , areModelInstancesEqual , createModelInstance , createModelInstancePlacementPosition , DEFAULT _MODEL _INSTANCE _POSITION , DEFAULT _MODEL _INSTANCE _ROTATION _DEGREES , DEFAULT _MODEL _INSTANCE _SCALE , normalizeModelInstanceName } from "../assets/model-instances" ;
import { getModelInstanceDisplayLabelById , getSortedModelInstanceDisplayLabels } from "../assets/model-instance-labels" ;
import { importAudioAssetFromFile , loadAudioAssetFromStorage } from "../assets/audio-assets" ;
import { importModelAssetFromFile , importModelAssetFromFiles , loadModelAssetFromStorage , disposeModelTemplate } from "../assets/gltf-model-import" ;
import { importBackgroundImageAssetFromFile , loadImageAssetFromStorage , disposeLoadedImageAsset } from "../assets/image-assets" ;
import { getProjectAssetKindLabel } from "../assets/project-assets" ;
import { getWhiteboxSelectionModeLabel , WHITEBOX _SELECTION _MODES } from "../core/whitebox-selection-mode" ;
import { BOX _EDGE _LABELS , BOX _FACE _IDS , BOX _FACE _LABELS , BOX _VERTEX _LABELS , DEFAULT _BOX _BRUSH _CENTER , DEFAULT _BOX _BRUSH _ROTATION _DEGREES , DEFAULT _BOX _BRUSH _SIZE , createDefaultFaceUvState , normalizeBrushName } from "../document/brushes" ;
import { ADVANCED _RENDERING _SHADOW _MAP _SIZES , ADVANCED _RENDERING _SHADOW _TYPES , ADVANCED _RENDERING _TONE _MAPPING _MODES , areWorldSettingsEqual , changeWorldBackgroundMode , cloneWorldSettings } from "../document/world-settings" ;
import { formatSceneDiagnosticSummary , validateSceneDocument } from "../document/scene-document-validation" ;
import { getBrowserProjectAssetStorageAccess } from "../assets/project-asset-storage" ;
import { DEFAULT _GRID _SIZE , snapPositiveSizeToGrid , snapVec3ToGrid } from "../geometry/grid-snapping" ;
import { createFitToFaceBoxBrushFaceUvState } from "../geometry/box-face-uvs" ;
import { DEFAULT _ENTITY _POSITION , DEFAULT _INTERACTABLE _PROMPT , DEFAULT _INTERACTABLE _RADIUS , DEFAULT _POINT _LIGHT _COLOR _HEX , DEFAULT _POINT _LIGHT _DISTANCE , DEFAULT _POINT _LIGHT _INTENSITY , DEFAULT _PLAYER _START _BOX _SIZE , DEFAULT _PLAYER _START _CAPSULE _HEIGHT , DEFAULT _PLAYER _START _CAPSULE _RADIUS , DEFAULT _PLAYER _START _EYE _HEIGHT , DEFAULT _SOUND _EMITTER _AUDIO _ASSET _ID , DEFAULT _SOUND _EMITTER _VOLUME , DEFAULT _SOUND _EMITTER _REF _DISTANCE , DEFAULT _SOUND _EMITTER _MAX _DISTANCE , DEFAULT _TELEPORT _TARGET _YAW _DEGREES , PLAYER _START _COLLIDER _MODES , DEFAULT _SPOT _LIGHT _ANGLE _DEGREES , DEFAULT _SPOT _LIGHT _COLOR _HEX , DEFAULT _SPOT _LIGHT _DISTANCE , DEFAULT _SPOT _LIGHT _DIRECTION , DEFAULT _SPOT _LIGHT _INTENSITY , DEFAULT _TRIGGER _VOLUME _SIZE , areEntityInstancesEqual , createInteractableEntity , createPointLightEntity , createPlayerStartEntity , createSoundEmitterEntity , createSpotLightEntity , createTeleportTargetEntity , createTriggerVolumeEntity , getEntityInstances , getEntityKindLabel , getPrimaryPlayerStartEntity , normalizeEntityName , normalizeYawDegrees , normalizeInteractablePrompt } from "../entities/entity-instances" ;
import { getEntityDisplayLabelById , getSortedEntityDisplayLabels } from "../entities/entity-labels" ;
import { areInteractionLinksEqual , createPlayAnimationInteractionLink , createPlaySoundInteractionLink , createStopAnimationInteractionLink , createStopSoundInteractionLink , createTeleportPlayerInteractionLink , createToggleVisibilityInteractionLink , getInteractionLinksForSource } from "../interactions/interaction-links" ;
import { STARTER _MATERIAL _LIBRARY } from "../materials/starter-material-library" ;
import { RunnerCanvas } from "../runner-web/RunnerCanvas" ;
import { buildRuntimeSceneFromDocument } from "../runtime-three/runtime-scene-build" ;
import { validateRuntimeSceneBuild } from "../runtime-three/runtime-scene-validation" ;
import { Panel } from "../shared-ui/Panel" ;
import { HierarchicalMenu } from "../shared-ui/HierarchicalMenu" ;
import { createWorldBackgroundStyle } from "../shared-ui/world-background-style" ;
import { ViewportPanel } from "../viewport-three/ViewportPanel" ;
import { getViewportViewModeLabel } from "../viewport-three/viewport-view-modes" ;
import { VIEWPORT _LAYOUT _MODES , VIEWPORT _PANEL _IDS , getViewportDisplayModeLabel , getViewportLayoutModeLabel , getViewportPanelLabel } from "../viewport-three/viewport-layout" ;
import { useEditorStoreState } from "./use-editor-store" ;
function getModelInstanceCollisionModeDescription ( mode ) {
switch ( mode ) {
case "none" :
return "No generated collider is built for this model instance." ;
case "terrain" :
return "Builds a Rapier heightfield from a regular-grid terrain mesh. Unsupported terrain sources fail with build diagnostics." ;
case "static" :
return "Builds a fixed Rapier triangle-mesh collider from the imported model geometry." ;
case "dynamic" :
return "Builds convex compound pieces for Rapier queries. In this slice they participate as fixed world collision, not fully simulated rigid bodies." ;
case "simple" :
return "Builds one cheap oriented box from the imported model bounds." ;
}
}
function getPlayerStartColliderModeDescription ( mode ) {
switch ( mode ) {
case "capsule" :
return "Uses a capsule player collider for standard grounded first-person traversal." ;
case "box" :
return "Uses an axis-aligned box player collider for sharper footprint bounds." ;
case "none" :
return "Disables player collision detection. First-person traversal continues without world clipping." ;
}
}
const STARTER _MATERIAL _ORDER = new Map ( STARTER _MATERIAL _LIBRARY . map ( ( material , index ) => [ material . id , index ] ) ) ;
const MIN _VIEWPORT _QUAD _SPLIT = 0.2 ;
const MAX _VIEWPORT _QUAD _SPLIT = 0.8 ;
function formatVec3 ( vector ) {
return ` ${ vector . x } , ${ vector . y } , ${ vector . z } ` ;
}
function resolveOptionalPositiveNumber ( value , fallback ) {
const parsedValue = Number ( value ) ;
return Number . isFinite ( parsedValue ) && parsedValue > 0 ? parsedValue : fallback ;
}
function getWhiteboxInputStep ( enabled , step ) {
return enabled ? step : "any" ;
}
function formatDiagnosticCount ( count , label ) {
return ` ${ count } ${ label } ${ count === 1 ? "" : "s" } ` ;
}
function clampViewportQuadSplitValue ( value ) {
return Math . min ( MAX _VIEWPORT _QUAD _SPLIT , Math . max ( MIN _VIEWPORT _QUAD _SPLIT , value ) ) ;
}
function createViewportQuadPanelsStyle ( viewportQuadSplit ) {
return {
"--viewport-quad-split-x" : String ( viewportQuadSplit . x ) ,
"--viewport-quad-split-y" : String ( viewportQuadSplit . y )
} ;
}
function getViewportQuadResizeCursor ( resizeMode ) {
switch ( resizeMode ) {
case "vertical" :
return "col-resize" ;
case "horizontal" :
return "row-resize" ;
case "center" :
return "move" ;
}
}
function createVec2Draft ( vector ) {
return {
x : String ( vector . x ) ,
y : String ( vector . y )
} ;
}
function createVec3Draft ( vector ) {
return {
x : String ( vector . x ) ,
y : String ( vector . y ) ,
z : String ( vector . z )
} ;
}
function readVec2Draft ( draft , label ) {
const vector = {
x : Number ( draft . x ) ,
y : Number ( draft . y )
} ;
if ( ! Number . isFinite ( vector . x ) || ! Number . isFinite ( vector . y ) ) {
throw new Error ( ` ${ label } values must be finite numbers. ` ) ;
}
return vector ;
}
function readPositiveVec2Draft ( draft , label ) {
const vector = readVec2Draft ( draft , label ) ;
if ( vector . x <= 0 || vector . y <= 0 ) {
throw new Error ( ` ${ label } values must remain positive. ` ) ;
}
return vector ;
}
function readPositiveVec3Draft ( draft , label ) {
const vector = readVec3Draft ( draft , label ) ;
if ( vector . x <= 0 || vector . y <= 0 || vector . z <= 0 ) {
throw new Error ( ` ${ label } values must remain positive. ` ) ;
}
return vector ;
}
function readVec3Draft ( draft , label ) {
const vector = {
x : Number ( draft . x ) ,
y : Number ( draft . y ) ,
z : Number ( draft . z )
} ;
if ( ! Number . isFinite ( vector . x ) || ! Number . isFinite ( vector . y ) || ! Number . isFinite ( vector . z ) ) {
throw new Error ( ` ${ label } values must be finite numbers. ` ) ;
}
return vector ;
}
function readYawDegreesDraft ( source ) {
const yawDegrees = Number ( source ) ;
if ( ! Number . isFinite ( yawDegrees ) ) {
throw new Error ( "Player start yaw must be a finite number." ) ;
}
return normalizeYawDegrees ( yawDegrees ) ;
}
function readInteractablePromptDraft ( source ) {
return normalizeInteractablePrompt ( source ) ;
}
function readNonNegativeNumberDraft ( source , label ) {
const value = Number ( source ) ;
if ( ! Number . isFinite ( value ) || value < 0 ) {
throw new Error ( ` ${ label } must be a finite number greater than or equal to zero. ` ) ;
}
return value ;
}
function readFiniteNumberDraft ( source , label ) {
const value = Number ( source ) ;
if ( ! Number . isFinite ( value ) ) {
throw new Error ( ` ${ label } must be a finite number. ` ) ;
}
return value ;
}
function readPositiveIntegerDraft ( source , label ) {
const value = Number ( source ) ;
if ( ! Number . isFinite ( value ) || value <= 0 || ! Number . isInteger ( value ) ) {
throw new Error ( ` ${ label } must be a positive integer. ` ) ;
}
return value ;
}
function readPositiveNumberDraft ( source , label ) {
const value = Number ( source ) ;
if ( ! Number . isFinite ( value ) || value <= 0 ) {
throw new Error ( ` ${ label } must be a finite number greater than zero. ` ) ;
}
return value ;
}
function areVec2Equal ( left , right ) {
return left . x === right . x && left . y === right . y ;
}
function areVec3Equal ( left , right ) {
return left . x === right . x && left . y === right . y && left . z === right . z ;
}
function maybeSnapVec3 ( vector , enabled , step ) {
if ( ! enabled ) {
return vector ;
}
return {
x : Math . round ( vector . x / step ) * step ,
y : Math . round ( vector . y / step ) * step ,
z : Math . round ( vector . z / step ) * step
} ;
}
function maybeSnapPositiveSize ( size , enabled , step ) {
const clampComponent = ( value ) => Math . max ( 0.01 , Math . abs ( value ) ) ;
if ( ! enabled ) {
return {
x : clampComponent ( size . x ) ,
y : clampComponent ( size . y ) ,
z : clampComponent ( size . z )
} ;
}
return {
x : Math . max ( 0.01 , Math . round ( Math . abs ( size . x ) / step ) * step ) ,
y : Math . max ( 0.01 , Math . round ( Math . abs ( size . y ) / step ) * step ) ,
z : Math . max ( 0.01 , Math . round ( Math . abs ( size . z ) / step ) * step )
} ;
}
function areFaceUvStatesEqual ( left , right ) {
return ( areVec2Equal ( left . offset , right . offset ) &&
areVec2Equal ( left . scale , right . scale ) &&
left . rotationQuarterTurns === right . rotationQuarterTurns &&
left . flipU === right . flipU &&
left . flipV === right . flipV ) ;
}
function getSelectedBoxBrush ( selection , brushes ) {
const selectedBrushId = getSingleSelectedBrushId ( selection ) ;
if ( selectedBrushId === null ) {
return null ;
}
return brushes . find ( ( brush ) => brush . id === selectedBrushId ) ? ? null ;
}
function getSelectedEntity ( selection , entities ) {
const selectedEntityId = getSingleSelectedEntityId ( selection ) ;
if ( selectedEntityId === null ) {
return null ;
}
return entities . find ( ( entity ) => entity . id === selectedEntityId ) ? ? null ;
}
function getSelectedModelInstance ( selection , modelInstances ) {
const selectedModelInstanceId = getSingleSelectedModelInstanceId ( selection ) ;
if ( selectedModelInstanceId === null ) {
return null ;
}
return modelInstances . find ( ( modelInstance ) => modelInstance . id === selectedModelInstanceId ) ? ? null ;
}
function isModelAsset ( asset ) {
return asset . kind === "model" ;
}
function isImageAsset ( asset ) {
return asset . kind === "image" ;
}
function isAudioAsset ( asset ) {
return asset . kind === "audio" ;
}
function formatByteLength ( byteLength ) {
if ( byteLength < 1024 ) {
return ` ${ byteLength } B ` ;
}
const kilobytes = byteLength / 1024 ;
if ( kilobytes < 1024 ) {
return ` ${ kilobytes . toFixed ( kilobytes >= 10 ? 0 : 1 ) } KB ` ;
}
return ` ${ ( kilobytes / 1024 ) . toFixed ( 1 ) } MB ` ;
}
function formatModelBoundingBoxLabel ( asset ) {
if ( asset . metadata . boundingBox === null ) {
return "Bounds unavailable" ;
}
const { size } = asset . metadata . boundingBox ;
return ` Bounds ${ size . x . toFixed ( 2 ) } x ${ size . y . toFixed ( 2 ) } x ${ size . z . toFixed ( 2 ) } m ` ;
}
function formatModelAssetSummary ( asset ) {
const details = [
asset . metadata . format . toUpperCase ( ) ,
formatByteLength ( asset . byteLength ) ,
` ${ asset . metadata . meshCount } mesh ${ asset . metadata . meshCount === 1 ? "" : "es" } ` ,
` ${ asset . metadata . materialNames . length } material ${ asset . metadata . materialNames . length === 1 ? "" : "s" } ` ,
` ${ asset . metadata . textureNames . length } texture ${ asset . metadata . textureNames . length === 1 ? "" : "s" } `
] ;
if ( asset . metadata . animationNames . length > 0 ) {
details . push ( ` ${ asset . metadata . animationNames . length } animation ${ asset . metadata . animationNames . length === 1 ? "" : "s" } ` ) ;
}
return details . join ( " | " ) ;
}
function formatImageAssetSummary ( asset ) {
const details = [
` ${ asset . metadata . width } x ${ asset . metadata . height } ` ,
asset . metadata . hasAlpha ? "alpha" : "opaque" ,
formatByteLength ( asset . byteLength )
] ;
return details . join ( " | " ) ;
}
function formatAudioAssetSummary ( asset ) {
const details = [
asset . metadata . durationSeconds === null ? "duration unavailable" : ` ${ asset . metadata . durationSeconds . toFixed ( 2 ) } s ` ,
asset . metadata . channelCount === null ? "channels unavailable" : ` ${ asset . metadata . channelCount } channel ${ asset . metadata . channelCount === 1 ? "" : "s" } ` ,
asset . metadata . sampleRateHz === null ? "sample rate unavailable" : ` ${ asset . metadata . sampleRateHz } Hz ` ,
formatByteLength ( asset . byteLength )
] ;
return details . join ( " | " ) ;
}
function formatAssetHoverStatus ( asset ) {
const details = [
` ${ getProjectAssetKindLabel ( asset . kind ) } asset ` ,
asset . mimeType ,
asset . kind === "model"
? formatModelAssetSummary ( asset )
: asset . kind === "image"
? formatImageAssetSummary ( asset )
: formatAudioAssetSummary ( asset ) ,
` Storage key: ${ asset . storageKey } `
] ;
if ( asset . kind === "model" ) {
details . push ( formatModelBoundingBoxLabel ( asset ) ) ;
}
if ( asset . metadata . warnings . length > 0 ) {
details . push ( ` Warnings: ${ asset . metadata . warnings . join ( " | " ) } ` ) ;
}
return ` ${ asset . sourceName } | ${ details . join ( " | " ) } ` ;
}
function getBrushLabel ( brush , index ) {
return brush . name ? ? ` Whitebox Box ${ index + 1 } ` ;
}
function getBrushLabelById ( brushId , brushes ) {
const brushIndex = brushes . findIndex ( ( brush ) => brush . id === brushId ) ;
return brushIndex === - 1 ? "Whitebox Box" : getBrushLabel ( brushes [ brushIndex ] , brushIndex ) ;
}
function getSelectedBrushLabel ( selection , brushes ) {
const selectedBrushId = getSingleSelectedBrushId ( selection ) ;
if ( selectedBrushId === null ) {
return "No solid selected" ;
}
return getBrushLabelById ( selectedBrushId , brushes ) ;
}
function describeSelection ( selection , brushes , modelInstances , assets , entities ) {
switch ( selection . kind ) {
case "none" :
return "No authored selection" ;
case "brushes" :
return ` ${ selection . ids . length } solid ${ selection . ids . length === 1 ? "" : "s" } selected ( ${ getSelectedBrushLabel ( selection , brushes ) } ) ` ;
case "brushFace" :
return ` 1 face selected ( ${ BOX _FACE _LABELS [ selection . faceId ] } on ${ getBrushLabelById ( selection . brushId , brushes ) } ) ` ;
case "brushEdge" :
return ` 1 edge selected ( ${ BOX _EDGE _LABELS [ selection . edgeId ] } on ${ getBrushLabelById ( selection . brushId , brushes ) } ) ` ;
case "brushVertex" :
return ` 1 vertex selected ( ${ BOX _VERTEX _LABELS [ selection . vertexId ] } on ${ getBrushLabelById ( selection . brushId , brushes ) } ) ` ;
case "entities" :
return ` ${ selection . ids . length } entity selected ( ${ getEntityDisplayLabelById ( selection . ids [ 0 ] , entities , assets ) } ) ` ;
case "modelInstances" :
return ` ${ selection . ids . length } model instance ${ selection . ids . length === 1 ? "" : "s" } selected ( ${ getModelInstanceDisplayLabelById ( selection . ids [ 0 ] , modelInstances , assets ) } ) ` ;
default :
return "Unknown selection" ;
}
}
function getWhiteboxSelectionModeStatus ( mode ) {
switch ( mode ) {
case "object" :
return "Whitebox selection mode set to Object. Whole-solid transforms are available." ;
case "face" :
return "Whitebox selection mode set to Face. Click a face to edit materials and UVs." ;
case "edge" :
return "Whitebox selection mode set to Edge. Edge transforms land in the next slice." ;
case "vertex" :
return "Whitebox selection mode set to Vertex. Vertex transforms land in the next slice." ;
}
}
function getInteractionTriggerLabel ( trigger ) {
switch ( trigger ) {
case "enter" :
return "On Enter" ;
case "exit" :
return "On Exit" ;
case "click" :
return "On Click" ;
}
}
function getInteractionActionLabel ( link ) {
switch ( link . action . type ) {
case "teleportPlayer" :
return "Teleport Player" ;
case "toggleVisibility" :
return "Toggle Visibility" ;
case "playAnimation" :
return "Play Animation" ;
case "stopAnimation" :
return "Stop Animation" ;
case "playSound" :
return "Play Sound" ;
case "stopSound" :
return "Stop Sound" ;
}
}
function getVisibilityModeSelectValue ( visible ) {
if ( visible === true ) {
return "show" ;
}
if ( visible === false ) {
return "hide" ;
}
return "toggle" ;
}
function readVisibilityModeSelectValue ( value ) {
switch ( value ) {
case "toggle" :
return undefined ;
case "show" :
return true ;
case "hide" :
return false ;
}
}
function getDefaultTriggerVolumeLinkTrigger ( triggerOnEnter , triggerOnExit ) {
if ( triggerOnEnter ) {
return "enter" ;
}
if ( triggerOnExit ) {
return "exit" ;
}
return "enter" ;
}
function isInteractionSourceEntity ( entity ) {
return entity !== null && ( entity . kind === "triggerVolume" || entity . kind === "interactable" ) ;
}
function isSoundEmitterEntity ( entity ) {
return entity !== null && entity . kind === "soundEmitter" ;
}
function getDefaultInteractionLinkTrigger ( sourceEntity ) {
return sourceEntity . kind === "triggerVolume"
? getDefaultTriggerVolumeLinkTrigger ( sourceEntity . triggerOnEnter , sourceEntity . triggerOnExit )
: "click" ;
}
function getErrorMessage ( error ) {
if ( error instanceof Error ) {
return error . message ;
}
return "An unexpected error occurred." ;
}
function isTextEntryTarget ( target ) {
if ( ! ( target instanceof HTMLElement ) ) {
return false ;
}
return ( target instanceof HTMLInputElement ||
target instanceof HTMLTextAreaElement ||
target instanceof HTMLSelectElement ||
target . isContentEditable ) ;
}
2026-04-05 04:23:49 +02:00
function selectionCanBeDuplicated ( selection ) {
switch ( selection . kind ) {
case "brushes" :
case "entities" :
case "modelInstances" :
return selection . ids . length > 0 ;
case "brushFace" :
case "brushEdge" :
case "brushVertex" :
return true ;
case "none" :
return false ;
}
}
2026-04-05 02:25:08 +02:00
function isCommitIncrementKey ( key ) {
return key === "ArrowUp" || key === "ArrowDown" || key === "PageUp" || key === "PageDown" ;
}
function blurActiveTextEntry ( ) {
const activeElement = document . activeElement ;
if ( ! ( activeElement instanceof HTMLElement ) || ! isTextEntryTarget ( activeElement ) ) {
return ;
}
activeElement . blur ( ) ;
}
function sortDocumentMaterials ( materials ) {
return Object . values ( materials ) . sort ( ( left , right ) => {
const leftStarterIndex = STARTER _MATERIAL _ORDER . get ( left . id ) ? ? Number . MAX _SAFE _INTEGER ;
const rightStarterIndex = STARTER _MATERIAL _ORDER . get ( right . id ) ? ? Number . MAX _SAFE _INTEGER ;
if ( leftStarterIndex !== rightStarterIndex ) {
return leftStarterIndex - rightStarterIndex ;
}
return left . name . localeCompare ( right . name ) ;
} ) ;
}
function getMaterialPreviewStyle ( material ) {
switch ( material . pattern ) {
case "grid" :
return {
backgroundColor : material . baseColorHex ,
backgroundImage : ` linear-gradient( ${ material . accentColorHex } 2px, transparent 2px), linear-gradient(90deg, ${ material . accentColorHex } 2px, transparent 2px) ` ,
backgroundSize : "18px 18px"
} ;
case "checker" :
return {
backgroundColor : material . baseColorHex ,
backgroundImage : ` linear-gradient(45deg, ${ material . accentColorHex } 25%, transparent 25%, transparent 75%, ${ material . accentColorHex } 75%, ${ material . accentColorHex } ), linear-gradient(45deg, ${ material . accentColorHex } 25%, transparent 25%, transparent 75%, ${ material . accentColorHex } 75%, ${ material . accentColorHex } ) ` ,
backgroundPosition : "0 0, 9px 9px" ,
backgroundSize : "18px 18px"
} ;
case "stripes" :
return {
backgroundColor : material . baseColorHex ,
backgroundImage : ` repeating-linear-gradient(135deg, ${ material . accentColorHex } 0 9px, transparent 9px 18px) `
} ;
case "diamond" :
return {
backgroundColor : material . baseColorHex ,
backgroundImage : ` linear-gradient(45deg, ${ material . accentColorHex } 12%, transparent 12%, transparent 88%, ${ material . accentColorHex } 88%), linear-gradient(-45deg, ${ material . accentColorHex } 12%, transparent 12%, transparent 88%, ${ material . accentColorHex } 88%) ` ,
backgroundSize : "22px 22px"
} ;
}
}
function rotateQuarterTurns ( rotationQuarterTurns ) {
return ( ( rotationQuarterTurns + 1 ) % 4 ) ;
}
function getTransformOperationPastTense ( operation ) {
switch ( operation ) {
case "translate" :
return "Moved" ;
case "rotate" :
return "Rotated" ;
case "scale" :
return "Scaled" ;
}
}
function getTransformOperationShortcut ( operation ) {
switch ( operation ) {
case "translate" :
return "G" ;
case "rotate" :
return "R" ;
case "scale" :
return "S" ;
}
}
function formatRunnerFeetPosition ( position ) {
if ( position === null ) {
return "n/a" ;
}
return ` ${ position . x . toFixed ( 2 ) } , ${ position . y . toFixed ( 2 ) } , ${ position . z . toFixed ( 2 ) } ` ;
}
function formatWorldBackgroundLabel ( world ) {
if ( world . background . mode === "solid" ) {
return "Solid" ;
}
if ( world . background . mode === "verticalGradient" ) {
return "Vertical Gradient" ;
}
return "Image" ;
}
function formatAdvancedRenderingShadowTypeLabel ( type ) {
switch ( type ) {
case "basic" :
return "Basic" ;
case "pcf" :
return "PCF" ;
case "pcfSoft" :
return "PCF Soft" ;
}
}
function formatAdvancedRenderingToneMappingLabel ( mode ) {
switch ( mode ) {
case "none" :
return "None" ;
case "linear" :
return "Linear" ;
case "reinhard" :
return "Reinhard" ;
case "cineon" :
return "Cineon" ;
case "acesFilmic" :
return "ACES Filmic" ;
}
}
export function App ( { store , initialStatusMessage } ) {
const editorState = useEditorStoreState ( store ) ;
const brushList = Object . values ( editorState . document . brushes ) ;
const layoutMode = editorState . viewportLayoutMode ;
const activePanelId = editorState . activeViewportPanelId ;
const viewportToolPreview = editorState . viewportTransientState . toolPreview ;
const transformSession = editorState . viewportTransientState . transformSession ;
const entityList = getEntityInstances ( editorState . document . entities ) ;
const entityDisplayList = getSortedEntityDisplayLabels ( editorState . document . entities , editorState . document . assets ) ;
const primaryPlayerStart = getPrimaryPlayerStartEntity ( editorState . document . entities ) ;
const materialList = sortDocumentMaterials ( editorState . document . materials ) ;
const selectedBrush = getSelectedBoxBrush ( editorState . selection , brushList ) ;
const selectedEntity = getSelectedEntity ( editorState . selection , entityList ) ;
const selectedModelInstance = getSelectedModelInstance ( editorState . selection , Object . values ( editorState . document . modelInstances ) ) ;
const whiteboxSelectionMode = editorState . whiteboxSelectionMode ;
const selectedFaceId = getSelectedBrushFaceId ( editorState . selection ) ;
const selectedEdgeId = getSelectedBrushEdgeId ( editorState . selection ) ;
const selectedVertexId = getSelectedBrushVertexId ( editorState . selection ) ;
const selectedFace = selectedBrush !== null && selectedFaceId !== null ? selectedBrush . faces [ selectedFaceId ] : null ;
const selectedFaceMaterial = selectedFace !== null && selectedFace . materialId !== null ? editorState . document . materials [ selectedFace . materialId ] ? ? null : null ;
const selectedModelAsset = selectedModelInstance !== null ? ( editorState . document . assets [ selectedModelInstance . assetId ] ? ? null ) : null ;
const selectedModelAssetRecord = selectedModelAsset !== null && selectedModelAsset . kind === "model" ? selectedModelAsset : null ;
const selectedPlayerStart = selectedEntity ? . kind === "playerStart" ? selectedEntity : null ;
const selectedSoundEmitter = isSoundEmitterEntity ( selectedEntity ) ? selectedEntity : null ;
const selectedSoundEmitterAsset = selectedSoundEmitter === null
? null
: selectedSoundEmitter . audioAssetId === null
? null
: editorState . document . assets [ selectedSoundEmitter . audioAssetId ] ? ? null ;
const selectedSoundEmitterAudioAssetRecord = selectedSoundEmitterAsset !== null && selectedSoundEmitterAsset . kind === "audio" ? selectedSoundEmitterAsset : null ;
const selectedTriggerVolume = selectedEntity ? . kind === "triggerVolume" ? selectedEntity : null ;
const selectedTeleportTarget = selectedEntity ? . kind === "teleportTarget" ? selectedEntity : null ;
const selectedInteractable = selectedEntity ? . kind === "interactable" ? selectedEntity : null ;
const projectAssetList = Object . values ( editorState . document . assets ) ;
const modelAssetList = projectAssetList . filter ( isModelAsset ) ;
const imageAssetList = projectAssetList . filter ( isImageAsset ) ;
const audioAssetList = projectAssetList . filter ( isAudioAsset ) ;
const selectedPointLight = selectedEntity ? . kind === "pointLight" ? selectedEntity : null ;
const selectedSpotLight = selectedEntity ? . kind === "spotLight" ? selectedEntity : null ;
const modelInstanceDisplayList = getSortedModelInstanceDisplayLabels ( editorState . document . modelInstances , editorState . document . assets ) ;
const selectedInteractionSource = isInteractionSourceEntity ( selectedEntity ) ? selectedEntity : null ;
const selectedTriggerVolumeLinks = selectedTriggerVolume === null
? [ ]
: getInteractionLinksForSource ( editorState . document . interactionLinks , selectedTriggerVolume . id ) ;
const selectedInteractableLinks = selectedInteractable === null ? [ ] : getInteractionLinksForSource ( editorState . document . interactionLinks , selectedInteractable . id ) ;
const teleportTargetOptions = entityDisplayList . filter ( ( { entity } ) => entity . kind === "teleportTarget" ) ;
const soundEmitterOptions = entityDisplayList . filter ( ( { entity } ) => entity . kind === "soundEmitter" ) ;
const playableSoundEmitterOptions = soundEmitterOptions . filter ( ( { entity } ) => {
if ( entity . audioAssetId === null ) {
return false ;
}
return editorState . document . assets [ entity . audioAssetId ] ? . kind === "audio" ;
} ) ;
const visibilityBrushOptions = brushList . map ( ( brush , brushIndex ) => ( {
brush ,
label : getBrushLabel ( brush , brushIndex )
} ) ) ;
const [ sceneNameDraft , setSceneNameDraft ] = useState ( editorState . document . name ) ;
const [ brushNameDraft , setBrushNameDraft ] = useState ( "" ) ;
const [ entityNameDraft , setEntityNameDraft ] = useState ( "" ) ;
const [ modelInstanceNameDraft , setModelInstanceNameDraft ] = useState ( "" ) ;
const [ positionDraft , setPositionDraft ] = useState ( createVec3Draft ( DEFAULT _BOX _BRUSH _CENTER ) ) ;
const [ rotationDraft , setRotationDraft ] = useState ( createVec3Draft ( DEFAULT _BOX _BRUSH _ROTATION _DEGREES ) ) ;
const [ sizeDraft , setSizeDraft ] = useState ( createVec3Draft ( DEFAULT _BOX _BRUSH _SIZE ) ) ;
const [ whiteboxSnapEnabled , setWhiteboxSnapEnabled ] = useState ( true ) ;
const [ whiteboxSnapStepDraft , setWhiteboxSnapStepDraft ] = useState ( String ( DEFAULT _GRID _SIZE ) ) ;
const [ uvOffsetDraft , setUvOffsetDraft ] = useState ( createVec2Draft ( createDefaultFaceUvState ( ) . offset ) ) ;
const [ uvScaleDraft , setUvScaleDraft ] = useState ( createVec2Draft ( createDefaultFaceUvState ( ) . scale ) ) ;
const [ entityPositionDraft , setEntityPositionDraft ] = useState ( createVec3Draft ( DEFAULT _ENTITY _POSITION ) ) ;
const [ pointLightColorDraft , setPointLightColorDraft ] = useState ( DEFAULT _POINT _LIGHT _COLOR _HEX ) ;
const [ pointLightIntensityDraft , setPointLightIntensityDraft ] = useState ( String ( DEFAULT _POINT _LIGHT _INTENSITY ) ) ;
const [ pointLightDistanceDraft , setPointLightDistanceDraft ] = useState ( String ( DEFAULT _POINT _LIGHT _DISTANCE ) ) ;
const [ spotLightColorDraft , setSpotLightColorDraft ] = useState ( DEFAULT _SPOT _LIGHT _COLOR _HEX ) ;
const [ spotLightIntensityDraft , setSpotLightIntensityDraft ] = useState ( String ( DEFAULT _SPOT _LIGHT _INTENSITY ) ) ;
const [ spotLightDistanceDraft , setSpotLightDistanceDraft ] = useState ( String ( DEFAULT _SPOT _LIGHT _DISTANCE ) ) ;
const [ spotLightAngleDraft , setSpotLightAngleDraft ] = useState ( String ( DEFAULT _SPOT _LIGHT _ANGLE _DEGREES ) ) ;
const [ spotLightDirectionDraft , setSpotLightDirectionDraft ] = useState ( createVec3Draft ( DEFAULT _SPOT _LIGHT _DIRECTION ) ) ;
const [ playerStartYawDraft , setPlayerStartYawDraft ] = useState ( "0" ) ;
const [ playerStartColliderModeDraft , setPlayerStartColliderModeDraft ] = useState ( "capsule" ) ;
const [ playerStartEyeHeightDraft , setPlayerStartEyeHeightDraft ] = useState ( String ( DEFAULT _PLAYER _START _EYE _HEIGHT ) ) ;
const [ playerStartCapsuleRadiusDraft , setPlayerStartCapsuleRadiusDraft ] = useState ( String ( DEFAULT _PLAYER _START _CAPSULE _RADIUS ) ) ;
const [ playerStartCapsuleHeightDraft , setPlayerStartCapsuleHeightDraft ] = useState ( String ( DEFAULT _PLAYER _START _CAPSULE _HEIGHT ) ) ;
const [ playerStartBoxSizeDraft , setPlayerStartBoxSizeDraft ] = useState ( createVec3Draft ( DEFAULT _PLAYER _START _BOX _SIZE ) ) ;
const [ soundEmitterAudioAssetIdDraft , setSoundEmitterAudioAssetIdDraft ] = useState ( DEFAULT _SOUND _EMITTER _AUDIO _ASSET _ID ? ? "" ) ;
const [ soundEmitterVolumeDraft , setSoundEmitterVolumeDraft ] = useState ( String ( DEFAULT _SOUND _EMITTER _VOLUME ) ) ;
const [ soundEmitterRefDistanceDraft , setSoundEmitterRefDistanceDraft ] = useState ( String ( DEFAULT _SOUND _EMITTER _REF _DISTANCE ) ) ;
const [ soundEmitterMaxDistanceDraft , setSoundEmitterMaxDistanceDraft ] = useState ( String ( DEFAULT _SOUND _EMITTER _MAX _DISTANCE ) ) ;
const [ soundEmitterAutoplayDraft , setSoundEmitterAutoplayDraft ] = useState ( false ) ;
const [ soundEmitterLoopDraft , setSoundEmitterLoopDraft ] = useState ( false ) ;
const [ triggerVolumeSizeDraft , setTriggerVolumeSizeDraft ] = useState ( createVec3Draft ( DEFAULT _TRIGGER _VOLUME _SIZE ) ) ;
const [ teleportTargetYawDraft , setTeleportTargetYawDraft ] = useState ( String ( DEFAULT _TELEPORT _TARGET _YAW _DEGREES ) ) ;
const [ interactableRadiusDraft , setInteractableRadiusDraft ] = useState ( String ( DEFAULT _INTERACTABLE _RADIUS ) ) ;
const [ interactablePromptDraft , setInteractablePromptDraft ] = useState ( DEFAULT _INTERACTABLE _PROMPT ) ;
const [ interactableEnabledDraft , setInteractableEnabledDraft ] = useState ( true ) ;
const [ modelPositionDraft , setModelPositionDraft ] = useState ( createVec3Draft ( DEFAULT _MODEL _INSTANCE _POSITION ) ) ;
const [ modelRotationDraft , setModelRotationDraft ] = useState ( createVec3Draft ( DEFAULT _MODEL _INSTANCE _ROTATION _DEGREES ) ) ;
const [ modelScaleDraft , setModelScaleDraft ] = useState ( createVec3Draft ( DEFAULT _MODEL _INSTANCE _SCALE ) ) ;
const [ ambientLightIntensityDraft , setAmbientLightIntensityDraft ] = useState ( String ( editorState . document . world . ambientLight . intensity ) ) ;
const [ sunLightIntensityDraft , setSunLightIntensityDraft ] = useState ( String ( editorState . document . world . sunLight . intensity ) ) ;
const [ sunDirectionDraft , setSunDirectionDraft ] = useState ( createVec3Draft ( editorState . document . world . sunLight . direction ) ) ;
const [ backgroundEnvironmentIntensityDraft , setBackgroundEnvironmentIntensityDraft ] = useState ( editorState . document . world . background . mode === "image" ? String ( editorState . document . world . background . environmentIntensity ) : "0.5" ) ;
const [ advancedRenderingShadowBiasDraft , setAdvancedRenderingShadowBiasDraft ] = useState ( String ( editorState . document . world . advancedRendering . shadows . bias ) ) ;
const [ advancedRenderingAmbientOcclusionIntensityDraft , setAdvancedRenderingAmbientOcclusionIntensityDraft ] = useState ( String ( editorState . document . world . advancedRendering . ambientOcclusion . intensity ) ) ;
const [ advancedRenderingAmbientOcclusionRadiusDraft , setAdvancedRenderingAmbientOcclusionRadiusDraft ] = useState ( String ( editorState . document . world . advancedRendering . ambientOcclusion . radius ) ) ;
const [ advancedRenderingAmbientOcclusionSamplesDraft , setAdvancedRenderingAmbientOcclusionSamplesDraft ] = useState ( String ( editorState . document . world . advancedRendering . ambientOcclusion . samples ) ) ;
const [ advancedRenderingBloomIntensityDraft , setAdvancedRenderingBloomIntensityDraft ] = useState ( String ( editorState . document . world . advancedRendering . bloom . intensity ) ) ;
const [ advancedRenderingBloomThresholdDraft , setAdvancedRenderingBloomThresholdDraft ] = useState ( String ( editorState . document . world . advancedRendering . bloom . threshold ) ) ;
const [ advancedRenderingBloomRadiusDraft , setAdvancedRenderingBloomRadiusDraft ] = useState ( String ( editorState . document . world . advancedRendering . bloom . radius ) ) ;
const [ advancedRenderingToneMappingExposureDraft , setAdvancedRenderingToneMappingExposureDraft ] = useState ( String ( editorState . document . world . advancedRendering . toneMapping . exposure ) ) ;
const [ advancedRenderingDepthOfFieldFocusDistanceDraft , setAdvancedRenderingDepthOfFieldFocusDistanceDraft ] = useState ( String ( editorState . document . world . advancedRendering . depthOfField . focusDistance ) ) ;
const [ advancedRenderingDepthOfFieldFocalLengthDraft , setAdvancedRenderingDepthOfFieldFocalLengthDraft ] = useState ( String ( editorState . document . world . advancedRendering . depthOfField . focalLength ) ) ;
const [ advancedRenderingDepthOfFieldBokehScaleDraft , setAdvancedRenderingDepthOfFieldBokehScaleDraft ] = useState ( String ( editorState . document . world . advancedRendering . depthOfField . bokehScale ) ) ;
const [ statusMessage , setStatusMessage ] = useState ( initialStatusMessage ? ? "Slice 3.5 advanced rendering ready." ) ;
const [ assetStatusMessage , setAssetStatusMessage ] = useState ( null ) ;
const [ hoveredAssetId , setHoveredAssetId ] = useState ( null ) ;
const [ hoveredViewportPanelId , setHoveredViewportPanelId ] = useState ( null ) ;
const [ addMenuPosition , setAddMenuPosition ] = useState ( null ) ;
const [ preferredNavigationMode , setPreferredNavigationMode ] = useState ( primaryPlayerStart === null ? "orbitVisitor" : "firstPerson" ) ;
const [ activeNavigationMode , setActiveNavigationMode ] = useState ( primaryPlayerStart === null ? "orbitVisitor" : "firstPerson" ) ;
const [ projectAssetStorage , setProjectAssetStorage ] = useState ( null ) ;
const [ projectAssetStorageReady , setProjectAssetStorageReady ] = useState ( false ) ;
const [ runtimeScene , setRuntimeScene ] = useState ( null ) ;
const [ runtimeMessage , setRuntimeMessage ] = useState ( null ) ;
const [ firstPersonTelemetry , setFirstPersonTelemetry ] = useState ( null ) ;
const [ runtimeInteractionPrompt , setRuntimeInteractionPrompt ] = useState ( null ) ;
const [ loadedModelAssets , setLoadedModelAssets ] = useState ( { } ) ;
const [ loadedImageAssets , setLoadedImageAssets ] = useState ( { } ) ;
const [ loadedAudioAssets , setLoadedAudioAssets ] = useState ( { } ) ;
const [ focusRequest , setFocusRequest ] = useState ( {
id : 0 ,
panelId : "topLeft" ,
selection : {
kind : "none"
}
} ) ;
const importInputRef = useRef ( null ) ;
const importModelInputRef = useRef ( null ) ;
const importBackgroundImageInputRef = useRef ( null ) ;
const importAudioInputRef = useRef ( null ) ;
const viewportPanelsRef = useRef ( null ) ;
const loadedModelAssetsRef = useRef ( { } ) ;
const loadedImageAssetsRef = useRef ( { } ) ;
const loadedAudioAssetsRef = useRef ( { } ) ;
const viewportQuadSplitRef = useRef ( editorState . viewportQuadSplit ) ;
const lastPointerPositionRef = useRef ( {
x : Math . round ( window . innerWidth * 0.5 ) ,
y : Math . round ( window . innerHeight * 0.5 )
} ) ;
const [ viewportQuadResizeMode , setViewportQuadResizeMode ] = useState ( null ) ;
const documentValidation = validateSceneDocument ( editorState . document ) ;
const runValidation = validateRuntimeSceneBuild ( editorState . document , {
navigationMode : preferredNavigationMode ,
loadedModelAssets
} ) ;
const diagnostics = [ ... documentValidation . errors , ... documentValidation . warnings , ... runValidation . errors , ... runValidation . warnings ] ;
const blockingDiagnostics = diagnostics . filter ( ( diagnostic ) => diagnostic . severity === "error" ) ;
const warningDiagnostics = diagnostics . filter ( ( diagnostic ) => diagnostic . severity === "warning" ) ;
const documentStatusLabel = documentValidation . errors . length === 0 ? "Valid" : formatDiagnosticCount ( documentValidation . errors . length , "error" ) ;
const lastCommandLabel = editorState . lastCommandLabel ? ? "No commands yet" ;
const runReadyLabel = blockingDiagnostics . length > 0
? "Blocked"
: preferredNavigationMode === "firstPerson"
? "Ready for First Person"
: "Ready for Orbit Visitor" ;
const advancedRendering = editorState . document . world . advancedRendering ;
const hoveredAsset = hoveredAssetId === null ? null : editorState . document . assets [ hoveredAssetId ] ? ? null ;
const hoveredAssetStatusMessage = hoveredAsset === null ? null : formatAssetHoverStatus ( hoveredAsset ) ;
const selectedTransformTarget = resolveTransformTarget ( editorState . document , editorState . selection , whiteboxSelectionMode ) . target ;
const canTranslateSelectedTarget = selectedTransformTarget !== null && supportsTransformOperation ( selectedTransformTarget , "translate" ) ;
const canRotateSelectedTarget = selectedTransformTarget !== null && supportsTransformOperation ( selectedTransformTarget , "rotate" ) ;
const canScaleSelectedTarget = selectedTransformTarget !== null && supportsTransformOperation ( selectedTransformTarget , "scale" ) ;
const whiteboxSnapStep = resolveOptionalPositiveNumber ( whiteboxSnapStepDraft , DEFAULT _GRID _SIZE ) ;
const whiteboxVectorInputStep = getWhiteboxInputStep ( whiteboxSnapEnabled , whiteboxSnapStep ) ;
useEffect ( ( ) => {
setSceneNameDraft ( editorState . document . name ) ;
} , [ editorState . document . name ] ) ;
useEffect ( ( ) => {
setBrushNameDraft ( selectedBrush ? . name ? ? "" ) ;
} , [ selectedBrush ] ) ;
useEffect ( ( ) => {
setEntityNameDraft ( selectedEntity ? . name ? ? "" ) ;
} , [ selectedEntity ] ) ;
useEffect ( ( ) => {
setModelInstanceNameDraft ( selectedModelInstance ? . name ? ? "" ) ;
} , [ selectedModelInstance ] ) ;
useEffect ( ( ) => {
if ( selectedBrush === null ) {
setPositionDraft ( createVec3Draft ( DEFAULT _BOX _BRUSH _CENTER ) ) ;
setRotationDraft ( createVec3Draft ( DEFAULT _BOX _BRUSH _ROTATION _DEGREES ) ) ;
setSizeDraft ( createVec3Draft ( DEFAULT _BOX _BRUSH _SIZE ) ) ;
return ;
}
setPositionDraft ( createVec3Draft ( selectedBrush . center ) ) ;
setRotationDraft ( createVec3Draft ( selectedBrush . rotationDegrees ) ) ;
setSizeDraft ( createVec3Draft ( selectedBrush . size ) ) ;
} , [ selectedBrush ] ) ;
useEffect ( ( ) => {
if ( selectedFace === null ) {
const defaultUvState = createDefaultFaceUvState ( ) ;
setUvOffsetDraft ( createVec2Draft ( defaultUvState . offset ) ) ;
setUvScaleDraft ( createVec2Draft ( defaultUvState . scale ) ) ;
return ;
}
setUvOffsetDraft ( createVec2Draft ( selectedFace . uv . offset ) ) ;
setUvScaleDraft ( createVec2Draft ( selectedFace . uv . scale ) ) ;
} , [ selectedFace ] ) ;
useEffect ( ( ) => {
if ( selectedEntity === null ) {
setEntityPositionDraft ( createVec3Draft ( DEFAULT _ENTITY _POSITION ) ) ;
setPointLightColorDraft ( DEFAULT _POINT _LIGHT _COLOR _HEX ) ;
setPointLightIntensityDraft ( String ( DEFAULT _POINT _LIGHT _INTENSITY ) ) ;
setPointLightDistanceDraft ( String ( DEFAULT _POINT _LIGHT _DISTANCE ) ) ;
setSpotLightColorDraft ( DEFAULT _SPOT _LIGHT _COLOR _HEX ) ;
setSpotLightIntensityDraft ( String ( DEFAULT _SPOT _LIGHT _INTENSITY ) ) ;
setSpotLightDistanceDraft ( String ( DEFAULT _SPOT _LIGHT _DISTANCE ) ) ;
setSpotLightAngleDraft ( String ( DEFAULT _SPOT _LIGHT _ANGLE _DEGREES ) ) ;
setSpotLightDirectionDraft ( createVec3Draft ( DEFAULT _SPOT _LIGHT _DIRECTION ) ) ;
setPlayerStartYawDraft ( "0" ) ;
setPlayerStartColliderModeDraft ( "capsule" ) ;
setPlayerStartEyeHeightDraft ( String ( DEFAULT _PLAYER _START _EYE _HEIGHT ) ) ;
setPlayerStartCapsuleRadiusDraft ( String ( DEFAULT _PLAYER _START _CAPSULE _RADIUS ) ) ;
setPlayerStartCapsuleHeightDraft ( String ( DEFAULT _PLAYER _START _CAPSULE _HEIGHT ) ) ;
setPlayerStartBoxSizeDraft ( createVec3Draft ( DEFAULT _PLAYER _START _BOX _SIZE ) ) ;
setSoundEmitterAudioAssetIdDraft ( DEFAULT _SOUND _EMITTER _AUDIO _ASSET _ID ? ? "" ) ;
setSoundEmitterVolumeDraft ( String ( DEFAULT _SOUND _EMITTER _VOLUME ) ) ;
setSoundEmitterRefDistanceDraft ( String ( DEFAULT _SOUND _EMITTER _REF _DISTANCE ) ) ;
setSoundEmitterMaxDistanceDraft ( String ( DEFAULT _SOUND _EMITTER _MAX _DISTANCE ) ) ;
setSoundEmitterAutoplayDraft ( false ) ;
setSoundEmitterLoopDraft ( false ) ;
setTriggerVolumeSizeDraft ( createVec3Draft ( DEFAULT _TRIGGER _VOLUME _SIZE ) ) ;
setTeleportTargetYawDraft ( String ( DEFAULT _TELEPORT _TARGET _YAW _DEGREES ) ) ;
setInteractableRadiusDraft ( String ( DEFAULT _INTERACTABLE _RADIUS ) ) ;
setInteractablePromptDraft ( DEFAULT _INTERACTABLE _PROMPT ) ;
setInteractableEnabledDraft ( true ) ;
return ;
}
setEntityPositionDraft ( createVec3Draft ( selectedEntity . position ) ) ;
switch ( selectedEntity . kind ) {
case "pointLight" :
setPointLightColorDraft ( selectedEntity . colorHex ) ;
setPointLightIntensityDraft ( String ( selectedEntity . intensity ) ) ;
setPointLightDistanceDraft ( String ( selectedEntity . distance ) ) ;
break ;
case "spotLight" :
setSpotLightColorDraft ( selectedEntity . colorHex ) ;
setSpotLightIntensityDraft ( String ( selectedEntity . intensity ) ) ;
setSpotLightDistanceDraft ( String ( selectedEntity . distance ) ) ;
setSpotLightAngleDraft ( String ( selectedEntity . angleDegrees ) ) ;
setSpotLightDirectionDraft ( createVec3Draft ( selectedEntity . direction ) ) ;
break ;
case "playerStart" :
setPlayerStartYawDraft ( String ( selectedEntity . yawDegrees ) ) ;
setPlayerStartColliderModeDraft ( selectedEntity . collider . mode ) ;
setPlayerStartEyeHeightDraft ( String ( selectedEntity . collider . eyeHeight ) ) ;
setPlayerStartCapsuleRadiusDraft ( String ( selectedEntity . collider . capsuleRadius ) ) ;
setPlayerStartCapsuleHeightDraft ( String ( selectedEntity . collider . capsuleHeight ) ) ;
setPlayerStartBoxSizeDraft ( createVec3Draft ( selectedEntity . collider . boxSize ) ) ;
break ;
case "soundEmitter" :
setSoundEmitterAudioAssetIdDraft ( selectedEntity . audioAssetId ? ? "" ) ;
setSoundEmitterVolumeDraft ( String ( selectedEntity . volume ) ) ;
setSoundEmitterRefDistanceDraft ( String ( selectedEntity . refDistance ) ) ;
setSoundEmitterMaxDistanceDraft ( String ( selectedEntity . maxDistance ) ) ;
setSoundEmitterAutoplayDraft ( selectedEntity . autoplay ) ;
setSoundEmitterLoopDraft ( selectedEntity . loop ) ;
break ;
case "triggerVolume" :
setTriggerVolumeSizeDraft ( createVec3Draft ( selectedEntity . size ) ) ;
break ;
case "teleportTarget" :
setTeleportTargetYawDraft ( String ( selectedEntity . yawDegrees ) ) ;
break ;
case "interactable" :
setInteractableRadiusDraft ( String ( selectedEntity . radius ) ) ;
setInteractablePromptDraft ( selectedEntity . prompt ) ;
setInteractableEnabledDraft ( selectedEntity . enabled ) ;
break ;
}
} , [ selectedEntity ] ) ;
useEffect ( ( ) => {
if ( selectedModelInstance === null ) {
setModelPositionDraft ( createVec3Draft ( DEFAULT _MODEL _INSTANCE _POSITION ) ) ;
setModelRotationDraft ( createVec3Draft ( DEFAULT _MODEL _INSTANCE _ROTATION _DEGREES ) ) ;
setModelScaleDraft ( createVec3Draft ( DEFAULT _MODEL _INSTANCE _SCALE ) ) ;
return ;
}
setModelPositionDraft ( createVec3Draft ( selectedModelInstance . position ) ) ;
setModelRotationDraft ( createVec3Draft ( selectedModelInstance . rotationDegrees ) ) ;
setModelScaleDraft ( createVec3Draft ( selectedModelInstance . scale ) ) ;
} , [ selectedModelInstance ] ) ;
useEffect ( ( ) => {
setAmbientLightIntensityDraft ( String ( editorState . document . world . ambientLight . intensity ) ) ;
} , [ editorState . document . world . ambientLight . intensity ] ) ;
useEffect ( ( ) => {
if ( editorState . document . world . background . mode === "image" ) {
setBackgroundEnvironmentIntensityDraft ( String ( editorState . document . world . background . environmentIntensity ) ) ;
}
} , [ editorState . document . world . background ] ) ;
useEffect ( ( ) => {
setSunLightIntensityDraft ( String ( editorState . document . world . sunLight . intensity ) ) ;
} , [ editorState . document . world . sunLight . intensity ] ) ;
useEffect ( ( ) => {
setSunDirectionDraft ( createVec3Draft ( editorState . document . world . sunLight . direction ) ) ;
} , [ editorState . document . world . sunLight . direction ] ) ;
useEffect ( ( ) => {
const advancedRendering = editorState . document . world . advancedRendering ;
setAdvancedRenderingShadowBiasDraft ( String ( advancedRendering . shadows . bias ) ) ;
setAdvancedRenderingAmbientOcclusionIntensityDraft ( String ( advancedRendering . ambientOcclusion . intensity ) ) ;
setAdvancedRenderingAmbientOcclusionRadiusDraft ( String ( advancedRendering . ambientOcclusion . radius ) ) ;
setAdvancedRenderingAmbientOcclusionSamplesDraft ( String ( advancedRendering . ambientOcclusion . samples ) ) ;
setAdvancedRenderingBloomIntensityDraft ( String ( advancedRendering . bloom . intensity ) ) ;
setAdvancedRenderingBloomThresholdDraft ( String ( advancedRendering . bloom . threshold ) ) ;
setAdvancedRenderingBloomRadiusDraft ( String ( advancedRendering . bloom . radius ) ) ;
setAdvancedRenderingToneMappingExposureDraft ( String ( advancedRendering . toneMapping . exposure ) ) ;
setAdvancedRenderingDepthOfFieldFocusDistanceDraft ( String ( advancedRendering . depthOfField . focusDistance ) ) ;
setAdvancedRenderingDepthOfFieldFocalLengthDraft ( String ( advancedRendering . depthOfField . focalLength ) ) ;
setAdvancedRenderingDepthOfFieldBokehScaleDraft ( String ( advancedRendering . depthOfField . bokehScale ) ) ;
} , [ editorState . document . world . advancedRendering ] ) ;
useEffect ( ( ) => {
loadedImageAssetsRef . current = loadedImageAssets ;
} , [ loadedImageAssets ] ) ;
useEffect ( ( ) => {
loadedModelAssetsRef . current = loadedModelAssets ;
} , [ loadedModelAssets ] ) ;
useEffect ( ( ) => {
loadedAudioAssetsRef . current = loadedAudioAssets ;
} , [ loadedAudioAssets ] ) ;
useEffect ( ( ) => {
viewportQuadSplitRef . current = editorState . viewportQuadSplit ;
} , [ editorState . viewportQuadSplit ] ) ;
useEffect ( ( ) => {
let cancelled = false ;
void ( async ( ) => {
const access = await getBrowserProjectAssetStorageAccess ( ) ;
if ( cancelled ) {
return ;
}
setProjectAssetStorage ( access . storage ) ;
setAssetStatusMessage ( access . diagnostic ) ;
setProjectAssetStorageReady ( true ) ;
} ) ( ) . catch ( ( error ) => {
if ( cancelled ) {
return ;
}
setProjectAssetStorage ( null ) ;
setProjectAssetStorageReady ( true ) ;
setAssetStatusMessage ( getErrorMessage ( error ) ) ;
} ) ;
return ( ) => {
cancelled = true ;
} ;
} , [ ] ) ;
useEffect ( ( ) => {
if ( ! projectAssetStorageReady ) {
return ;
}
let cancelled = false ;
const currentAssets = editorState . document . assets ;
const previousLoadedModelAssets = loadedModelAssetsRef . current ;
const previousLoadedImageAssets = loadedImageAssetsRef . current ;
const previousLoadedAudioAssets = loadedAudioAssetsRef . current ;
const previousLoadedModelAssetIds = new Set ( Object . keys ( previousLoadedModelAssets ) ) ;
const previousLoadedImageAssetIds = new Set ( Object . keys ( previousLoadedImageAssets ) ) ;
const previousLoadedAudioAssetIds = new Set ( Object . keys ( previousLoadedAudioAssets ) ) ;
const nextLoadedModelAssets = { } ;
const nextLoadedImageAssets = { } ;
const nextLoadedAudioAssets = { } ;
const syncErrorMessages = [ ] ;
const syncAssets = async ( ) => {
if ( projectAssetStorage === null ) {
for ( const loadedAsset of Object . values ( previousLoadedModelAssets ) ) {
disposeModelTemplate ( loadedAsset . template ) ;
}
for ( const loadedAsset of Object . values ( previousLoadedImageAssets ) ) {
disposeLoadedImageAsset ( loadedAsset ) ;
}
if ( ! cancelled ) {
loadedModelAssetsRef . current = { } ;
loadedImageAssetsRef . current = { } ;
loadedAudioAssetsRef . current = { } ;
setLoadedModelAssets ( { } ) ;
setLoadedImageAssets ( { } ) ;
setLoadedAudioAssets ( { } ) ;
}
return ;
}
for ( const asset of Object . values ( currentAssets ) ) {
if ( isModelAsset ( asset ) ) {
previousLoadedModelAssetIds . delete ( asset . id ) ;
const cachedLoadedAsset = previousLoadedModelAssets [ asset . id ] ;
if ( cachedLoadedAsset !== undefined && cachedLoadedAsset . storageKey === asset . storageKey ) {
nextLoadedModelAssets [ asset . id ] = cachedLoadedAsset ;
continue ;
}
try {
nextLoadedModelAssets [ asset . id ] = await loadModelAssetFromStorage ( projectAssetStorage , asset ) ;
}
catch ( error ) {
syncErrorMessages . push ( ` Model asset ${ asset . sourceName } could not be restored: ${ getErrorMessage ( error ) } ` ) ;
}
continue ;
}
if ( isImageAsset ( asset ) ) {
previousLoadedImageAssetIds . delete ( asset . id ) ;
const cachedLoadedAsset = previousLoadedImageAssets [ asset . id ] ;
if ( cachedLoadedAsset !== undefined && cachedLoadedAsset . storageKey === asset . storageKey ) {
nextLoadedImageAssets [ asset . id ] = cachedLoadedAsset ;
continue ;
}
try {
nextLoadedImageAssets [ asset . id ] = await loadImageAssetFromStorage ( projectAssetStorage , asset ) ;
}
catch ( error ) {
syncErrorMessages . push ( ` Image asset ${ asset . sourceName } could not be restored: ${ getErrorMessage ( error ) } ` ) ;
}
continue ;
}
if ( isAudioAsset ( asset ) ) {
previousLoadedAudioAssetIds . delete ( asset . id ) ;
const cachedLoadedAsset = previousLoadedAudioAssets [ asset . id ] ;
if ( cachedLoadedAsset !== undefined && cachedLoadedAsset . storageKey === asset . storageKey ) {
nextLoadedAudioAssets [ asset . id ] = cachedLoadedAsset ;
continue ;
}
try {
nextLoadedAudioAssets [ asset . id ] = await loadAudioAssetFromStorage ( projectAssetStorage , asset ) ;
}
catch ( error ) {
syncErrorMessages . push ( ` Audio asset ${ asset . sourceName } could not be restored: ${ getErrorMessage ( error ) } ` ) ;
}
}
}
if ( cancelled ) {
for ( const loadedAsset of Object . values ( nextLoadedModelAssets ) ) {
if ( previousLoadedModelAssets [ loadedAsset . assetId ] !== loadedAsset ) {
disposeModelTemplate ( loadedAsset . template ) ;
}
}
for ( const loadedAsset of Object . values ( nextLoadedImageAssets ) ) {
if ( previousLoadedImageAssets [ loadedAsset . assetId ] !== loadedAsset ) {
disposeLoadedImageAsset ( loadedAsset ) ;
}
}
return ;
}
for ( const assetId of previousLoadedModelAssetIds ) {
const removedAsset = previousLoadedModelAssets [ assetId ] ;
if ( removedAsset !== undefined ) {
disposeModelTemplate ( removedAsset . template ) ;
}
}
for ( const assetId of previousLoadedImageAssetIds ) {
const removedAsset = previousLoadedImageAssets [ assetId ] ;
if ( removedAsset !== undefined ) {
disposeLoadedImageAsset ( removedAsset ) ;
}
}
loadedModelAssetsRef . current = nextLoadedModelAssets ;
loadedImageAssetsRef . current = nextLoadedImageAssets ;
loadedAudioAssetsRef . current = nextLoadedAudioAssets ;
setLoadedModelAssets ( nextLoadedModelAssets ) ;
setLoadedImageAssets ( nextLoadedImageAssets ) ;
setLoadedAudioAssets ( nextLoadedAudioAssets ) ;
setAssetStatusMessage ( syncErrorMessages . length === 0 ? null : syncErrorMessages . join ( " | " ) ) ;
} ;
void syncAssets ( ) ;
return ( ) => {
cancelled = true ;
} ;
} , [ editorState . document . assets , projectAssetStorage , projectAssetStorageReady ] ) ;
useEffect ( ( ) => {
if ( editorState . toolMode === "play" ) {
return ;
}
const handleWindowPointerMove = ( event ) => {
lastPointerPositionRef . current = {
x : event . clientX ,
y : event . clientY
} ;
const hoveredViewportPanelElement = event . target instanceof Element ? event . target . closest ( "[data-viewport-panel-id]" ) : null ;
const hoveredPanelId = hoveredViewportPanelElement ? . dataset . viewportPanelId ;
setHoveredViewportPanelId ( hoveredPanelId === "topLeft" || hoveredPanelId === "topRight" || hoveredPanelId === "bottomLeft" || hoveredPanelId === "bottomRight"
? hoveredPanelId
: null ) ;
} ;
const handleWindowKeyDown = ( event ) => {
if ( isTextEntryTarget ( event . target ) ) {
return ;
}
const hasPrimaryModifier = ( event . metaKey || event . ctrlKey ) && ! event . altKey ;
if ( hasPrimaryModifier && event . code === "KeyR" && ! event . shiftKey ) {
event . preventDefault ( ) ;
handleEnterPlayMode ( ) ;
return ;
}
if ( hasPrimaryModifier && event . code === "KeyS" && ! event . shiftKey ) {
event . preventDefault ( ) ;
handleSaveDraft ( ) ;
return ;
}
if ( hasPrimaryModifier && event . code === "KeyZ" ) {
event . preventDefault ( ) ;
if ( event . shiftKey ) {
if ( store . redo ( ) ) {
setStatusMessage ( "Redid the last action." ) ;
}
else {
setStatusMessage ( "Nothing to redo." ) ;
}
}
else if ( store . undo ( ) ) {
setStatusMessage ( "Undid the last action." ) ;
}
else {
setStatusMessage ( "Nothing to undo." ) ;
}
return ;
}
if ( hasPrimaryModifier && event . code === "KeyY" ) {
event . preventDefault ( ) ;
if ( store . redo ( ) ) {
setStatusMessage ( "Redid the last action." ) ;
}
else {
setStatusMessage ( "Nothing to redo." ) ;
}
return ;
}
if ( event . key === "Escape" && addMenuPosition !== null ) {
event . preventDefault ( ) ;
setAddMenuPosition ( null ) ;
return ;
}
if ( transformSession . kind === "active" ) {
if ( event . key === "Escape" ) {
event . preventDefault ( ) ;
cancelTransformSession ( ) ;
return ;
}
if ( event . key === "Enter" ) {
event . preventDefault ( ) ;
commitTransformSession ( transformSession ) ;
return ;
}
if ( ! event . altKey && ! event . ctrlKey && ! event . metaKey ) {
if ( event . code === "KeyX" ) {
event . preventDefault ( ) ;
applyTransformAxisConstraint ( "x" ) ;
return ;
}
if ( event . code === "KeyY" ) {
event . preventDefault ( ) ;
applyTransformAxisConstraint ( "y" ) ;
return ;
}
if ( event . code === "KeyZ" ) {
event . preventDefault ( ) ;
applyTransformAxisConstraint ( "z" ) ;
return ;
}
}
}
if ( event . key === "Escape" && editorState . toolMode === "create" ) {
event . preventDefault ( ) ;
store . setToolMode ( "select" ) ;
setStatusMessage ( "Cancelled the current creation preview." ) ;
return ;
}
if ( event . shiftKey && event . code === "KeyA" ) {
event . preventDefault ( ) ;
setAddMenuPosition ( {
x : lastPointerPositionRef . current . x ,
y : lastPointerPositionRef . current . y
} ) ;
return ;
}
if ( ! event . altKey && ! event . ctrlKey && ! event . metaKey ) {
let transformOperation = null ;
if ( event . code === "KeyG" ) {
transformOperation = "translate" ;
}
else if ( event . code === "KeyR" ) {
transformOperation = "rotate" ;
}
else if ( event . code === "KeyS" ) {
transformOperation = "scale" ;
}
if ( transformOperation !== null ) {
event . preventDefault ( ) ;
beginTransformOperation ( transformOperation , "keyboard" ) ;
return ;
}
}
const isDeletionKey = event . key === "Delete" || event . key === "Backspace" ;
const isDeleteShortcut = ! event . altKey && ! event . ctrlKey && ! event . metaKey && ( event . code === "KeyX" || isDeletionKey ) ;
if ( addMenuPosition !== null ) {
if ( isDeletionKey ) {
event . preventDefault ( ) ;
}
return ;
}
if ( isDeleteShortcut ) {
if ( editorState . toolMode !== "create" ) {
const deleted = handleDeleteSelectedSceneItem ( ) ;
if ( deleted || isDeletionKey ) {
event . preventDefault ( ) ;
}
}
else if ( isDeletionKey ) {
event . preventDefault ( ) ;
}
return ;
}
if ( event . code !== "NumpadComma" &&
! ( event . key === "," && event . location === globalThis . KeyboardEvent . DOM _KEY _LOCATION _NUMPAD ) ) {
return ;
}
event . preventDefault ( ) ;
if ( editorState . selection . kind === "none" && brushList . length === 0 && entityList . length === 0 ) {
setStatusMessage ( "Nothing authored yet to frame in the viewport." ) ;
return ;
}
setFocusRequest ( ( current ) => ( {
id : current . id + 1 ,
panelId : activePanelId ,
selection : editorState . selection
} ) ) ;
setStatusMessage ( editorState . selection . kind === "none" ? "Framed the authored scene in the viewport." : "Framed the current selection." ) ;
} ;
document . addEventListener ( "pointermove" , handleWindowPointerMove ) ;
window . addEventListener ( "pointermove" , handleWindowPointerMove ) ;
window . addEventListener ( "keydown" , handleWindowKeyDown ) ;
return ( ) => {
document . removeEventListener ( "pointermove" , handleWindowPointerMove ) ;
window . removeEventListener ( "pointermove" , handleWindowPointerMove ) ;
window . removeEventListener ( "keydown" , handleWindowKeyDown ) ;
} ;
} , [ activePanelId , addMenuPosition , brushList . length , editorState . selection , editorState . toolMode , entityList . length , hoveredViewportPanelId , layoutMode , transformSession ] ) ;
useEffect ( ( ) => {
if ( layoutMode === "quad" || viewportQuadResizeMode === null ) {
return ;
}
setViewportQuadResizeMode ( null ) ;
} , [ layoutMode , viewportQuadResizeMode ] ) ;
useEffect ( ( ) => {
if ( layoutMode !== "quad" || viewportQuadResizeMode === null ) {
return ;
}
const previousCursor = document . body . style . cursor ;
const previousUserSelect = document . body . style . userSelect ;
document . body . style . cursor = getViewportQuadResizeCursor ( viewportQuadResizeMode ) ;
document . body . style . userSelect = "none" ;
const handlePointerMove = ( event ) => {
const viewportPanels = viewportPanelsRef . current ;
if ( viewportPanels === null ) {
return ;
}
const rect = viewportPanels . getBoundingClientRect ( ) ;
if ( rect . width <= 0 || rect . height <= 0 ) {
return ;
}
const nextViewportQuadSplit = {
... viewportQuadSplitRef . current
} ;
if ( viewportQuadResizeMode !== "horizontal" ) {
nextViewportQuadSplit . x = clampViewportQuadSplitValue ( ( event . clientX - rect . left ) / rect . width ) ;
}
if ( viewportQuadResizeMode !== "vertical" ) {
nextViewportQuadSplit . y = clampViewportQuadSplitValue ( ( event . clientY - rect . top ) / rect . height ) ;
}
store . setViewportQuadSplit ( nextViewportQuadSplit ) ;
} ;
const stopViewportResize = ( ) => {
setViewportQuadResizeMode ( null ) ;
} ;
window . addEventListener ( "pointermove" , handlePointerMove ) ;
window . addEventListener ( "pointerup" , stopViewportResize ) ;
window . addEventListener ( "pointercancel" , stopViewportResize ) ;
return ( ) => {
document . body . style . cursor = previousCursor ;
document . body . style . userSelect = previousUserSelect ;
window . removeEventListener ( "pointermove" , handlePointerMove ) ;
window . removeEventListener ( "pointerup" , stopViewportResize ) ;
window . removeEventListener ( "pointercancel" , stopViewportResize ) ;
} ;
} , [ layoutMode , store , viewportQuadResizeMode ] ) ;
useEffect ( ( ) => {
if ( editorState . toolMode !== "play" ) {
return ;
}
const handleWindowKeyDown = ( event ) => {
if ( isTextEntryTarget ( event . target ) ) {
return ;
}
if ( event . key !== "Escape" ) {
return ;
}
const pointerCaptured = activeNavigationMode === "firstPerson" && firstPersonTelemetry ? . pointerLocked === true ;
if ( pointerCaptured ) {
return ;
}
event . preventDefault ( ) ;
handleExitPlayMode ( ) ;
} ;
window . addEventListener ( "keydown" , handleWindowKeyDown ) ;
return ( ) => {
window . removeEventListener ( "keydown" , handleWindowKeyDown ) ;
} ;
} , [ activeNavigationMode , editorState . toolMode , firstPersonTelemetry ] ) ;
const applySceneName = ( ) => {
const normalizedName = sceneNameDraft . trim ( ) || "Untitled Scene" ;
if ( normalizedName === editorState . document . name ) {
return ;
}
store . executeCommand ( createSetSceneNameCommand ( normalizedName ) ) ;
setStatusMessage ( ` Scene renamed to ${ normalizedName } . ` ) ;
} ;
const requestViewportFocus = ( selection , status ) => {
setFocusRequest ( ( current ) => ( {
id : current . id + 1 ,
panelId : activePanelId ,
selection
} ) ) ;
if ( status !== undefined ) {
setStatusMessage ( status ) ;
}
} ;
const openAddMenuAt = ( position ) => {
setHoveredAssetId ( null ) ;
setAddMenuPosition ( position ) ;
} ;
const closeAddMenu = ( ) => {
setHoveredAssetId ( null ) ;
setAddMenuPosition ( null ) ;
} ;
const handleOpenAddMenuFromButton = ( event ) => {
const rect = event . currentTarget . getBoundingClientRect ( ) ;
openAddMenuAt ( {
x : rect . left ,
y : rect . bottom + 8
} ) ;
} ;
const handleSetViewportLayoutMode = ( nextLayoutMode ) => {
if ( editorState . viewportLayoutMode === nextLayoutMode ) {
return ;
}
blurActiveTextEntry ( ) ;
store . setViewportLayoutMode ( nextLayoutMode ) ;
setStatusMessage ( ` Switched the viewport to ${ getViewportLayoutModeLabel ( nextLayoutMode ) } . ` ) ;
} ;
const handleActivateViewportPanel = ( panelId ) => {
if ( editorState . activeViewportPanelId === panelId ) {
return ;
}
blurActiveTextEntry ( ) ;
store . setActiveViewportPanel ( panelId ) ;
setStatusMessage ( "Activated the viewport panel." ) ;
} ;
const handleSetViewportPanelViewMode = ( panelId , nextViewMode ) => {
if ( editorState . viewportPanels [ panelId ] . viewMode === nextViewMode ) {
return ;
}
blurActiveTextEntry ( ) ;
store . setViewportPanelViewMode ( panelId , nextViewMode ) ;
setStatusMessage ( ` Set the viewport panel to ${ getViewportViewModeLabel ( nextViewMode ) } view. ` ) ;
} ;
const handleSetViewportPanelDisplayMode = ( panelId , nextDisplayMode ) => {
if ( editorState . viewportPanels [ panelId ] . displayMode === nextDisplayMode ) {
return ;
}
blurActiveTextEntry ( ) ;
store . setViewportPanelDisplayMode ( panelId , nextDisplayMode ) ;
setStatusMessage ( ` Set the viewport panel to ${ getViewportDisplayModeLabel ( nextDisplayMode ) } display. ` ) ;
} ;
const beginTransformOperation = ( operation , source ) => {
if ( editorState . toolMode !== "select" ) {
return ;
}
const transformSourcePanelId = layoutMode === "quad" ? hoveredViewportPanelId ? ? activePanelId : activePanelId ;
const transformTargetResult = resolveTransformTarget ( editorState . document , editorState . selection , whiteboxSelectionMode ) ;
const transformTarget = transformTargetResult . target ;
if ( transformTarget === null ) {
setStatusMessage ( transformTargetResult . message ? ? "Select a single brush, entity, or model instance before transforming it." ) ;
return ;
}
if ( ! supportsTransformOperation ( transformTarget , operation ) ) {
setStatusMessage ( ` ${ getTransformOperationLabel ( operation ) } is not supported for ${ getTransformTargetLabel ( transformTarget ) } . ` ) ;
return ;
}
blurActiveTextEntry ( ) ;
closeAddMenu ( ) ;
if ( editorState . activeViewportPanelId !== transformSourcePanelId ) {
store . setActiveViewportPanel ( transformSourcePanelId ) ;
}
store . setTransformSession ( createTransformSession ( {
source ,
sourcePanelId : transformSourcePanelId ,
operation ,
target : transformTarget
} ) ) ;
setStatusMessage ( ` ${ getTransformOperationLabel ( operation ) } ${ getTransformTargetLabel ( transformTarget ) . toLowerCase ( ) } in ${ getViewportPanelLabel ( transformSourcePanelId ) } . Move the pointer, press X/Y/Z to constrain, click or press Enter to commit, Escape cancels. ` ) ;
} ;
const cancelTransformSession = ( status = "Cancelled the current transform." ) => {
if ( transformSession . kind === "none" ) {
return ;
}
store . clearTransformSession ( ) ;
setStatusMessage ( status ) ;
} ;
const commitTransformSession = ( activeTransformSession ) => {
if ( ! doesTransformSessionChangeTarget ( activeTransformSession ) ) {
store . clearTransformSession ( ) ;
setStatusMessage ( "No transform change was committed." ) ;
return ;
}
try {
store . clearTransformSession ( ) ;
store . executeCommand ( createCommitTransformSessionCommand ( editorState . document , activeTransformSession ) ) ;
setStatusMessage ( ` ${ getTransformOperationPastTense ( activeTransformSession . operation ) } ${ getTransformTargetLabel ( activeTransformSession . target ) . toLowerCase ( ) } . ` ) ;
}
catch ( error ) {
store . clearTransformSession ( ) ;
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyTransformAxisConstraint = ( axis ) => {
if ( transformSession . kind !== "active" ) {
return ;
}
if ( ! supportsTransformAxisConstraint ( transformSession , axis ) ) {
const supportedAxes = [ "x" , "y" , "z" ]
. filter ( ( candidateAxis ) => supportsTransformAxisConstraint ( transformSession , candidateAxis ) )
. map ( ( candidateAxis ) => candidateAxis . toUpperCase ( ) )
. join ( "/" ) ;
setStatusMessage ( supportedAxes . length === 0
? ` ${ getTransformOperationLabel ( transformSession . operation ) } does not support axis constraints for ${ getTransformTargetLabel ( transformSession . target ) } . `
: ` ${ getTransformOperationLabel ( transformSession . operation ) } on ${ getTransformTargetLabel ( transformSession . target ) } only supports ${ supportedAxes } . ` ) ;
return ;
}
store . setTransformAxisConstraint ( axis ) ;
setStatusMessage ( ` Constrained ${ getTransformOperationLabel ( transformSession . operation ) . toLowerCase ( ) } to ${ axis . toUpperCase ( ) } . ` ) ;
} ;
const handleViewportQuadResizeStart = ( resizeMode ) => ( event ) => {
if ( layoutMode !== "quad" ) {
return ;
}
const viewportPanels = viewportPanelsRef . current ;
if ( viewportPanels === null ) {
return ;
}
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
blurActiveTextEntry ( ) ;
const rect = viewportPanels . getBoundingClientRect ( ) ;
if ( rect . width > 0 && rect . height > 0 ) {
const nextViewportQuadSplit = {
... viewportQuadSplitRef . current
} ;
if ( resizeMode !== "horizontal" ) {
nextViewportQuadSplit . x = clampViewportQuadSplitValue ( ( event . clientX - rect . left ) / rect . width ) ;
}
if ( resizeMode !== "vertical" ) {
nextViewportQuadSplit . y = clampViewportQuadSplitValue ( ( event . clientY - rect . top ) / rect . height ) ;
}
store . setViewportQuadSplit ( nextViewportQuadSplit ) ;
}
setViewportQuadResizeMode ( resizeMode ) ;
} ;
const beginCreation = ( toolPreview , status ) => {
blurActiveTextEntry ( ) ;
closeAddMenu ( ) ;
store . setToolMode ( "create" ) ;
store . setViewportToolPreview ( toolPreview ) ;
setStatusMessage ( status ) ;
} ;
const completeCreation = ( status ) => {
store . setToolMode ( "select" ) ;
store . clearViewportToolPreview ( ) ;
setStatusMessage ( status ) ;
} ;
const beginBoxCreation = ( ) => {
beginCreation ( {
kind : "create" ,
sourcePanelId : activePanelId ,
target : {
kind : "box-brush"
} ,
center : null
} , ` Previewing a whitebox box. Click in the viewport to create it ${ whiteboxSnapEnabled ? ` on the ${ whiteboxSnapStep } m grid ` : "" } . ` ) ;
} ;
const handleWhiteboxSnapToggle = ( ) => {
const nextEnabled = ! whiteboxSnapEnabled ;
setWhiteboxSnapEnabled ( nextEnabled ) ;
setStatusMessage ( nextEnabled ? ` Grid snap enabled at ${ whiteboxSnapStep } m. ` : "Grid snap disabled for whitebox transforms." ) ;
} ;
const handleWhiteboxSnapStepBlur = ( ) => {
const normalizedStep = resolveOptionalPositiveNumber ( whiteboxSnapStepDraft , DEFAULT _GRID _SIZE ) ;
setWhiteboxSnapStepDraft ( String ( normalizedStep ) ) ;
} ;
const handleWhiteboxSelectionModeChange = ( mode ) => {
if ( whiteboxSelectionMode === mode ) {
return ;
}
blurActiveTextEntry ( ) ;
store . setWhiteboxSelectionMode ( mode ) ;
setStatusMessage ( getWhiteboxSelectionModeStatus ( mode ) ) ;
} ;
const applySelection = ( selection , source , options = { } ) => {
blurActiveTextEntry ( ) ;
store . setSelection ( selection ) ;
const suffix = source === "outliner" && options . focusViewport ? " and framed it in the viewport" : "" ;
switch ( selection . kind ) {
case "none" :
setStatusMessage ( ` ${ source === "viewport" ? "Viewport" : "Editor" } selection cleared ${ suffix } . ` ) ;
break ;
case "brushes" :
setStatusMessage ( ` Selected ${ getBrushLabelById ( selection . ids [ 0 ] , brushList ) } from the ${ source } ${ suffix } . ` ) ;
break ;
case "brushFace" :
setStatusMessage ( ` Selected ${ BOX _FACE _LABELS [ selection . faceId ] } on ${ getBrushLabelById ( selection . brushId , brushList ) } from the ${ source } ${ suffix } . ` ) ;
break ;
case "brushEdge" :
setStatusMessage ( ` Selected ${ BOX _EDGE _LABELS [ selection . edgeId ] } on ${ getBrushLabelById ( selection . brushId , brushList ) } from the ${ source } ${ suffix } . ` ) ;
break ;
case "brushVertex" :
setStatusMessage ( ` Selected ${ BOX _VERTEX _LABELS [ selection . vertexId ] } on ${ getBrushLabelById ( selection . brushId , brushList ) } from the ${ source } ${ suffix } . ` ) ;
break ;
case "entities" :
setStatusMessage ( ` Selected ${ getEntityDisplayLabelById ( selection . ids [ 0 ] , editorState . document . entities , editorState . document . assets ) } from the ${ source } ${ suffix } . ` ) ;
break ;
case "modelInstances" :
setStatusMessage ( ` Selected ${ getModelInstanceDisplayLabelById ( selection . ids [ 0 ] , editorState . document . modelInstances , editorState . document . assets ) } from the ${ source } ${ suffix } . ` ) ;
break ;
default :
setStatusMessage ( ` Selection updated from the ${ source } ${ suffix } . ` ) ;
break ;
}
if ( options . focusViewport ) {
requestViewportFocus ( selection ) ;
}
} ;
const applyPositionChange = ( ) => {
if ( selectedBrush === null || editorState . selection . kind !== "brushes" || whiteboxSelectionMode !== "object" ) {
setStatusMessage ( "Switch to Object mode and select a whitebox box before moving it." ) ;
return ;
}
try {
const nextCenter = maybeSnapVec3 ( readVec3Draft ( positionDraft , "Whitebox box position" ) , whiteboxSnapEnabled , whiteboxSnapStep ) ;
if ( areVec3Equal ( nextCenter , selectedBrush . center ) ) {
return ;
}
store . executeCommand ( createMoveBoxBrushCommand ( {
brushId : selectedBrush . id ,
center : nextCenter ,
snapToGrid : false
} ) ) ;
setStatusMessage ( "Moved selected whitebox box." ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyRotationChange = ( ) => {
if ( selectedBrush === null || editorState . selection . kind !== "brushes" || whiteboxSelectionMode !== "object" ) {
setStatusMessage ( "Switch to Object mode and select a whitebox box before rotating it." ) ;
return ;
}
try {
const nextRotationDegrees = readVec3Draft ( rotationDraft , "Whitebox box rotation" ) ;
if ( areVec3Equal ( nextRotationDegrees , selectedBrush . rotationDegrees ) ) {
return ;
}
store . executeCommand ( createRotateBoxBrushCommand ( {
brushId : selectedBrush . id ,
rotationDegrees : nextRotationDegrees
} ) ) ;
setStatusMessage ( "Rotated selected whitebox box." ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applySizeChange = ( ) => {
if ( selectedBrush === null || editorState . selection . kind !== "brushes" || whiteboxSelectionMode !== "object" ) {
setStatusMessage ( "Switch to Object mode and select a whitebox box before scaling it." ) ;
return ;
}
try {
const nextSize = maybeSnapPositiveSize ( readVec3Draft ( sizeDraft , "Whitebox box size" ) , whiteboxSnapEnabled , whiteboxSnapStep ) ;
if ( areVec3Equal ( nextSize , selectedBrush . size ) ) {
return ;
}
store . executeCommand ( createResizeBoxBrushCommand ( {
brushId : selectedBrush . id ,
size : nextSize ,
snapToGrid : false
} ) ) ;
setStatusMessage ( "Scaled selected whitebox box." ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const commitEntityChange = ( currentEntity , nextEntity , successMessage ) => {
if ( areEntityInstancesEqual ( currentEntity , nextEntity ) ) {
return ;
}
store . executeCommand ( createUpsertEntityCommand ( {
entity : nextEntity ,
label : ` Update ${ getEntityKindLabel ( nextEntity . kind ) . toLowerCase ( ) } `
} ) ) ;
setStatusMessage ( successMessage ) ;
} ;
const beginEntityCreation = ( kind , options = { } ) => {
beginCreation ( {
kind : "create" ,
sourcePanelId : activePanelId ,
target : {
kind : "entity" ,
entityKind : kind ,
audioAssetId : options . audioAssetId ? ? null
} ,
center : null
} , ` Previewing ${ getEntityKindLabel ( kind ) } . Click in the viewport to place it. ` ) ;
} ;
const beginModelInstanceCreation = ( assetId ) => {
const asset = editorState . document . assets [ assetId ] ;
if ( asset === undefined || asset . kind !== "model" ) {
setStatusMessage ( "Select a model asset before placing a model instance." ) ;
return ;
}
beginCreation ( {
kind : "create" ,
sourcePanelId : activePanelId ,
target : {
kind : "model-instance" ,
assetId : asset . id
} ,
center : null
} , ` Previewing ${ asset . sourceName } . Click in the viewport to place it. ` ) ;
} ;
const handleCommitCreation = ( creationPreview ) => {
try {
if ( creationPreview . target . kind === "box-brush" ) {
const center = creationPreview . center === null ? undefined : creationPreview . center ;
store . executeCommand ( createCreateBoxBrushCommand ( center === undefined
? {
snapToGrid : whiteboxSnapEnabled ,
gridSize : whiteboxSnapStep
}
: {
center ,
snapToGrid : whiteboxSnapEnabled ,
gridSize : whiteboxSnapStep
} ) ) ;
completeCreation ( center === undefined
? whiteboxSnapEnabled
? ` Created a whitebox box on the ${ whiteboxSnapStep } m grid. `
: "Created a whitebox box."
: whiteboxSnapEnabled
? ` Created a whitebox box at snapped center ${ formatVec3 ( center ) } . `
: ` Created a whitebox box at ${ formatVec3 ( center ) } . ` ) ;
return true ;
}
if ( creationPreview . target . kind === "model-instance" ) {
const asset = editorState . document . assets [ creationPreview . target . assetId ] ;
if ( asset === undefined || asset . kind !== "model" ) {
setStatusMessage ( "Select a model asset before placing a model instance." ) ;
return false ;
}
const nextModelInstance = createModelInstance ( {
assetId : asset . id ,
position : creationPreview . center === null ? createModelInstancePlacementPosition ( asset , null ) : creationPreview . center ,
rotationDegrees : DEFAULT _MODEL _INSTANCE _ROTATION _DEGREES ,
scale : DEFAULT _MODEL _INSTANCE _SCALE
} ) ;
store . executeCommand ( createUpsertModelInstanceCommand ( {
modelInstance : nextModelInstance ,
label : ` Place ${ asset . sourceName } `
} ) ) ;
completeCreation ( ` Placed ${ asset . sourceName } . ` ) ;
return true ;
}
const position = creationPreview . center ? ? DEFAULT _ENTITY _POSITION ;
switch ( creationPreview . target . entityKind ) {
case "pointLight" :
store . executeCommand ( createUpsertEntityCommand ( {
entity : createPointLightEntity ( {
position
} ) ,
label : "Place point light"
} ) ) ;
completeCreation ( "Placed Point Light." ) ;
return true ;
case "spotLight" :
store . executeCommand ( createUpsertEntityCommand ( {
entity : createSpotLightEntity ( {
position
} ) ,
label : "Place spot light"
} ) ) ;
completeCreation ( "Placed Spot Light." ) ;
return true ;
case "playerStart" :
store . executeCommand ( createUpsertEntityCommand ( {
entity : createPlayerStartEntity ( {
position
} ) ,
label : "Place player start"
} ) ) ;
completeCreation ( "Placed Player Start." ) ;
return true ;
case "soundEmitter" : {
const placedAudioAssetId = creationPreview . target . audioAssetId ? ? audioAssetList [ 0 ] ? . id ? ? null ;
store . executeCommand ( createUpsertEntityCommand ( {
entity : createSoundEmitterEntity ( {
position ,
audioAssetId : placedAudioAssetId
} ) ,
label : "Place sound emitter"
} ) ) ;
completeCreation ( placedAudioAssetId === null
? "Placed Sound Emitter."
: ` Placed Sound Emitter using ${ editorState . document . assets [ placedAudioAssetId ] ? . sourceName ? ? "the authored audio asset" } . ` ) ;
return true ;
}
case "triggerVolume" :
store . executeCommand ( createUpsertEntityCommand ( {
entity : createTriggerVolumeEntity ( {
position
} ) ,
label : "Place trigger volume"
} ) ) ;
completeCreation ( "Placed Trigger Volume." ) ;
return true ;
case "teleportTarget" :
store . executeCommand ( createUpsertEntityCommand ( {
entity : createTeleportTargetEntity ( {
position
} ) ,
label : "Place teleport target"
} ) ) ;
completeCreation ( "Placed Teleport Target." ) ;
return true ;
case "interactable" :
store . executeCommand ( createUpsertEntityCommand ( {
entity : createInteractableEntity ( {
position
} ) ,
label : "Place interactable"
} ) ) ;
completeCreation ( "Placed Interactable." ) ;
return true ;
}
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
return false ;
} ;
const commitModelInstanceChange = ( currentModelInstance , nextModelInstance , successMessage ) => {
if ( areModelInstancesEqual ( currentModelInstance , nextModelInstance ) ) {
return ;
}
store . executeCommand ( createUpsertModelInstanceCommand ( {
modelInstance : nextModelInstance ,
label : ` Update ${ getModelInstanceDisplayLabelById ( currentModelInstance . id , editorState . document . modelInstances , editorState . document . assets ) . toLowerCase ( ) } `
} ) ) ;
setStatusMessage ( successMessage ) ;
} ;
const applyModelInstanceChange = ( ) => {
if ( selectedModelInstance === null ) {
setStatusMessage ( "Select a model instance before editing it." ) ;
return ;
}
try {
const nextModelInstance = createModelInstance ( {
id : selectedModelInstance . id ,
assetId : selectedModelInstance . assetId ,
name : selectedModelInstance . name ,
collision : selectedModelInstance . collision ,
position : readVec3Draft ( modelPositionDraft , "Model instance position" ) ,
rotationDegrees : readVec3Draft ( modelRotationDraft , "Model instance rotation" ) ,
scale : readPositiveVec3Draft ( modelScaleDraft , "Model instance scale" ) ,
animationClipName : selectedModelInstance . animationClipName ,
animationAutoplay : selectedModelInstance . animationAutoplay
} ) ;
commitModelInstanceChange ( selectedModelInstance , nextModelInstance , "Updated model instance." ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyPlayerStartChange = ( overrides = { } ) => {
if ( selectedPlayerStart === null ) {
setStatusMessage ( "Select a Player Start before editing it." ) ;
return ;
}
try {
const snappedPosition = snapVec3ToGrid ( readVec3Draft ( entityPositionDraft , "Player Start position" ) , DEFAULT _GRID _SIZE ) ;
const yawDegrees = readYawDegreesDraft ( playerStartYawDraft ) ;
const colliderMode = overrides . colliderMode ? ? playerStartColliderModeDraft ;
const nextEntity = createPlayerStartEntity ( {
id : selectedPlayerStart . id ,
name : selectedPlayerStart . name ,
position : snappedPosition ,
yawDegrees ,
collider : {
mode : colliderMode ,
eyeHeight : readPositiveNumberDraft ( playerStartEyeHeightDraft , "Player Start eye height" ) ,
capsuleRadius : readPositiveNumberDraft ( playerStartCapsuleRadiusDraft , "Player Start capsule radius" ) ,
capsuleHeight : readPositiveNumberDraft ( playerStartCapsuleHeightDraft , "Player Start capsule height" ) ,
boxSize : readPositiveVec3Draft ( playerStartBoxSizeDraft , "Player Start box size" )
}
} ) ;
commitEntityChange ( selectedPlayerStart , nextEntity , "Updated Player Start." ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyPointLightChange = ( overrides = { } ) => {
if ( selectedPointLight === null ) {
setStatusMessage ( "Select a Point Light before editing it." ) ;
return ;
}
try {
const nextEntity = createPointLightEntity ( {
id : selectedPointLight . id ,
name : selectedPointLight . name ,
position : snapVec3ToGrid ( readVec3Draft ( entityPositionDraft , "Point Light position" ) , DEFAULT _GRID _SIZE ) ,
colorHex : overrides . colorHex ? ? pointLightColorDraft ,
intensity : readNonNegativeNumberDraft ( pointLightIntensityDraft , "Point Light intensity" ) ,
distance : readPositiveNumberDraft ( pointLightDistanceDraft , "Point Light distance" )
} ) ;
commitEntityChange ( selectedPointLight , nextEntity , "Updated Point Light." ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applySpotLightChange = ( overrides = { } ) => {
if ( selectedSpotLight === null ) {
setStatusMessage ( "Select a Spot Light before editing it." ) ;
return ;
}
try {
const nextEntity = createSpotLightEntity ( {
id : selectedSpotLight . id ,
name : selectedSpotLight . name ,
position : snapVec3ToGrid ( readVec3Draft ( entityPositionDraft , "Spot Light position" ) , DEFAULT _GRID _SIZE ) ,
direction : readVec3Draft ( spotLightDirectionDraft , "Spot Light direction" ) ,
colorHex : overrides . colorHex ? ? spotLightColorDraft ,
intensity : readNonNegativeNumberDraft ( spotLightIntensityDraft , "Spot Light intensity" ) ,
distance : readPositiveNumberDraft ( spotLightDistanceDraft , "Spot Light distance" ) ,
angleDegrees : readPositiveNumberDraft ( spotLightAngleDraft , "Spot Light angle" )
} ) ;
commitEntityChange ( selectedSpotLight , nextEntity , "Updated Spot Light." ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applySelectedEntityDraftChange = ( ) => {
if ( selectedEntity === null ) {
return ;
}
switch ( selectedEntity . kind ) {
case "pointLight" :
applyPointLightChange ( ) ;
break ;
case "spotLight" :
applySpotLightChange ( ) ;
break ;
case "playerStart" :
applyPlayerStartChange ( ) ;
break ;
case "soundEmitter" :
applySoundEmitterChange ( ) ;
break ;
case "triggerVolume" :
applyTriggerVolumeChange ( ) ;
break ;
case "teleportTarget" :
applyTeleportTargetChange ( ) ;
break ;
case "interactable" :
applyInteractableChange ( ) ;
break ;
}
} ;
const applySoundEmitterChange = ( overrides = { } ) => {
if ( selectedSoundEmitter === null ) {
setStatusMessage ( "Select a Sound Emitter before editing it." ) ;
return ;
}
try {
const trimmedAudioAssetId = soundEmitterAudioAssetIdDraft . trim ( ) ;
const nextAudioAssetId = overrides . audioAssetId !== undefined
? overrides . audioAssetId
: trimmedAudioAssetId . length === 0
? null
: trimmedAudioAssetId ;
const nextEntity = createSoundEmitterEntity ( {
id : selectedSoundEmitter . id ,
name : selectedSoundEmitter . name ,
position : snapVec3ToGrid ( readVec3Draft ( entityPositionDraft , "Sound Emitter position" ) , DEFAULT _GRID _SIZE ) ,
audioAssetId : nextAudioAssetId ,
volume : readNonNegativeNumberDraft ( soundEmitterVolumeDraft , "Sound Emitter volume" ) ,
refDistance : readPositiveNumberDraft ( soundEmitterRefDistanceDraft , "Sound Emitter ref distance" ) ,
maxDistance : readPositiveNumberDraft ( soundEmitterMaxDistanceDraft , "Sound Emitter max distance" ) ,
autoplay : overrides . autoplay ? ? soundEmitterAutoplayDraft ,
loop : overrides . loop ? ? soundEmitterLoopDraft
} ) ;
commitEntityChange ( selectedSoundEmitter , nextEntity , "Updated Sound Emitter." ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyTriggerVolumeChange = ( ) => {
if ( selectedTriggerVolume === null ) {
setStatusMessage ( "Select a Trigger Volume before editing it." ) ;
return ;
}
try {
// Derive triggerOnEnter/triggerOnExit from the actual links so the flags
// stay in sync automatically — no manual checkbox needed.
const links = getInteractionLinksForSource ( editorState . document . interactionLinks , selectedTriggerVolume . id ) ;
const triggerOnEnter = links . some ( ( l ) => l . trigger === "enter" ) ;
const triggerOnExit = links . some ( ( l ) => l . trigger === "exit" ) ;
const nextEntity = createTriggerVolumeEntity ( {
id : selectedTriggerVolume . id ,
name : selectedTriggerVolume . name ,
position : snapVec3ToGrid ( readVec3Draft ( entityPositionDraft , "Trigger Volume position" ) , DEFAULT _GRID _SIZE ) ,
size : snapPositiveSizeToGrid ( readVec3Draft ( triggerVolumeSizeDraft , "Trigger Volume size" ) , DEFAULT _GRID _SIZE ) ,
triggerOnEnter ,
triggerOnExit
} ) ;
commitEntityChange ( selectedTriggerVolume , nextEntity , "Updated Trigger Volume." ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyTeleportTargetChange = ( ) => {
if ( selectedTeleportTarget === null ) {
setStatusMessage ( "Select a Teleport Target before editing it." ) ;
return ;
}
try {
const nextEntity = createTeleportTargetEntity ( {
id : selectedTeleportTarget . id ,
name : selectedTeleportTarget . name ,
position : snapVec3ToGrid ( readVec3Draft ( entityPositionDraft , "Teleport Target position" ) , DEFAULT _GRID _SIZE ) ,
yawDegrees : readYawDegreesDraft ( teleportTargetYawDraft )
} ) ;
commitEntityChange ( selectedTeleportTarget , nextEntity , "Updated Teleport Target." ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyInteractableChange = ( overrides = { } ) => {
if ( selectedInteractable === null ) {
setStatusMessage ( "Select an Interactable before editing it." ) ;
return ;
}
try {
const nextEntity = createInteractableEntity ( {
id : selectedInteractable . id ,
name : selectedInteractable . name ,
position : snapVec3ToGrid ( readVec3Draft ( entityPositionDraft , "Interactable position" ) , DEFAULT _GRID _SIZE ) ,
radius : readPositiveNumberDraft ( interactableRadiusDraft , "Interactable radius" ) ,
prompt : readInteractablePromptDraft ( interactablePromptDraft ) ,
enabled : overrides . enabled ? ? interactableEnabledDraft
} ) ;
commitEntityChange ( selectedInteractable , nextEntity , "Updated Interactable." ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const commitInteractionLinkChange = ( currentLink , nextLink , successMessage , label = "Update interaction link" ) => {
if ( areInteractionLinksEqual ( currentLink , nextLink ) ) {
return ;
}
store . executeCommand ( createUpsertInteractionLinkCommand ( {
link : nextLink ,
label
} ) ) ;
setStatusMessage ( successMessage ) ;
} ;
const getInteractionSourceEntityForLink = ( link ) => {
const sourceEntity = editorState . document . entities [ link . sourceEntityId ] ;
return sourceEntity ? . kind === "triggerVolume" || sourceEntity ? . kind === "interactable" ? sourceEntity : null ;
} ;
const handleAddTeleportInteractionLink = ( ) => {
if ( selectedInteractionSource === null ) {
setStatusMessage ( "Select a Trigger Volume or Interactable before adding links." ) ;
return ;
}
const defaultTarget = teleportTargetOptions [ 0 ] ? . entity ;
if ( defaultTarget === undefined || defaultTarget . kind !== "teleportTarget" ) {
setStatusMessage ( "Author a Teleport Target before adding a teleport link." ) ;
return ;
}
store . executeCommand ( createUpsertInteractionLinkCommand ( {
link : createTeleportPlayerInteractionLink ( {
sourceEntityId : selectedInteractionSource . id ,
trigger : getDefaultInteractionLinkTrigger ( selectedInteractionSource ) ,
targetEntityId : defaultTarget . id
} ) ,
label : "Add teleport interaction link"
} ) ) ;
setStatusMessage ( ` Added a teleport link to the selected ${ selectedInteractionSource . kind === "triggerVolume" ? "Trigger Volume" : "Interactable" } . ` ) ;
} ;
const handleAddVisibilityInteractionLink = ( ) => {
if ( selectedInteractionSource === null ) {
setStatusMessage ( "Select a Trigger Volume or Interactable before adding links." ) ;
return ;
}
const defaultTarget = visibilityBrushOptions [ 0 ] ? . brush ;
if ( defaultTarget === undefined ) {
setStatusMessage ( "Author at least one whitebox solid before adding a visibility link." ) ;
return ;
}
store . executeCommand ( createUpsertInteractionLinkCommand ( {
link : createToggleVisibilityInteractionLink ( {
sourceEntityId : selectedInteractionSource . id ,
trigger : getDefaultInteractionLinkTrigger ( selectedInteractionSource ) ,
targetBrushId : defaultTarget . id
} ) ,
label : "Add visibility interaction link"
} ) ) ;
setStatusMessage ( ` Added a visibility link to the selected ${ selectedInteractionSource . kind === "triggerVolume" ? "Trigger Volume" : "Interactable" } . ` ) ;
} ;
const handleAddSoundInteractionLink = ( actionType ) => {
if ( selectedInteractionSource === null ) {
setStatusMessage ( "Select a Trigger Volume or Interactable before adding links." ) ;
return ;
}
const defaultTarget = playableSoundEmitterOptions [ 0 ] ? . entity ;
if ( defaultTarget === undefined ) {
setStatusMessage ( "Author a Sound Emitter with an audio asset before adding sound links." ) ;
return ;
}
const link = actionType === "playSound"
? createPlaySoundInteractionLink ( {
sourceEntityId : selectedInteractionSource . id ,
trigger : getDefaultInteractionLinkTrigger ( selectedInteractionSource ) ,
targetSoundEmitterId : defaultTarget . id
} )
: createStopSoundInteractionLink ( {
sourceEntityId : selectedInteractionSource . id ,
trigger : getDefaultInteractionLinkTrigger ( selectedInteractionSource ) ,
targetSoundEmitterId : defaultTarget . id
} ) ;
store . executeCommand ( createUpsertInteractionLinkCommand ( {
link ,
label : actionType === "playSound" ? "Add play sound link" : "Add stop sound link"
} ) ) ;
setStatusMessage ( ` Added a ${ actionType === "playSound" ? "play sound" : "stop sound" } link to the selected ${ selectedInteractionSource . kind === "triggerVolume" ? "Trigger Volume" : "Interactable" } . ` ) ;
} ;
const handleDeleteInteractionLink = ( linkId ) => {
try {
store . executeCommand ( createDeleteInteractionLinkCommand ( linkId ) ) ;
setStatusMessage ( "Deleted interaction link." ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const confirmDeleteSceneItem = ( label ) => globalThis . window . confirm ( ` Delete ${ label } ? \n \n This can be undone with Undo. ` ) ;
const handleDeleteBrush = ( brushId ) => {
const label = getBrushLabelById ( brushId , brushList ) ;
if ( ! confirmDeleteSceneItem ( label ) ) {
return false ;
}
try {
store . executeCommand ( createDeleteBoxBrushCommand ( brushId ) ) ;
setStatusMessage ( ` Deleted ${ label } . ` ) ;
return true ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
return false ;
}
} ;
const handleDeleteEntity = ( entityId ) => {
const label = getEntityDisplayLabelById ( entityId , editorState . document . entities , editorState . document . assets ) ;
if ( ! confirmDeleteSceneItem ( label ) ) {
return false ;
}
try {
store . executeCommand ( createDeleteEntityCommand ( entityId ) ) ;
setStatusMessage ( ` Deleted ${ label } . ` ) ;
return true ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
return false ;
}
} ;
const handleDeleteModelInstance = ( modelInstanceId ) => {
const label = getModelInstanceDisplayLabelById ( modelInstanceId , editorState . document . modelInstances , editorState . document . assets ) ;
if ( ! confirmDeleteSceneItem ( label ) ) {
return false ;
}
try {
store . executeCommand ( createDeleteModelInstanceCommand ( modelInstanceId ) ) ;
setStatusMessage ( ` Deleted ${ label } . ` ) ;
return true ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
return false ;
}
} ;
const handleDeleteSelectedSceneItem = ( ) => {
const selectedBrushId = getSingleSelectedBrushId ( editorState . selection ) ;
if ( selectedBrushId !== null ) {
return handleDeleteBrush ( selectedBrushId ) ;
}
const selectedEntityId = getSingleSelectedEntityId ( editorState . selection ) ;
if ( selectedEntityId !== null ) {
return handleDeleteEntity ( selectedEntityId ) ;
}
const selectedModelInstanceId = getSingleSelectedModelInstanceId ( editorState . selection ) ;
if ( selectedModelInstanceId !== null ) {
return handleDeleteModelInstance ( selectedModelInstanceId ) ;
}
return false ;
} ;
2026-04-05 04:23:58 +02:00
const handleDuplicateSelection = ( ) => {
if ( ! selectionCanBeDuplicated ( editorState . selection ) ) {
return false ;
}
try {
store . executeCommand ( createDuplicateSelectionCommand ( ) ) ;
setStatusMessage ( "Duplicated selection." ) ;
return true ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
return false ;
}
} ;
2026-04-05 02:25:08 +02:00
const updateInteractionLinkTrigger = ( link , trigger ) => {
const sourceEntity = getInteractionSourceEntityForLink ( link ) ;
if ( sourceEntity ? . kind === "interactable" && trigger !== "click" ) {
setStatusMessage ( "Interactable links always use the click trigger." ) ;
return ;
}
if ( sourceEntity ? . kind === "triggerVolume" && trigger === "click" ) {
setStatusMessage ( "Trigger Volume links may only use enter or exit triggers." ) ;
return ;
}
let nextLink ;
switch ( link . action . type ) {
case "teleportPlayer" :
nextLink = createTeleportPlayerInteractionLink ( {
id : link . id ,
sourceEntityId : link . sourceEntityId ,
trigger ,
targetEntityId : link . action . targetEntityId
} ) ;
break ;
case "toggleVisibility" :
nextLink = createToggleVisibilityInteractionLink ( {
id : link . id ,
sourceEntityId : link . sourceEntityId ,
trigger ,
targetBrushId : link . action . targetBrushId ,
visible : link . action . visible
} ) ;
break ;
case "playAnimation" :
nextLink = createPlayAnimationInteractionLink ( {
id : link . id ,
sourceEntityId : link . sourceEntityId ,
trigger ,
targetModelInstanceId : link . action . targetModelInstanceId ,
clipName : link . action . clipName ,
loop : link . action . loop
} ) ;
break ;
case "stopAnimation" :
nextLink = createStopAnimationInteractionLink ( {
id : link . id ,
sourceEntityId : link . sourceEntityId ,
trigger ,
targetModelInstanceId : link . action . targetModelInstanceId
} ) ;
break ;
case "playSound" :
nextLink = createPlaySoundInteractionLink ( {
id : link . id ,
sourceEntityId : link . sourceEntityId ,
trigger ,
targetSoundEmitterId : link . action . targetSoundEmitterId
} ) ;
break ;
case "stopSound" :
nextLink = createStopSoundInteractionLink ( {
id : link . id ,
sourceEntityId : link . sourceEntityId ,
trigger ,
targetSoundEmitterId : link . action . targetSoundEmitterId
} ) ;
break ;
}
commitInteractionLinkChange ( link , nextLink , ` Updated ${ getInteractionTriggerLabel ( trigger ) . toLowerCase ( ) } trigger link. ` ) ;
} ;
const updateInteractionLinkActionType = ( link , actionType ) => {
const sourceEntity = getInteractionSourceEntityForLink ( link ) ;
if ( sourceEntity === null || link . action . type === actionType ) {
return ;
}
if ( actionType === "teleportPlayer" ) {
const defaultTarget = teleportTargetOptions [ 0 ] ? . entity ;
if ( defaultTarget === undefined || defaultTarget . kind !== "teleportTarget" ) {
setStatusMessage ( "Author a Teleport Target before switching this link to teleport." ) ;
return ;
}
commitInteractionLinkChange ( link , createTeleportPlayerInteractionLink ( {
id : link . id ,
sourceEntityId : sourceEntity . id ,
trigger : link . trigger ,
targetEntityId : defaultTarget . id
} ) , "Switched link action to teleport player." ) ;
return ;
}
if ( actionType === "playAnimation" ) {
const targetModelInstance = ( link . action . type === "playAnimation" || link . action . type === "stopAnimation"
? editorState . document . modelInstances [ link . action . targetModelInstanceId ]
: undefined ) ? ? modelInstanceDisplayList [ 0 ] ? . modelInstance ;
if ( targetModelInstance === undefined ) {
setStatusMessage ( "Place a model instance before switching this link to play animation." ) ;
return ;
}
const asset = editorState . document . assets [ targetModelInstance . assetId ] ;
const firstClip = asset ? . kind === "model" ? ( asset . metadata . animationNames [ 0 ] ? ? "" ) : "" ;
if ( firstClip === "" ) {
setStatusMessage ( "The model instance has no animation clips." ) ;
return ;
}
commitInteractionLinkChange ( link , createPlayAnimationInteractionLink ( {
id : link . id ,
sourceEntityId : sourceEntity . id ,
trigger : link . trigger ,
targetModelInstanceId : targetModelInstance . id ,
clipName : firstClip
} ) , "Switched link action to play animation." ) ;
return ;
}
if ( actionType === "stopAnimation" ) {
const targetModelInstance = ( link . action . type === "playAnimation" || link . action . type === "stopAnimation"
? editorState . document . modelInstances [ link . action . targetModelInstanceId ]
: undefined ) ? ? modelInstanceDisplayList [ 0 ] ? . modelInstance ;
if ( targetModelInstance === undefined ) {
setStatusMessage ( "Place a model instance before switching this link to stop animation." ) ;
return ;
}
commitInteractionLinkChange ( link , createStopAnimationInteractionLink ( {
id : link . id ,
sourceEntityId : sourceEntity . id ,
trigger : link . trigger ,
targetModelInstanceId : targetModelInstance . id
} ) , "Switched link action to stop animation." ) ;
return ;
}
if ( actionType === "playSound" || actionType === "stopSound" ) {
const targetSoundEmitter = ( link . action . type === "playSound" || link . action . type === "stopSound"
? editorState . document . entities [ link . action . targetSoundEmitterId ]
: undefined ) ? ? playableSoundEmitterOptions [ 0 ] ? . entity ;
if ( targetSoundEmitter === undefined || targetSoundEmitter . kind !== "soundEmitter" ) {
setStatusMessage ( "Author a Sound Emitter with an audio asset before switching this link to sound playback." ) ;
return ;
}
if ( actionType === "playSound" ) {
commitInteractionLinkChange ( link , createPlaySoundInteractionLink ( {
id : link . id ,
sourceEntityId : sourceEntity . id ,
trigger : link . trigger ,
targetSoundEmitterId : targetSoundEmitter . id
} ) , "Switched link action to play sound." ) ;
}
else {
commitInteractionLinkChange ( link , createStopSoundInteractionLink ( {
id : link . id ,
sourceEntityId : sourceEntity . id ,
trigger : link . trigger ,
targetSoundEmitterId : targetSoundEmitter . id
} ) , "Switched link action to stop sound." ) ;
}
return ;
}
const defaultBrush = visibilityBrushOptions [ 0 ] ? . brush ;
if ( defaultBrush === undefined ) {
setStatusMessage ( "Author at least one whitebox solid before switching this link to visibility." ) ;
return ;
}
commitInteractionLinkChange ( link , createToggleVisibilityInteractionLink ( {
id : link . id ,
sourceEntityId : sourceEntity . id ,
trigger : link . trigger ,
targetBrushId : defaultBrush . id
} ) , "Switched link action to toggle visibility." ) ;
} ;
const updateTeleportInteractionLinkTarget = ( link , targetEntityId ) => {
if ( link . action . type !== "teleportPlayer" ) {
return ;
}
commitInteractionLinkChange ( link , createTeleportPlayerInteractionLink ( {
id : link . id ,
sourceEntityId : link . sourceEntityId ,
trigger : link . trigger ,
targetEntityId
} ) , "Updated teleport link target." ) ;
} ;
const updateVisibilityInteractionLinkTarget = ( link , targetBrushId ) => {
if ( link . action . type !== "toggleVisibility" ) {
return ;
}
commitInteractionLinkChange ( link , createToggleVisibilityInteractionLink ( {
id : link . id ,
sourceEntityId : link . sourceEntityId ,
trigger : link . trigger ,
targetBrushId ,
visible : link . action . visible
} ) , "Updated visibility link target." ) ;
} ;
const updateVisibilityInteractionMode = ( link , mode ) => {
if ( link . action . type !== "toggleVisibility" ) {
return ;
}
commitInteractionLinkChange ( link , createToggleVisibilityInteractionLink ( {
id : link . id ,
sourceEntityId : link . sourceEntityId ,
trigger : link . trigger ,
targetBrushId : link . action . targetBrushId ,
visible : readVisibilityModeSelectValue ( mode )
} ) , "Updated visibility link mode." ) ;
} ;
const updateSoundInteractionLinkTarget = ( link , targetSoundEmitterId ) => {
if ( link . action . type !== "playSound" && link . action . type !== "stopSound" ) {
return ;
}
if ( link . action . type === "playSound" ) {
commitInteractionLinkChange ( link , createPlaySoundInteractionLink ( {
id : link . id ,
sourceEntityId : link . sourceEntityId ,
trigger : link . trigger ,
targetSoundEmitterId
} ) , "Updated play sound link target." ) ;
}
else {
commitInteractionLinkChange ( link , createStopSoundInteractionLink ( {
id : link . id ,
sourceEntityId : link . sourceEntityId ,
trigger : link . trigger ,
targetSoundEmitterId
} ) , "Updated stop sound link target." ) ;
}
} ;
const updateAnimationInteractionLinkTarget = ( link , targetModelInstanceId ) => {
if ( link . action . type !== "playAnimation" && link . action . type !== "stopAnimation" ) {
return ;
}
if ( link . action . type === "playAnimation" ) {
commitInteractionLinkChange ( link , createPlayAnimationInteractionLink ( {
id : link . id ,
sourceEntityId : link . sourceEntityId ,
trigger : link . trigger ,
targetModelInstanceId ,
clipName : link . action . clipName ,
loop : link . action . loop
} ) , "Updated play animation link target." ) ;
}
else {
commitInteractionLinkChange ( link , createStopAnimationInteractionLink ( {
id : link . id ,
sourceEntityId : link . sourceEntityId ,
trigger : link . trigger ,
targetModelInstanceId
} ) , "Updated stop animation link target." ) ;
}
} ;
const updatePlayAnimationLinkClip = ( link , clipName ) => {
if ( link . action . type !== "playAnimation" ) {
return ;
}
commitInteractionLinkChange ( link , createPlayAnimationInteractionLink ( {
id : link . id ,
sourceEntityId : link . sourceEntityId ,
trigger : link . trigger ,
targetModelInstanceId : link . action . targetModelInstanceId ,
clipName ,
loop : link . action . loop
} ) , "Updated play animation clip." ) ;
} ;
const updatePlayAnimationLinkLoop = ( link , loop ) => {
if ( link . action . type !== "playAnimation" ) {
return ;
}
commitInteractionLinkChange ( link , createPlayAnimationInteractionLink ( {
id : link . id ,
sourceEntityId : link . sourceEntityId ,
trigger : link . trigger ,
targetModelInstanceId : link . action . targetModelInstanceId ,
clipName : link . action . clipName ,
loop
} ) , "Updated play animation loop setting." ) ;
} ;
const handleAddPlayAnimationLink = ( sourceEntity ) => {
const firstInstance = modelInstanceDisplayList [ 0 ] ;
if ( firstInstance === undefined ) {
setStatusMessage ( "Place a model instance before adding an animation link." ) ;
return ;
}
const asset = editorState . document . assets [ firstInstance . modelInstance . assetId ] ;
const firstClip = asset ? . kind === "model" ? ( asset . metadata . animationNames [ 0 ] ? ? "" ) : "" ;
if ( firstClip === "" ) {
setStatusMessage ( "The model instance has no animation clips." ) ;
return ;
}
store . executeCommand ( createUpsertInteractionLinkCommand ( {
link : createPlayAnimationInteractionLink ( {
sourceEntityId : sourceEntity . id ,
trigger : getDefaultInteractionLinkTrigger ( sourceEntity ) ,
targetModelInstanceId : firstInstance . modelInstance . id ,
clipName : firstClip
} ) ,
label : "Add play animation link"
} ) ) ;
setStatusMessage ( "Added a play animation link." ) ;
} ;
const handleAddStopAnimationLink = ( sourceEntity ) => {
const firstInstance = modelInstanceDisplayList [ 0 ] ;
if ( firstInstance === undefined ) {
setStatusMessage ( "Place a model instance before adding an animation link." ) ;
return ;
}
store . executeCommand ( createUpsertInteractionLinkCommand ( {
link : createStopAnimationInteractionLink ( {
sourceEntityId : sourceEntity . id ,
trigger : getDefaultInteractionLinkTrigger ( sourceEntity ) ,
targetModelInstanceId : firstInstance . modelInstance . id
} ) ,
label : "Add stop animation link"
} ) ) ;
setStatusMessage ( "Added a stop animation link." ) ;
} ;
const renderInteractionLinksSection = ( sourceEntity , links , addTeleportTestId , addVisibilityTestId , addPlaySoundTestId , addStopSoundTestId ) => ( _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Links" } ) , links . length === 0 ? ( _jsx ( "div" , { className : "outliner-empty" , children : sourceEntity . kind === "triggerVolume" ? "No trigger links authored yet." : "No click links authored yet." } ) ) : ( _jsx ( "div" , { className : "outliner-list" , children : links . map ( ( link , index ) => ( _jsxs ( "div" , { className : "outliner-item" , children : [ _jsxs ( "div" , { className : "outliner-item__select" , children : [ _jsx ( "span" , { className : "outliner-item__title" , children : ` Link ${ index + 1 } ` } ) , _jsx ( "span" , { className : "outliner-item__meta" , children : getInteractionActionLabel ( link ) } ) ] } ) , _jsx ( "div" , { className : "form-section" , children : _jsxs ( "div" , { className : "vector-inputs vector-inputs--two" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Trigger" } ) , sourceEntity . kind === "triggerVolume" ? ( _jsxs ( "select" , { "data-testid" : ` interaction-link-trigger- ${ link . id } ` , className : "text-input" , value : link . trigger , onChange : ( event ) => updateInteractionLinkTrigger ( link , event . currentTarget . value ) , children : [ _jsx ( "option" , { value : "enter" , children : "On Enter" } ) , _jsx ( "option" , { value : "exit" , children : "On Exit" } ) ] } ) ) : ( _jsx ( "input" , { "data-testid" : ` interaction-link-trigger- ${ link . id } ` , className : "text-input" , type : "text" , value : getInteractionTriggerLabel ( link . trigger ) , readOnly : true } ) ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Action" } ) , _jsxs ( "select" , { "data-testid" : ` interaction-link-action- ${ link . id } ` , className : "text-input" , value : link . action . type , onChange : ( event ) => updateInteractionLinkActionType ( link , event . currentTarget . value ) , children : [ _jsx ( "option" , { value : "teleportPlayer" , children : "Teleport Player" } ) , _jsx ( "option" , { value : "toggleVisibility" , children : "Toggle Visibility" } ) , _jsx ( "option" , { value : "playAnimation" , children : "Play Animation" } ) , _jsx ( "option" , { value : "stopAnimation" , children : "Stop Animation" } ) , _jsx ( "option" , { value : "playSound" , children : "Play Sound" } ) , _jsx ( "option" , { value : "stopSound" , children : "Stop Sound" } ) ] } ) ] } ) ] } ) } ) , link . action . type === "teleportPlayer" ? ( _jsx ( "div" , { className : "form-section" , children : _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Target" } ) , _jsx ( "select" , { "data-testid" : ` interaction-link-teleport-target- ${ link . id } ` , className : "text-input" , value : link . action . targetEntityId , onChange : ( event ) => updateTeleportInteractionLinkTarget ( link , event . currentTarget . value ) , children : teleportTargetOptions . map ( ( { entity , label } ) => ( _jsx ( "option" , { value : entity . id , children : label } , entity . id ) ) ) } ) ] } ) } ) ) : link . action . type === "toggleVisibility" ? ( _jsx ( "div" , { className : "form-section" , children : _jsxs ( "div" , { className : "vector-inputs vector-inputs--two" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Solid" } ) , _jsx ( "select" , { "data-testid" : ` interaction-link-visibility-target- ${ link . id } ` , className : "text-input" , value : link . action . targetBrushId , onChange : ( event ) => updateVisibilityInteractionLinkTarget ( link , event . currentTarget . value ) , children : visibilityBrushOptions . map ( ( { brush , label } ) => ( _jsx ( "option" , { value : brush . id , children : label } , brush . id ) ) ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Mode" } ) , _jsxs ( "select" , { "data-testid" : ` interaction-link-visibility-mode- ${ link . id } ` , className : "text-input" , value : getVisibilityModeSelectValue ( link . action . visible ) , onChange : ( event ) => updateVisibilityInteractionMode ( link , event . currentTarget . value ) , children : [ _jsx ( "option" , { value : "toggle" , children : "Toggle" } ) , _jsx ( "option" , { value : "show" , children : "Show" } ) , _jsx ( "option"
const applyWorldSettings = ( nextWorld , label , successMessage ) => {
if ( areWorldSettingsEqual ( editorState . document . world , nextWorld ) ) {
return ;
}
try {
store . executeCommand ( createSetWorldSettingsCommand ( {
label ,
world : nextWorld
} ) ) ;
setStatusMessage ( successMessage ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyAdvancedRenderingSettings = ( label , successMessage , mutate ) => {
const nextWorld = cloneWorldSettings ( editorState . document . world ) ;
mutate ( nextWorld . advancedRendering ) ;
applyWorldSettings ( nextWorld , label , successMessage ) ;
} ;
const applyWorldBackgroundMode = ( mode , imageAssetId ) => {
if ( mode === "image" ) {
const currentBackgroundAssetId = editorState . document . world . background . mode === "image" ? editorState . document . world . background . assetId : null ;
const nextImageAssetId = imageAssetId ? ?
( currentBackgroundAssetId !== null && editorState . document . assets [ currentBackgroundAssetId ] ? . kind === "image"
? currentBackgroundAssetId
: imageAssetList [ 0 ] ? . id ) ;
if ( nextImageAssetId === undefined ) {
setStatusMessage ( "Import an image asset before using an image background." ) ;
return ;
}
applyWorldSettings ( {
... editorState . document . world ,
background : changeWorldBackgroundMode ( editorState . document . world . background , "image" , nextImageAssetId )
} , "Set world background image" , ` World background set to ${ editorState . document . assets [ nextImageAssetId ] ? . sourceName ? ? nextImageAssetId } . ` ) ;
return ;
}
applyWorldSettings ( {
... editorState . document . world ,
background : changeWorldBackgroundMode ( editorState . document . world . background , mode )
} , "Set world background mode" , mode === "solid" ? "World background set to a solid color." : "World background set to a vertical gradient." ) ;
} ;
const applyWorldBackgroundColor = ( colorHex ) => {
if ( editorState . document . world . background . mode !== "solid" ) {
return ;
}
applyWorldSettings ( {
... editorState . document . world ,
background : {
mode : "solid" ,
colorHex
}
} , "Set world background color" , "Updated the world background color." ) ;
} ;
const applyWorldGradientColor = ( edge , colorHex ) => {
if ( editorState . document . world . background . mode !== "verticalGradient" ) {
return ;
}
applyWorldSettings ( {
... editorState . document . world ,
background : edge === "top"
? {
... editorState . document . world . background ,
topColorHex : colorHex
}
: {
... editorState . document . world . background ,
bottomColorHex : colorHex
}
} , edge === "top" ? "Set world gradient top color" : "Set world gradient bottom color" , edge === "top" ? "Updated the world gradient top color." : "Updated the world gradient bottom color." ) ;
} ;
const applyBackgroundEnvironmentIntensity = ( ) => {
if ( editorState . document . world . background . mode !== "image" ) {
return ;
}
const intensity = readNonNegativeNumberDraft ( backgroundEnvironmentIntensityDraft , "Environment intensity" ) ;
applyWorldSettings ( {
... editorState . document . world ,
background : {
... editorState . document . world . background ,
environmentIntensity : intensity
}
} , "Set background environment intensity" , "Updated the background environment intensity." ) ;
} ;
const applyAmbientLightColor = ( colorHex ) => {
applyWorldSettings ( {
... editorState . document . world ,
ambientLight : {
... editorState . document . world . ambientLight ,
colorHex
}
} , "Set world ambient light color" , "Updated the world ambient light color." ) ;
} ;
const applyAmbientLightIntensity = ( ) => {
try {
applyWorldSettings ( {
... editorState . document . world ,
ambientLight : {
... editorState . document . world . ambientLight ,
intensity : readNonNegativeNumberDraft ( ambientLightIntensityDraft , "Ambient light intensity" )
}
} , "Set world ambient light intensity" , "Updated the world ambient light intensity." ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applySunLightColor = ( colorHex ) => {
applyWorldSettings ( {
... editorState . document . world ,
sunLight : {
... editorState . document . world . sunLight ,
colorHex
}
} , "Set world sun color" , "Updated the world sun color." ) ;
} ;
const applySunLightIntensity = ( ) => {
try {
applyWorldSettings ( {
... editorState . document . world ,
sunLight : {
... editorState . document . world . sunLight ,
intensity : readNonNegativeNumberDraft ( sunLightIntensityDraft , "Sun intensity" )
}
} , "Set world sun intensity" , "Updated the world sun intensity." ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applySunLightDirection = ( ) => {
try {
const direction = readVec3Draft ( sunDirectionDraft , "Sun direction" ) ;
if ( direction . x === 0 && direction . y === 0 && direction . z === 0 ) {
throw new Error ( "Sun direction must not be the zero vector." ) ;
}
applyWorldSettings ( {
... editorState . document . world ,
sunLight : {
... editorState . document . world . sunLight ,
direction
}
} , "Set world sun direction" , "Updated the world sun direction." ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyAdvancedRenderingEnabled = ( enabled ) => {
applyAdvancedRenderingSettings ( "Set advanced rendering" , enabled ? "Advanced rendering enabled." : "Advanced rendering disabled." , ( advancedRendering ) => {
advancedRendering . enabled = enabled ;
} ) ;
} ;
const applyAdvancedRenderingShadowsEnabled = ( enabled ) => {
applyAdvancedRenderingSettings ( "Set advanced rendering shadows" , enabled ? "Advanced rendering shadows enabled." : "Advanced rendering shadows disabled." , ( advancedRendering ) => {
advancedRendering . shadows . enabled = enabled ;
} ) ;
} ;
const applyAdvancedRenderingShadowMapSize = ( shadowMapSize ) => {
applyAdvancedRenderingSettings ( "Set advanced rendering shadow map size" , "Updated the shadow map size." , ( advancedRendering ) => {
advancedRendering . shadows . mapSize = shadowMapSize ;
} ) ;
} ;
const applyAdvancedRenderingShadowType = ( shadowType ) => {
applyAdvancedRenderingSettings ( "Set advanced rendering shadow type" , "Updated the shadow map type." , ( advancedRendering ) => {
advancedRendering . shadows . type = shadowType ;
} ) ;
} ;
const applyAdvancedRenderingShadowBias = ( ) => {
try {
applyAdvancedRenderingSettings ( "Set advanced rendering shadow bias" , "Updated the shadow bias." , ( advancedRendering ) => {
advancedRendering . shadows . bias = readFiniteNumberDraft ( advancedRenderingShadowBiasDraft , "Shadow bias" ) ;
} ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyAdvancedRenderingAmbientOcclusionEnabled = ( enabled ) => {
applyAdvancedRenderingSettings ( "Set ambient occlusion" , enabled ? "Ambient occlusion enabled." : "Ambient occlusion disabled." , ( advancedRendering ) => {
advancedRendering . ambientOcclusion . enabled = enabled ;
} ) ;
} ;
const applyAdvancedRenderingAmbientOcclusionIntensity = ( ) => {
try {
applyAdvancedRenderingSettings ( "Set ambient occlusion intensity" , "Updated the ambient occlusion intensity." , ( advancedRendering ) => {
advancedRendering . ambientOcclusion . intensity = readNonNegativeNumberDraft ( advancedRenderingAmbientOcclusionIntensityDraft , "Ambient occlusion intensity" ) ;
} ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyAdvancedRenderingAmbientOcclusionRadius = ( ) => {
try {
applyAdvancedRenderingSettings ( "Set ambient occlusion radius" , "Updated the ambient occlusion radius." , ( advancedRendering ) => {
advancedRendering . ambientOcclusion . radius = readNonNegativeNumberDraft ( advancedRenderingAmbientOcclusionRadiusDraft , "Ambient occlusion radius" ) ;
} ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyAdvancedRenderingAmbientOcclusionSamples = ( ) => {
try {
applyAdvancedRenderingSettings ( "Set ambient occlusion samples" , "Updated the ambient occlusion samples." , ( advancedRendering ) => {
advancedRendering . ambientOcclusion . samples = readPositiveIntegerDraft ( advancedRenderingAmbientOcclusionSamplesDraft , "Ambient occlusion samples" ) ;
} ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyAdvancedRenderingBloomEnabled = ( enabled ) => {
applyAdvancedRenderingSettings ( "Set bloom" , enabled ? "Bloom enabled." : "Bloom disabled." , ( advancedRendering ) => {
advancedRendering . bloom . enabled = enabled ;
} ) ;
} ;
const applyAdvancedRenderingBloomIntensity = ( ) => {
try {
applyAdvancedRenderingSettings ( "Set bloom intensity" , "Updated the bloom intensity." , ( advancedRendering ) => {
advancedRendering . bloom . intensity = readNonNegativeNumberDraft ( advancedRenderingBloomIntensityDraft , "Bloom intensity" ) ;
} ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyAdvancedRenderingBloomThreshold = ( ) => {
try {
applyAdvancedRenderingSettings ( "Set bloom threshold" , "Updated the bloom threshold." , ( advancedRendering ) => {
advancedRendering . bloom . threshold = readNonNegativeNumberDraft ( advancedRenderingBloomThresholdDraft , "Bloom threshold" ) ;
} ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyAdvancedRenderingBloomRadius = ( ) => {
try {
applyAdvancedRenderingSettings ( "Set bloom radius" , "Updated the bloom radius." , ( advancedRendering ) => {
advancedRendering . bloom . radius = readNonNegativeNumberDraft ( advancedRenderingBloomRadiusDraft , "Bloom radius" ) ;
} ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyAdvancedRenderingToneMappingMode = ( mode ) => {
applyAdvancedRenderingSettings ( "Set tone mapping mode" , "Updated the tone mapping mode." , ( advancedRendering ) => {
advancedRendering . toneMapping . mode = mode ;
} ) ;
} ;
const applyAdvancedRenderingToneMappingExposure = ( ) => {
try {
applyAdvancedRenderingSettings ( "Set tone mapping exposure" , "Updated the tone mapping exposure." , ( advancedRendering ) => {
advancedRendering . toneMapping . exposure = readPositiveNumberDraft ( advancedRenderingToneMappingExposureDraft , "Tone mapping exposure" ) ;
} ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyAdvancedRenderingDepthOfFieldEnabled = ( enabled ) => {
applyAdvancedRenderingSettings ( "Set depth of field" , enabled ? "Depth of field enabled." : "Depth of field disabled." , ( advancedRendering ) => {
advancedRendering . depthOfField . enabled = enabled ;
} ) ;
} ;
const applyAdvancedRenderingDepthOfFieldFocusDistance = ( ) => {
try {
applyAdvancedRenderingSettings ( "Set focus distance" , "Updated the focus distance." , ( advancedRendering ) => {
advancedRendering . depthOfField . focusDistance = readNonNegativeNumberDraft ( advancedRenderingDepthOfFieldFocusDistanceDraft , "Focus distance" ) ;
} ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyAdvancedRenderingDepthOfFieldFocalLength = ( ) => {
try {
applyAdvancedRenderingSettings ( "Set focal length" , "Updated the focal length." , ( advancedRendering ) => {
advancedRendering . depthOfField . focalLength = readPositiveNumberDraft ( advancedRenderingDepthOfFieldFocalLengthDraft , "Focal length" ) ;
} ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyAdvancedRenderingDepthOfFieldBokehScale = ( ) => {
try {
applyAdvancedRenderingSettings ( "Set bokeh scale" , "Updated the bokeh scale." , ( advancedRendering ) => {
advancedRendering . depthOfField . bokehScale = readPositiveNumberDraft ( advancedRenderingDepthOfFieldBokehScaleDraft , "Bokeh scale" ) ;
} ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyBrushNameChange = ( ) => {
if ( selectedBrush === null ) {
setStatusMessage ( "Select a whitebox box before renaming it." ) ;
return ;
}
const nextName = normalizeBrushName ( brushNameDraft ) ;
if ( selectedBrush . name === nextName ) {
return ;
}
try {
store . executeCommand ( createSetBoxBrushNameCommand ( {
brushId : selectedBrush . id ,
name : brushNameDraft
} ) ) ;
setStatusMessage ( nextName === undefined ? "Cleared the authored brush name." : ` Renamed brush to ${ nextName } . ` ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyEntityNameChange = ( ) => {
if ( selectedEntity === null ) {
setStatusMessage ( "Select an entity before renaming it." ) ;
return ;
}
const nextName = normalizeEntityName ( entityNameDraft ) ;
if ( selectedEntity . name === nextName ) {
return ;
}
try {
store . executeCommand ( createSetEntityNameCommand ( {
entityId : selectedEntity . id ,
name : entityNameDraft
} ) ) ;
setStatusMessage ( nextName === undefined ? "Cleared the authored entity name." : ` Renamed entity to ${ nextName } . ` ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const applyModelInstanceNameChange = ( ) => {
if ( selectedModelInstance === null ) {
setStatusMessage ( "Select a model instance before renaming it." ) ;
return ;
}
const nextName = normalizeModelInstanceName ( modelInstanceNameDraft ) ;
if ( selectedModelInstance . name === nextName ) {
return ;
}
try {
store . executeCommand ( createSetModelInstanceNameCommand ( {
modelInstanceId : selectedModelInstance . id ,
name : modelInstanceNameDraft
} ) ) ;
setStatusMessage ( nextName === undefined ? "Cleared the authored model instance name." : ` Renamed model instance to ${ nextName } . ` ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const handleInlineNameInputKeyDown = ( event , resetDraft ) => {
if ( event . key === "Enter" ) {
event . preventDefault ( ) ;
event . currentTarget . blur ( ) ;
return ;
}
if ( event . key === "Escape" ) {
event . preventDefault ( ) ;
resetDraft ( ) ;
event . currentTarget . blur ( ) ;
}
} ;
const handleDraftVectorKeyDown = ( event , applyChange ) => {
if ( event . key === "Enter" ) {
applyChange ( ) ;
}
} ;
const scheduleDraftCommit = ( applyChange ) => {
window . setTimeout ( ( ) => {
applyChange ( ) ;
} , 0 ) ;
} ;
const handleNumberInputPointerUp = ( _event , applyChange ) => {
scheduleDraftCommit ( applyChange ) ;
} ;
const handleNumberInputKeyUp = ( event , applyChange ) => {
if ( ! isCommitIncrementKey ( event . key ) ) {
return ;
}
scheduleDraftCommit ( applyChange ) ;
} ;
const handleSaveDraft = ( ) => {
const result = store . saveDraft ( ) ;
setStatusMessage ( result . message ) ;
} ;
const handleLoadDraft = ( ) => {
const result = store . loadDraft ( ) ;
setStatusMessage ( result . message ) ;
} ;
const handleExportJson = ( ) => {
try {
const exportedJson = store . exportDocumentJson ( ) ;
const blob = new Blob ( [ exportedJson ] , { type : "application/json" } ) ;
const objectUrl = URL . createObjectURL ( blob ) ;
const anchor = document . createElement ( "a" ) ;
anchor . href = objectUrl ;
anchor . download = ` ${ editorState . document . name . replace ( /\s+/g , "-" ) . toLowerCase ( ) || "scene" } .json ` ;
anchor . click ( ) ;
URL . revokeObjectURL ( objectUrl ) ;
setStatusMessage ( "Scene document exported as JSON." ) ;
}
catch ( error ) {
const message = getErrorMessage ( error ) ;
setStatusMessage ( message ) ;
}
} ;
const handleImportJsonButtonClick = ( ) => {
importInputRef . current ? . click ( ) ;
} ;
const handleImportJsonChange = async ( event ) => {
const input = event . currentTarget ;
const file = input . files ? . [ 0 ] ;
if ( file === undefined ) {
return ;
}
try {
const source = await file . text ( ) ;
store . importDocumentJson ( source ) ;
setStatusMessage ( ` Imported ${ file . name } . ` ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
finally {
input . value = "" ;
}
} ;
const handleImportModelButtonClick = ( ) => {
importModelInputRef . current ? . click ( ) ;
} ;
const handleImportBackgroundImageButtonClick = ( ) => {
importBackgroundImageInputRef . current ? . click ( ) ;
} ;
const handleImportAudioButtonClick = ( ) => {
importAudioInputRef . current ? . click ( ) ;
} ;
const handleImportModelChange = async ( event ) => {
const input = event . currentTarget ;
const files = Array . from ( input . files ? ? [ ] ) ;
if ( files . length === 0 ) {
return ;
}
if ( projectAssetStorage === null ) {
setAssetStatusMessage ( "Imported model assets require project asset storage. IndexedDB is unavailable in this browser." ) ;
input . value = "" ;
return ;
}
let importedModelForCleanup = null ;
try {
const importedModel = files . length === 1
? await importModelAssetFromFile ( files [ 0 ] , projectAssetStorage )
: await importModelAssetFromFiles ( files , projectAssetStorage ) ;
importedModelForCleanup = importedModel ;
store . executeCommand ( createImportModelAssetCommand ( {
asset : importedModel . asset ,
modelInstance : importedModel . modelInstance ,
label : ` Import ${ importedModel . asset . sourceName } `
} ) ) ;
loadedModelAssetsRef . current = {
... loadedModelAssetsRef . current ,
[ importedModel . asset . id ] : importedModel . loadedAsset
} ;
setLoadedModelAssets ( ( currentLoadedAssets ) => ( {
... currentLoadedAssets ,
[ importedModel . asset . id ] : importedModel . loadedAsset
} ) ) ;
setAssetStatusMessage ( null ) ;
setStatusMessage ( ` Imported ${ importedModel . asset . sourceName } and placed a model instance. ` ) ;
}
catch ( error ) {
if ( importedModelForCleanup !== null ) {
await projectAssetStorage . deleteAsset ( importedModelForCleanup . asset . storageKey ) . catch ( ( ) => undefined ) ;
disposeModelTemplate ( importedModelForCleanup . loadedAsset . template ) ;
}
const message = getErrorMessage ( error ) ;
setStatusMessage ( message ) ;
setAssetStatusMessage ( message ) ;
}
finally {
input . value = "" ;
}
} ;
const handleImportBackgroundImageChange = async ( event ) => {
const input = event . currentTarget ;
const file = input . files ? . [ 0 ] ;
if ( file === undefined ) {
return ;
}
if ( projectAssetStorage === null ) {
setAssetStatusMessage ( "Imported background images require project asset storage. IndexedDB is unavailable in this browser." ) ;
input . value = "" ;
return ;
}
let importedImageForCleanup = null ;
try {
const importedImage = await importBackgroundImageAssetFromFile ( file , projectAssetStorage ) ;
importedImageForCleanup = importedImage ;
store . executeCommand ( createImportBackgroundImageAssetCommand ( {
asset : importedImage . asset ,
world : {
... editorState . document . world ,
background : changeWorldBackgroundMode ( editorState . document . world . background , "image" , importedImage . asset . id )
} ,
label : ` Import ${ importedImage . asset . sourceName } as background `
} ) ) ;
loadedImageAssetsRef . current = {
... loadedImageAssetsRef . current ,
[ importedImage . asset . id ] : importedImage . loadedAsset
} ;
setLoadedImageAssets ( ( currentLoadedAssets ) => ( {
... currentLoadedAssets ,
[ importedImage . asset . id ] : importedImage . loadedAsset
} ) ) ;
setAssetStatusMessage ( null ) ;
setStatusMessage ( ` Imported ${ importedImage . asset . sourceName } and set it as the world background. ` ) ;
}
catch ( error ) {
if ( importedImageForCleanup !== null ) {
await projectAssetStorage . deleteAsset ( importedImageForCleanup . asset . storageKey ) . catch ( ( ) => undefined ) ;
disposeLoadedImageAsset ( importedImageForCleanup . loadedAsset ) ;
}
const message = getErrorMessage ( error ) ;
setStatusMessage ( message ) ;
setAssetStatusMessage ( message ) ;
}
finally {
input . value = "" ;
}
} ;
const handleImportAudioChange = async ( event ) => {
const input = event . currentTarget ;
const file = input . files ? . [ 0 ] ;
if ( file === undefined ) {
return ;
}
if ( projectAssetStorage === null ) {
setAssetStatusMessage ( "Imported audio assets require project asset storage. IndexedDB is unavailable in this browser." ) ;
input . value = "" ;
return ;
}
let importedAudioForCleanup = null ;
try {
const importedAudio = await importAudioAssetFromFile ( file , projectAssetStorage ) ;
importedAudioForCleanup = importedAudio ;
store . executeCommand ( createImportAudioAssetCommand ( {
asset : importedAudio . asset ,
label : ` Import ${ importedAudio . asset . sourceName } `
} ) ) ;
loadedAudioAssetsRef . current = {
... loadedAudioAssetsRef . current ,
[ importedAudio . asset . id ] : importedAudio . loadedAsset
} ;
setLoadedAudioAssets ( ( currentLoadedAssets ) => ( {
... currentLoadedAssets ,
[ importedAudio . asset . id ] : importedAudio . loadedAsset
} ) ) ;
setAssetStatusMessage ( null ) ;
setStatusMessage ( ` Imported ${ importedAudio . asset . sourceName } and registered it as an audio asset. ` ) ;
}
catch ( error ) {
if ( importedAudioForCleanup !== null ) {
await projectAssetStorage . deleteAsset ( importedAudioForCleanup . asset . storageKey ) . catch ( ( ) => undefined ) ;
}
const message = getErrorMessage ( error ) ;
setStatusMessage ( message ) ;
setAssetStatusMessage ( message ) ;
}
finally {
input . value = "" ;
}
} ;
const applyFaceMaterial = ( materialId ) => {
if ( selectedBrush === null || selectedFaceId === null || selectedFace === null ) {
setStatusMessage ( "Select a single box face before applying a material." ) ;
return ;
}
if ( selectedFace . materialId === materialId ) {
setStatusMessage ( ` ${ BOX _FACE _LABELS [ selectedFaceId ] } already uses that material. ` ) ;
return ;
}
try {
store . executeCommand ( createSetBoxBrushFaceMaterialCommand ( {
brushId : selectedBrush . id ,
faceId : selectedFaceId ,
materialId
} ) ) ;
setStatusMessage ( ` Applied ${ editorState . document . materials [ materialId ] ? . name ? ? materialId } to ${ BOX _FACE _LABELS [ selectedFaceId ] } . ` ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const clearFaceMaterial = ( ) => {
if ( selectedBrush === null || selectedFaceId === null || selectedFace === null ) {
setStatusMessage ( "Select a single box face before clearing its material." ) ;
return ;
}
if ( selectedFace . materialId === null ) {
setStatusMessage ( ` ${ BOX _FACE _LABELS [ selectedFaceId ] } already uses the fallback face material. ` ) ;
return ;
}
store . executeCommand ( createSetBoxBrushFaceMaterialCommand ( {
brushId : selectedBrush . id ,
faceId : selectedFaceId ,
materialId : null
} ) ) ;
setStatusMessage ( ` Cleared the authored material on ${ BOX _FACE _LABELS [ selectedFaceId ] } . ` ) ;
} ;
const applyFaceUvState = ( uvState , label , successMessage ) => {
if ( selectedBrush === null || selectedFaceId === null || selectedFace === null ) {
setStatusMessage ( "Select a single box face before editing UVs." ) ;
return ;
}
if ( areFaceUvStatesEqual ( selectedFace . uv , uvState ) ) {
setStatusMessage ( "That face UV state is already current." ) ;
return ;
}
try {
store . executeCommand ( createSetBoxBrushFaceUvStateCommand ( {
brushId : selectedBrush . id ,
faceId : selectedFaceId ,
uvState ,
label
} ) ) ;
setStatusMessage ( successMessage ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const handleApplyUvDraft = ( ) => {
if ( selectedFace === null ) {
setStatusMessage ( "Select a single box face before editing UVs." ) ;
return ;
}
try {
applyFaceUvState ( {
... selectedFace . uv ,
offset : readVec2Draft ( uvOffsetDraft , "Face UV offset" ) ,
scale : readPositiveVec2Draft ( uvScaleDraft , "Face UV scale" )
} , "Set face UV offset and scale" , "Updated face UV offset and scale." ) ;
}
catch ( error ) {
setStatusMessage ( getErrorMessage ( error ) ) ;
}
} ;
const handleRotateUv = ( ) => {
if ( selectedFace === null ) {
setStatusMessage ( "Select a single box face before rotating UVs." ) ;
return ;
}
applyFaceUvState ( {
... selectedFace . uv ,
rotationQuarterTurns : rotateQuarterTurns ( selectedFace . uv . rotationQuarterTurns )
} , "Rotate face UV 90 degrees" , "Rotated face UVs 90 degrees." ) ;
} ;
const handleFlipUv = ( axis ) => {
if ( selectedFace === null ) {
setStatusMessage ( "Select a single box face before flipping UVs." ) ;
return ;
}
applyFaceUvState ( {
... selectedFace . uv ,
flipU : axis === "u" ? ! selectedFace . uv . flipU : selectedFace . uv . flipU ,
flipV : axis === "v" ? ! selectedFace . uv . flipV : selectedFace . uv . flipV
} , axis === "u" ? "Flip face UV U" : "Flip face UV V" , axis === "u" ? "Flipped face UVs on U." : "Flipped face UVs on V." ) ;
} ;
const handleFitUvToFace = ( ) => {
if ( selectedBrush === null || selectedFaceId === null ) {
setStatusMessage ( "Select a single box face before fitting UVs." ) ;
return ;
}
applyFaceUvState ( createFitToFaceBoxBrushFaceUvState ( selectedBrush , selectedFaceId ) , "Fit face UV to face" , "Fit the selected face UVs to the face bounds." ) ;
} ;
const handleEnterPlayMode = ( ) => {
if ( blockingDiagnostics . length > 0 ) {
setStatusMessage ( ` Run mode blocked: ${ formatSceneDiagnosticSummary ( blockingDiagnostics ) } ` ) ;
return ;
}
try {
const nextRuntimeScene = buildRuntimeSceneFromDocument ( editorState . document , {
navigationMode : preferredNavigationMode ,
loadedModelAssets
} ) ;
const nextNavigationMode = preferredNavigationMode ;
setRuntimeScene ( nextRuntimeScene ) ;
setRuntimeMessage ( nextRuntimeScene . spawn . source === "playerStart"
? "Running from the authored Player Start."
: "No Player Start is authored yet. Orbit Visitor opened first, with a fallback FPS spawn still available." ) ;
setFirstPersonTelemetry ( null ) ;
setRuntimeInteractionPrompt ( null ) ;
setActiveNavigationMode ( nextNavigationMode ) ;
store . enterPlayMode ( ) ;
setStatusMessage ( nextNavigationMode === "firstPerson"
? "Entered run mode with first-person navigation."
: "Entered run mode with Orbit Visitor." ) ;
}
catch ( error ) {
setStatusMessage ( ` Run mode could not start: ${ getErrorMessage ( error ) } ` ) ;
}
} ;
const handleExitPlayMode = ( ) => {
setRuntimeScene ( null ) ;
setRuntimeMessage ( null ) ;
setFirstPersonTelemetry ( null ) ;
setRuntimeInteractionPrompt ( null ) ;
store . exitPlayMode ( ) ;
setStatusMessage ( "Returned to editor mode." ) ;
} ;
const handleSetPreferredNavigationMode = ( navigationMode ) => {
setPreferredNavigationMode ( navigationMode ) ;
if ( navigationMode === "firstPerson" && primaryPlayerStart === null ) {
setStatusMessage ( "First Person selected. Author a Player Start before running, or switch back to Orbit Visitor." ) ;
}
if ( editorState . toolMode === "play" ) {
setActiveNavigationMode ( navigationMode ) ;
setStatusMessage ( navigationMode === "firstPerson" ? "Runner switched to first-person navigation." : "Runner switched to Orbit Visitor." ) ;
}
} ;
const createAssetMenuHoverHandler = ( assetId ) => ( hovered ) => {
setHoveredAssetId ( ( current ) => ( hovered ? assetId : current === assetId ? null : current ) ) ;
} ;
const createDisabledMenuAction = ( label , testId ) => ( {
kind : "action" ,
label ,
testId ,
disabled : true ,
onSelect : ( ) => undefined
} ) ;
const addMenuItems = [
{
kind : "action" ,
label : "Whitebox Box" ,
testId : "add-menu-box" ,
onSelect : beginBoxCreation
} ,
{
kind : "group" ,
label : "Entities" ,
testId : "add-menu-entities" ,
children : [
{
kind : "action" ,
label : "Player Start" ,
testId : "add-menu-player-start" ,
onSelect : ( ) => beginEntityCreation ( "playerStart" )
} ,
{
kind : "action" ,
label : "Sound Emitter" ,
testId : "add-menu-sound-emitter" ,
onSelect : ( ) => beginEntityCreation ( "soundEmitter" , { audioAssetId : audioAssetList [ 0 ] ? . id ? ? null } )
} ,
{
kind : "action" ,
label : "Trigger Volume" ,
testId : "add-menu-trigger-volume" ,
onSelect : ( ) => beginEntityCreation ( "triggerVolume" )
} ,
{
kind : "action" ,
label : "Teleport Target" ,
testId : "add-menu-teleport-target" ,
onSelect : ( ) => beginEntityCreation ( "teleportTarget" )
} ,
{
kind : "action" ,
label : "Interactable" ,
testId : "add-menu-interactable" ,
onSelect : ( ) => beginEntityCreation ( "interactable" )
}
]
} ,
{
kind : "group" ,
label : "Lights" ,
testId : "add-menu-lights" ,
children : [
{
kind : "action" ,
label : "Point Light" ,
testId : "add-menu-point-light" ,
onSelect : ( ) => beginEntityCreation ( "pointLight" )
} ,
{
kind : "action" ,
label : "Spot Light" ,
testId : "add-menu-spot-light" ,
onSelect : ( ) => beginEntityCreation ( "spotLight" )
}
]
} ,
{
kind : "group" ,
label : "Assets" ,
testId : "add-menu-assets" ,
children : [
{
kind : "group" ,
label : "3D Models" ,
testId : "add-menu-assets-models" ,
children : modelAssetList . length === 0
? [ createDisabledMenuAction ( "No imported 3D models" , "add-menu-assets-models-empty" ) ]
: modelAssetList . map ( ( asset ) => ( {
kind : "action" ,
label : asset . sourceName ,
testId : ` add-menu-model-asset- ${ asset . id } ` ,
onSelect : ( ) => beginModelInstanceCreation ( asset . id ) ,
onHoverChange : createAssetMenuHoverHandler ( asset . id )
} ) )
} ,
{
kind : "group" ,
label : "Environments" ,
testId : "add-menu-assets-environments" ,
children : imageAssetList . length === 0
? [ createDisabledMenuAction ( "No imported environments" , "add-menu-assets-environments-empty" ) ]
: imageAssetList . map ( ( asset ) => ( {
kind : "action" ,
label : asset . sourceName ,
testId : ` add-menu-image-asset- ${ asset . id } ` ,
onSelect : ( ) => applyWorldBackgroundMode ( "image" , asset . id ) ,
onHoverChange : createAssetMenuHoverHandler ( asset . id )
} ) )
} ,
{
kind : "group" ,
label : "Audio" ,
testId : "add-menu-assets-audio" ,
children : audioAssetList . length === 0
? [ createDisabledMenuAction ( "No imported audio" , "add-menu-assets-audio-empty" ) ]
: audioAssetList . map ( ( asset ) => ( {
kind : "action" ,
label : asset . sourceName ,
testId : ` add-menu-audio-asset- ${ asset . id } ` ,
onSelect : ( ) => beginEntityCreation ( "soundEmitter" , { audioAssetId : asset . id } ) ,
onHoverChange : createAssetMenuHoverHandler ( asset . id )
} ) )
}
]
} ,
{
kind : "group" ,
label : "Import" ,
testId : "add-menu-import" ,
children : [
{
kind : "action" ,
label : "3D Model (GLB/GLTF)" ,
testId : "import-menu-model" ,
disabled : ! projectAssetStorageReady || projectAssetStorage === null ,
onSelect : handleImportModelButtonClick
} ,
{
kind : "action" ,
label : "Environment" ,
testId : "import-menu-environment" ,
disabled : ! projectAssetStorageReady || projectAssetStorage === null ,
onSelect : handleImportBackgroundImageButtonClick
} ,
{
kind : "action" ,
label : "Audio" ,
testId : "import-menu-audio" ,
disabled : ! projectAssetStorageReady || projectAssetStorage === null ,
onSelect : handleImportAudioButtonClick
}
]
}
] ;
const viewportPanelsStyle = layoutMode === "quad" ? createViewportQuadPanelsStyle ( editorState . viewportQuadSplit ) : undefined ;
if ( editorState . toolMode === "play" && runtimeScene !== null ) {
return ( _jsxs ( "div" , { className : "app-shell app-shell--play" , children : [ _jsxs ( "header" , { className : "toolbar" , children : [ _jsxs ( "div" , { className : "toolbar__brand" , children : [ _jsx ( "div" , { className : "toolbar__title" , children : "WebEditor3D" } ) , _jsx ( "div" , { className : "toolbar__subtitle" , children : "Slice 3.1 GLB/GLTF import and unified creation" } ) ] } ) , _jsxs ( "div" , { className : "toolbar__actions" , children : [ _jsxs ( "div" , { className : "toolbar__group" , children : [ _jsx ( "button" , { className : ` toolbar__button ${ activeNavigationMode === "firstPerson" ? "toolbar__button--active" : "" } ` , type : "button" , "data-testid" : "runner-mode-first-person" , onClick : ( ) => handleSetPreferredNavigationMode ( "firstPerson" ) , children : "First Person" } ) , _jsx ( "button" , { className : ` toolbar__button ${ activeNavigationMode === "orbitVisitor" ? "toolbar__button--active" : "" } ` , type : "button" , "data-testid" : "runner-mode-orbit-visitor" , onClick : ( ) => handleSetPreferredNavigationMode ( "orbitVisitor" ) , children : "Orbit Visitor" } ) ] } ) , _jsx ( "div" , { className : "toolbar__group" , children : _jsx ( "button" , { className : "toolbar__button toolbar__button--accent" , type : "button" , "data-testid" : "exit-run-mode" , onClick : handleExitPlayMode , children : "Return To Editor" } ) } ) ] } ) ] } ) , _jsxs ( "div" , { className : "runner-workspace" , children : [ _jsx ( "main" , { className : "runner-region" , children : _jsx ( RunnerCanvas , { runtimeScene : runtimeScene , projectAssets : editorState . document . assets , loadedModelAssets : loadedModelAssets , loadedImageAssets : loadedImageAssets , loadedAudioAssets : loadedAudioAssets , navigationMode : activeNavigationMode , onRuntimeMessageChange : setRuntimeMessage , onFirstPersonTelemetryChange : setFirstPersonTelemetry , onInteractionPromptChange : setRuntimeInteractionPrompt } ) } ) , _jsx ( "aside" , { className : "side-column" , children : _jsxs ( Panel , { title : "Runner" , children : [ _jsxs ( "div" , { className : "stat-grid" , children : [ _jsxs ( "div" , { className : "stat-card" , children : [ _jsx ( "div" , { className : "label" , children : "Navigation" } ) , _jsx ( "div" , { className : "value" , children : activeNavigationMode === "firstPerson" ? "First Person" : "Orbit Visitor" } ) ] } ) , _jsxs ( "div" , { className : "stat-card" , children : [ _jsx ( "div" , { className : "label" , children : "Spawn Source" } ) , _jsx ( "div" , { className : "value" , children : runtimeScene . spawn . source === "playerStart" ? "Player Start" : "Fallback" } ) ] } ) , _jsxs ( "div" , { className : "stat-card" , children : [ _jsx ( "div" , { className : "label" , children : "Pointer Lock" } ) , _jsx ( "div" , { className : "value" , children : activeNavigationMode === "firstPerson" ? ( firstPersonTelemetry ? . pointerLocked ? "active" : "idle" ) : "not used" } ) ] } ) , _jsxs ( "div" , { className : "stat-card" , children : [ _jsx ( "div" , { className : "label" , children : "Grounded" } ) , _jsx ( "div" , { className : "value" , children : firstPersonTelemetry ? . grounded ? "yes" : activeNavigationMode === "firstPerson" ? "no" : "n/a" } ) ] } ) ] } ) , _jsxs ( "div" , { className : "stat-card" , children : [ _jsx ( "div" , { className : "label" , children : "FPS Feet Position" } ) , _jsx ( "div" , { className : "value" , "data-testid" : "runner-player-position" , children : formatRunnerFeetPosition ( firstPersonTelemetry ? . feetPosition ? ? runtimeScene . spawn . position ) } ) , _jsxs ( "div" , { className : "material-summary" , "data-testid" : "runner-spawn-state" , children : [ "Spawn: " , runtimeScene . spawn . source === "playerStart" ? "Player Start" : "Fallback" , " at" , " " , formatRunnerFeetPosition ( runtimeScene . spawn . position ) ] } ) ] } ) , _jsxs ( "div" , { className : "stat-card" , children : [ _jsx ( "div" , { className : "label" , children : "Interaction" } ) , _jsx ( "div" , { className : "value" , "data-testid" : "runner-interaction-state" , children : activeNavigationMode === "firstPerson" ? ( runtimeInteractionPrompt === null ? "No target" : "Ready" ) : "Not available" } ) , _jsx ( "div" , { className : "material-summary" , "data-testid" : "runner-interaction-summary" , children : activeNavigationMode === "firstPerson"
? runtimeInteractionPrompt === null
? "Aim at an authored Interactable and click when a prompt appears."
: ` Click " ${ runtimeInteractionPrompt . prompt } " within ${ runtimeInteractionPrompt . range . toFixed ( 1 ) } m. `
: "Switch to First Person to use click interactions." } ) ] } ) , runtimeMessage === null ? null : _jsx ( "div" , { className : "info-banner" , children : runtimeMessage } ) , activeNavigationMode === "firstPerson" ? ( _jsx ( "div" , { className : "info-banner" , "data-testid" : "runner-interaction-help" , children : "Mouse click activates the current prompt target. Keyboard/controller fallback is not active yet." } ) ) : null ] } ) } ) ] } ) , _jsxs ( "footer" , { className : "status-bar" , children : [ _jsxs ( "div" , { children : [ _jsx ( "span" , { className : "status-bar__strong" , children : "Status:" } ) , " " , statusMessage ] } ) , _jsxs ( "div" , { children : [ _jsx ( "span" , { className : "status-bar__strong" , children : "Spawn:" } ) , " " , runtimeScene . spawn . source === "playerStart" ? "Authored Player Start" : "Fallback runtime spawn" ] } ) ] } ) ] } ) ) ;
}
return ( _jsxs ( "div" , { className : "app-shell" , children : [ _jsxs ( "header" , { className : "toolbar" , children : [ _jsxs ( "label" , { className : "toolbar__scene-name" , children : [ _jsx ( "span" , { className : "visually-hidden" , children : "Scene Name" } ) , _jsx ( "input" , { "data-testid" : "toolbar-scene-name" , className : "text-input toolbar__scene-name-input" , type : "text" , value : sceneNameDraft , onChange : ( event ) => setSceneNameDraft ( event . currentTarget . value ) , onBlur : applySceneName , onKeyDown : ( event ) => {
if ( event . key === "Enter" ) {
applySceneName ( ) ;
}
} } ) ] } ) , _jsxs ( "div" , { className : "toolbar__actions" , children : [ _jsxs ( "div" , { className : "toolbar__group" , children : [ _jsx ( "button" , { className : "toolbar__button toolbar__button--accent" , type : "button" , "data-testid" : "outliner-add-button" , "aria-haspopup" : "menu" , "aria-expanded" : addMenuPosition !== null , onClick : handleOpenAddMenuFromButton , children : "Add" } ) , _jsx ( "button" , { className : "toolbar__button" , type : "button" , disabled : ! editorState . storageAvailable , onClick : handleSaveDraft , children : "Save Draft" } ) , _jsx ( "button" , { className : "toolbar__button" , type : "button" , disabled : ! editorState . storageAvailable , onClick : handleLoadDraft , children : "Load Draft" } ) , _jsx ( "button" , { className : "toolbar__button" , type : "button" , onClick : handleExportJson , children : "Export JSON" } ) , _jsx ( "button" , { className : "toolbar__button" , type : "button" , onClick : handleImportJsonButtonClick , children : "Import JSON" } ) ] } ) , _jsx ( "div" , { className : "toolbar__group" , role : "group" , "aria-label" : "Viewport layout mode" , children : VIEWPORT _LAYOUT _MODES . map ( ( mode ) => ( _jsx ( "button" , { className : ` toolbar__button toolbar__button--compact ${ editorState . viewportLayoutMode === mode ? "toolbar__button--active" : "" } ` , type : "button" , "data-testid" : ` viewport-layout- ${ mode } ` , "aria-pressed" : editorState . viewportLayoutMode === mode , onClick : ( ) => handleSetViewportLayoutMode ( mode ) , children : getViewportLayoutModeLabel ( mode ) } , mode ) ) ) } ) , _jsxs ( "div" , { className : "toolbar__group" , role : "group" , "aria-label" : "Transform operations" , children : [ _jsxs ( "button" , { className : ` toolbar__button ${ transformSession . kind === "active" && transformSession . operation === "translate" ? "toolbar__button--active" : "" } ` , type : "button" , "data-testid" : "transform-translate-button" , "aria-pressed" : transformSession . kind === "active" && transformSession . operation === "translate" , disabled : editorState . toolMode !== "select" || ! canTranslateSelectedTarget , onClick : ( ) => beginTransformOperation ( "translate" , "toolbar" ) , children : [ "Move (" , getTransformOperationShortcut ( "translate" ) , ")" ] } ) , _jsxs ( "button" , { className : ` toolbar__button ${ transformSession . kind === "active" && transformSession . operation === "rotate" ? "toolbar__button--active" : "" } ` , type : "button" , "data-testid" : "transform-rotate-button" , "aria-pressed" : transformSession . kind === "active" && transformSession . operation === "rotate" , disabled : editorState . toolMode !== "select" || ! canRotateSelectedTarget , onClick : ( ) => beginTransformOperation ( "rotate" , "toolbar" ) , children : [ "Rotate (" , getTransformOperationShortcut ( "rotate" ) , ")" ] } ) , _jsxs ( "button" , { className : ` toolbar__button ${ transformSession . kind === "active" && transformSession . operation === "scale" ? "toolbar__button--active" : "" } ` , type : "button" , "data-testid" : "transform-scale-button" , "aria-pressed" : transformSession . kind === "active" && transformSession . operation === "scale" , disabled : editorState . toolMode !== "select" || ! canScaleSelectedTarget , onClick : ( ) => beginTransformOperation ( "scale" , "toolbar" ) , children : [ "Scale (" , getTransformOperationShortcut ( "scale" ) , ")" ] } ) ] } ) , _jsx ( "div" , { className : "toolbar__group" , role : "group" , "aria-label" : "Whitebox selection mode" , children : WHITEBOX _SELECTION _MODES . map ( ( mode ) => ( _jsx ( "button" , { className : ` toolbar__button toolbar__button--compact ${ whiteboxSelectionMode === mode ? "toolbar__button--active" : "" } ` , type : "button" , "data-testid" : ` whitebox-selection-mode- ${ mode } ` , "aria-pressed" : whiteboxSelectionMode === mode , onClick : ( ) => handleWhiteboxSelectionModeChange ( mode ) , children : getWhiteboxSelectionModeLabel ( mode ) } , mode ) ) ) } ) , _jsxs ( "div" , { className : "toolbar__group" , role : "group" , "aria-label" : "Whitebox snap settings" , children : [ _jsx ( "button" , { className : ` toolbar__button ${ whiteboxSnapEnabled ? "toolbar__button--active" : "" } ` , type : "button" , "data-testid" : "whitebox-snap-toggle" , "aria-pressed" : whiteboxSnapEnabled , onClick : handleWhiteboxSnapToggle , children : whiteboxSnapEnabled ? "Grid Snap On" : "Grid Snap Off"
if ( event . key === "Enter" ) {
handleWhiteboxSnapStepBlur ( ) ;
}
} } ) ] } ) ] } ) , _jsx ( "div" , { className : "toolbar__group" , children : _jsx ( "button" , { className : ` toolbar__button toolbar__button--accent ${ blockingDiagnostics . length > 0 ? "toolbar__button--warn" : "" } ` , type : "button" , "data-testid" : "enter-run-mode" , onClick : handleEnterPlayMode , children : "Run Scene" } ) } ) , _jsxs ( "div" , { className : "toolbar__group" , children : [ _jsx ( "button" , { className : ` toolbar__button ${ preferredNavigationMode === "firstPerson" ? "toolbar__button--active" : "" } ` , type : "button" , onClick : ( ) => handleSetPreferredNavigationMode ( "firstPerson" ) , children : "First Person" } ) , _jsx ( "button" , { className : ` toolbar__button ${ preferredNavigationMode === "orbitVisitor" ? "toolbar__button--active" : "" } ` , type : "button" , onClick : ( ) => handleSetPreferredNavigationMode ( "orbitVisitor" ) , children : "Orbit Visitor" } ) ] } ) , _jsxs ( "div" , { className : "toolbar__group" , children : [ _jsx ( "button" , { className : "toolbar__button" , type : "button" , disabled : ! editorState . canUndo , onClick : ( ) => store . undo ( ) , children : "Undo" } ) , _jsx ( "button" , { className : "toolbar__button" , type : "button" , disabled : ! editorState . canRedo , onClick : ( ) => store . redo ( ) , children : "Redo" } ) ] } ) ] } ) ] } ) , _jsxs ( "div" , { className : "workspace" , children : [ _jsx ( "aside" , { className : "side-column" , children : _jsxs ( Panel , { title : "Outliner" , children : [ assetStatusMessage === null ? null : ( _jsx ( "div" , { className : "info-banner" , "data-testid" : "asset-status-message" , children : assetStatusMessage } ) ) , projectAssetStorageReady && projectAssetStorage === null ? ( _jsx ( "div" , { className : "outliner-empty" , children : "Project asset storage is unavailable. Imported assets cannot be persisted." } ) ) : null , _jsxs ( "div" , { className : "outliner-section" , children : [ _jsx ( "div" , { className : "label" , children : "Whitebox Solids" } ) , brushList . length === 0 ? ( _jsx ( "div" , { className : "outliner-empty" , children : "Use Add > Whitebox Box and click in the viewport to create the first solid." } ) ) : ( _jsx ( "div" , { className : "outliner-list" , "data-testid" : "outliner-brush-list" , children : brushList . map ( ( brush , brushIndex ) => {
const label = getBrushLabel ( brush , brushIndex ) ;
const isSelected = selectedBrush ? . id === brush . id ;
return ( _jsx ( "div" , { className : ` outliner-item outliner-item--compact ${ isBrushSelected ( editorState . selection , brush . id ) ? "outliner-item--selected" : "" } ` , children : _jsxs ( "div" , { className : "outliner-item__row" , children : [ isSelected ? ( _jsx ( "input" , { className : "outliner-item__rename" , "data-testid" : "selected-brush-name" , type : "text" , value : brushNameDraft , placeholder : ` Whitebox Box ${ brushIndex + 1 } ` , onChange : ( event ) => setBrushNameDraft ( event . currentTarget . value ) , onBlur : applyBrushNameChange , onFocus : ( event ) => event . currentTarget . select ( ) , onKeyDown : ( event ) => handleInlineNameInputKeyDown ( event , ( ) => {
setBrushNameDraft ( selectedBrush ? . name ? ? "" ) ;
} ) } ) ) : ( _jsx ( "button" , { className : "outliner-item__select" , type : "button" , "data-testid" : ` outliner-brush- ${ brush . id } ` , onClick : ( ) => applySelection ( {
kind : "brushes" ,
ids : [ brush . id ]
} , "outliner" , {
focusViewport : true
} ) , children : _jsx ( "span" , { className : "outliner-item__title" , children : label } ) } ) ) , _jsx ( "button" , { className : "outliner-item__delete" , type : "button" , "data-testid" : ` outliner-delete-brush- ${ brush . id } ` , "aria-label" : ` Delete ${ label } ` , onClick : ( ) => handleDeleteBrush ( brush . id ) , children : "x" } ) ] } ) } , brush . id ) ) ;
} ) } ) ) ] } ) , _jsxs ( "div" , { className : "outliner-section" , children : [ _jsx ( "div" , { className : "label" , children : "Model Instances" } ) , modelInstanceDisplayList . length === 0 ? ( _jsx ( "div" , { className : "outliner-empty" , children : "No model instances placed yet." } ) ) : ( _jsx ( "div" , { className : "outliner-list" , "data-testid" : "outliner-model-instance-list" , children : modelInstanceDisplayList . map ( ( { modelInstance , label } ) => {
const isSelected = editorState . selection . kind === "modelInstances" && editorState . selection . ids . includes ( modelInstance . id ) ;
return ( _jsx ( "div" , { className : ` outliner-item ${ isSelected ? "outliner-item--selected" : "" } outliner-item--compact ` , children : _jsxs ( "div" , { className : "outliner-item__row" , children : [ isSelected ? ( _jsx ( "input" , { className : "outliner-item__rename" , "data-testid" : "selected-model-instance-name" , type : "text" , value : modelInstanceNameDraft , placeholder : editorState . document . assets [ modelInstance . assetId ] ? . sourceName ? ? "Model Instance" , onChange : ( event ) => setModelInstanceNameDraft ( event . currentTarget . value ) , onBlur : applyModelInstanceNameChange , onFocus : ( event ) => event . currentTarget . select ( ) , onKeyDown : ( event ) => handleInlineNameInputKeyDown ( event , ( ) => {
setModelInstanceNameDraft ( selectedModelInstance ? . name ? ? "" ) ;
} ) } ) ) : ( _jsx ( "button" , { "data-testid" : ` outliner-model-instance- ${ modelInstance . id } ` , className : "outliner-item__select" , type : "button" , onClick : ( ) => applySelection ( {
kind : "modelInstances" ,
ids : [ modelInstance . id ]
} , "outliner" , {
focusViewport : true
} ) , children : _jsx ( "span" , { className : "outliner-item__title" , children : label } ) } ) ) , _jsx ( "button" , { className : "outliner-item__delete" , type : "button" , "data-testid" : ` outliner-delete-model-instance- ${ modelInstance . id } ` , "aria-label" : ` Delete ${ label } ` , onClick : ( ) => handleDeleteModelInstance ( modelInstance . id ) , children : "x" } ) ] } ) } , modelInstance . id ) ) ;
} ) } ) ) ] } ) , _jsxs ( "div" , { className : "outliner-section" , children : [ _jsx ( "div" , { className : "label" , children : "Entities" } ) , entityDisplayList . length === 0 ? _jsx ( "div" , { className : "outliner-empty" , children : "No entities authored yet." } ) : null , entityDisplayList . length === 0 ? null : ( _jsx ( "div" , { className : "outliner-list" , children : entityDisplayList . map ( ( { entity , label } ) => {
const isSelected = editorState . selection . kind === "entities" && editorState . selection . ids . includes ( entity . id ) ;
return ( _jsx ( "div" , { className : ` outliner-item ${ isSelected ? "outliner-item--selected" : "" } outliner-item--compact ` , children : _jsxs ( "div" , { className : "outliner-item__row" , children : [ isSelected ? ( _jsx ( "input" , { className : "outliner-item__rename" , "data-testid" : "selected-entity-name" , type : "text" , value : entityNameDraft , placeholder : getEntityKindLabel ( entity . kind ) , onChange : ( event ) => setEntityNameDraft ( event . currentTarget . value ) , onBlur : applyEntityNameChange , onFocus : ( event ) => event . currentTarget . select ( ) , onKeyDown : ( event ) => handleInlineNameInputKeyDown ( event , ( ) => {
setEntityNameDraft ( selectedEntity ? . name ? ? "" ) ;
} ) } ) ) : ( _jsx ( "button" , { "data-testid" : ` outliner-entity- ${ entity . id } ` , className : "outliner-item__select" , type : "button" , onClick : ( ) => applySelection ( {
kind : "entities" ,
ids : [ entity . id ]
} , "outliner" , {
focusViewport : true
} ) , children : _jsx ( "span" , { className : "outliner-item__title" , children : label } ) } ) ) , _jsx ( "button" , { className : "outliner-item__delete" , type : "button" , "data-testid" : ` outliner-delete-entity- ${ entity . id } ` , "aria-label" : ` Delete ${ label } ` , onClick : ( ) => handleDeleteEntity ( entity . id ) , children : "x" } ) ] } ) } , entity . id ) ) ;
} ) } ) ) ] } ) ] } ) } ) , _jsx ( "main" , { className : ` viewport-region viewport-region-- ${ layoutMode } ` , "data-testid" : "viewport-shell" , children : _jsxs ( "div" , { ref : viewportPanelsRef , className : ` viewport-region__panels viewport-region__panels-- ${ layoutMode } ${ viewportQuadResizeMode === null ? "" : "viewport-region__panels--resizing" } ` . trim ( ) , style : viewportPanelsStyle , children : [ VIEWPORT _PANEL _IDS . map ( ( panelId ) => ( _jsx ( ViewportPanel , { panelId : panelId , className : ` viewport-panel-- ${ panelId } ` , panelState : editorState . viewportPanels [ panelId ] , layoutMode : layoutMode , isActive : activePanelId === panelId , world : editorState . document . world , sceneDocument : editorState . document , projectAssets : editorState . document . assets , loadedModelAssets : loadedModelAssets , loadedImageAssets : loadedImageAssets , whiteboxSelectionMode : whiteboxSelectionMode , whiteboxSnapEnabled : whiteboxSnapEnabled , whiteboxSnapStep : whiteboxSnapStep , selection : editorState . selection , toolMode : editorState . toolMode , toolPreview : viewportToolPreview , transformSession : transformSession , cameraState : editorState . viewportPanels [ panelId ] . cameraState , focusRequestId : focusRequest . panelId === panelId ? focusRequest . id : 0 , focusSelection : focusRequest . selection , onActivatePanel : handleActivateViewportPanel , onSetPanelViewMode : handleSetViewportPanelViewMode , onSetPanelDisplayMode : handleSetViewportPanelDisplayMode , onCommitCreation : handleCommitCreation , onCameraStateChange : ( cameraState ) => {
store . setViewportPanelCameraState ( panelId , cameraState ) ;
} , onToolPreviewChange : ( toolPreview ) => {
store . setViewportToolPreview ( toolPreview ) ;
} , onTransformSessionChange : ( nextTransformSession ) => {
store . setTransformSession ( nextTransformSession ) ;
} , onTransformCommit : commitTransformSession , onTransformCancel : ( ) => cancelTransformSession ( ) , onSelectionChange : ( selection ) => applySelection ( selection , "viewport" ) } , panelId ) ) ) , layoutMode !== "quad" ? null : ( _jsxs ( _Fragment , { children : [ _jsx ( "div" , { className : "viewport-region__splitter viewport-region__splitter--vertical" , "data-testid" : "viewport-quad-splitter-vertical" , onPointerDown : handleViewportQuadResizeStart ( "vertical" ) } ) , _jsx ( "div" , { className : "viewport-region__splitter viewport-region__splitter--horizontal" , "data-testid" : "viewport-quad-splitter-horizontal" , onPointerDown : handleViewportQuadResizeStart ( "horizontal" ) } ) , _jsx ( "div" , { className : "viewport-region__splitter viewport-region__splitter--center" , "data-testid" : "viewport-quad-splitter-center" , onPointerDown : handleViewportQuadResizeStart ( "center" ) } ) ] } ) ) ] } ) } ) , _jsx ( "aside" , { className : "side-column" , children : editorState . selection . kind === "none" ? ( _jsxs ( Panel , { title : "World" , children : [ _jsxs ( "div" , { className : "stat-card" , children : [ _jsx ( "div" , { className : "label" , children : "Background" } ) , _jsx ( "div" , { className : "value" , "data-testid" : "world-background-mode-value" , children : formatWorldBackgroundLabel ( editorState . document . world ) } ) , _jsx ( "div" , { className : "world-background-preview" , "data-testid" : "world-background-preview" , style : createWorldBackgroundStyle ( editorState . document . world . background , editorState . document . world . background . mode === "image"
? loadedImageAssets [ editorState . document . world . background . assetId ] ? . sourceUrl ? ? null
: null ) } ) , _jsx ( "div" , { className : "material-summary" , children : editorState . document . world . background . mode === "solid"
? editorState . document . world . background . colorHex
: editorState . document . world . background . mode === "verticalGradient"
? ` ${ editorState . document . world . background . topColorHex } -> ${ editorState . document . world . background . bottomColorHex } `
: editorState . document . assets [ editorState . document . world . background . assetId ] ? . sourceName ? ?
editorState . document . world . background . assetId } ) , editorState . document . world . background . mode !== "image" ? null : ( _jsxs ( "div" , { className : "material-summary" , "data-testid" : "world-background-asset-value" , children : [ "Background Asset:" , " " , editorState . document . assets [ editorState . document . world . background . assetId ] ? . sourceName ? ?
editorState . document . world . background . assetId ] } ) ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Background Mode" } ) , _jsxs ( "div" , { className : "inline-actions" , children : [ _jsx ( "button" , { className : ` toolbar__button ${ editorState . document . world . background . mode === "solid" ? "toolbar__button--active" : "" } ` , type : "button" , "data-testid" : "world-background-mode-solid" , onClick : ( ) => applyWorldBackgroundMode ( "solid" ) , children : "Solid" } ) , _jsx ( "button" , { className : ` toolbar__button ${ editorState . document . world . background . mode === "verticalGradient" ? "toolbar__button--active" : "" } ` , type : "button" , "data-testid" : "world-background-mode-gradient" , onClick : ( ) => applyWorldBackgroundMode ( "verticalGradient" ) , children : "Gradient" } ) , _jsx ( "button" , { className : ` toolbar__button ${ editorState . document . world . background . mode === "image" ? "toolbar__button--active" : "" } ` , type : "button" , "data-testid" : "world-background-mode-image" , onClick : ( ) => applyWorldBackgroundMode ( "image" ) , children : "Image" } ) ] } ) ] } ) , editorState . document . world . background . mode === "image" && ( _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Environment Intensity" } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Intensity" } ) , _jsx ( "input" , { "data-testid" : "world-background-environment-intensity" , className : "text-input" , type : "number" , min : "0" , step : "0.1" , value : backgroundEnvironmentIntensityDraft , onChange : ( event ) => setBackgroundEnvironmentIntensityDraft ( event . currentTarget . value ) , onBlur : applyBackgroundEnvironmentIntensity , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyBackgroundEnvironmentIntensity ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyBackgroundEnvironmentIntensity ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyBackgroundEnvironmentIntensity ) } ) ] } ) ] } ) ) , editorState . document . world . background . mode !== "image" && ( _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Background Colors" } ) , editorState . document . world . background . mode === "solid" ? ( _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Color" } ) , _jsx ( "input" , { "data-testid" : "world-background-solid-color" , className : "color-input" , type : "color" , value : editorState . document . world . background . colorHex , onChange : ( event ) => applyWorldBackgroundColor ( event . currentTarget . value ) } ) ] } ) ) : ( _jsxs ( "div" , { className : "vector-inputs vector-inputs--two" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Top" } ) , _jsx ( "input" , { "data-testid" : "world-background-top-color" , className : "color-input" , type : "color" , value : editorState . document . world . background . topColorHex , onChange : ( event ) => applyWorldGradientColor ( "top" , event . currentTarget . value ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Bottom" } ) , _jsx ( "input" , { "data-testid" : "world-background-bottom-color" , className : "color-input" , type : "color" , value : editorState . document . world . background . bottomColorHex , onChange : ( event ) => applyWorldGradientColor ( "bottom" , event . currentTarget . value ) } ) ] } ) ] } ) ) ] } ) ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Ambient Light" } ) , _jsxs ( "div" , { className : "vector-inputs vector-inputs--two" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Color" } ) , _jsx ( "input" , { "data-testid" : "world-ambient-color" , className : "color-input" , type : "color" , value : editorState . document . world . ambientLight . colorHex , onChange : ( event ) => applyAmbientLightColor ( event . currentTarget . value ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Intensity" } ) , _jsx ( "input" , { " da
const nextValue = event . currentTarget . value ;
setSunDirectionDraft ( ( draft ) => ( { ... draft , x : nextValue } ) ) ;
} , onBlur : applySunLightDirection , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySunLightDirection ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySunLightDirection ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySunLightDirection ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Dir Y" } ) , _jsx ( "input" , { "data-testid" : "world-sun-direction-y" , className : "text-input" , type : "number" , step : "0.1" , value : sunDirectionDraft . y , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setSunDirectionDraft ( ( draft ) => ( { ... draft , y : nextValue } ) ) ;
} , onBlur : applySunLightDirection , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySunLightDirection ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySunLightDirection ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySunLightDirection ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Dir Z" } ) , _jsx ( "input" , { "data-testid" : "world-sun-direction-z" , className : "text-input" , type : "number" , step : "0.1" , value : sunDirectionDraft . z , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setSunDirectionDraft ( ( draft ) => ( { ... draft , z : nextValue } ) ) ;
} , onBlur : applySunLightDirection , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySunLightDirection ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySunLightDirection ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySunLightDirection ) } ) ] } ) ] } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Advanced Rendering" } ) , _jsxs ( "label" , { className : "form-field form-field--toggle" , children : [ _jsx ( "span" , { className : "label" , children : "Advanced Rendering" } ) , _jsx ( "input" , { type : "checkbox" , checked : advancedRendering . enabled , onChange : ( event ) => applyAdvancedRenderingEnabled ( event . currentTarget . checked ) } ) ] } ) , ! advancedRendering . enabled ? null : ( _jsxs ( _Fragment , { children : [ _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Shadows" } ) , _jsxs ( "label" , { className : "form-field form-field--toggle" , children : [ _jsx ( "span" , { className : "label" , children : "Enabled" } ) , _jsx ( "input" , { type : "checkbox" , checked : advancedRendering . shadows . enabled , onChange : ( event ) => applyAdvancedRenderingShadowsEnabled ( event . currentTarget . checked ) } ) ] } ) , _jsxs ( "div" , { className : "vector-inputs vector-inputs--two" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Shadow Map Size" } ) , _jsx ( "select" , { className : "select-input" , value : advancedRendering . shadows . mapSize , onChange : ( event ) => applyAdvancedRenderingShadowMapSize ( Number ( event . currentTarget . value ) ) , children : ADVANCED _RENDERING _SHADOW _MAP _SIZES . map ( ( size ) => ( _jsx ( "option" , { value : size , children : size } , size ) ) ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Shadow Type" } ) , _jsx ( "select" , { className : "select-input" , value : advancedRendering . shadows . type , onChange : ( event ) => applyAdvancedRenderingShadowType ( event . currentTarget . value ) , children : ADVANCED _RENDERING _SHADOW _TYPES . map ( ( shadowType ) => ( _jsx ( "option" , { value : shadowType , children : formatAdvancedRenderingShadowTypeLabel ( shadowType ) } , shadowType ) ) ) } ) ] } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Bias" } ) , _jsx ( "input" , { className : "text-input" , type : "number" , step : "0.0001" , value : advancedRenderingShadowBiasDraft , onChange : ( event ) => setAdvancedRenderingShadowBiasDraft ( event . currentTarget . value ) , onBlur : applyAdvancedRenderingShadowBias , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyAdvancedRenderingShadowBias ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyAdvancedRenderingShadowBias ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyAdvancedRenderingShadowBias ) } ) ] } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Ambient Occlusion" } ) , _jsxs ( "label" , { className : "form-field form-field--toggle" , children : [ _jsx ( "span" , { className : "label" , children : "Enabled" } ) , _jsx ( "input" , { type : "checkbox" , checked : advancedRendering . ambientOcclusion . enabled , onChange : ( event ) => applyAdvancedRenderingAmbientOcclusionEnabled ( event . currentTarget . checked ) } ) ] } ) , _jsxs ( "div" , { className : "vector-inputs vector-inputs--two" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Intensity" } ) , _jsx ( "input" , { className : "text-input" , type : "number" , min : "0" , step : "0.1" , value : advancedRenderingAmbientOcclusionIntensityDraft , onChange : ( event ) => setAdvancedRenderingAmbientOcclusionIntensityDraft ( event . currentTarget . value ) , onBlur : applyAdvancedRenderingAmbientOcclusionIntensity , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyAdvancedRenderingAmbientOcclusionIntensity ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyAdvancedRenderingAmbientOcclusionIntensity ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyAdvancedRenderingAmbientOcclusionIntensity ) } ) ] } ) , _jsxs ( "
? "This model instance references an asset that is missing from the registry."
: formatModelAssetSummary ( selectedModelAssetRecord ) } ) , selectedModelAssetRecord === null ? null : ( _jsx ( "div" , { className : "material-summary" , children : formatModelBoundingBoxLabel ( selectedModelAssetRecord ) } ) ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Position" } ) , _jsxs ( "div" , { className : "vector-inputs" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "X" } ) , _jsx ( "input" , { "data-testid" : "model-instance-position-x" , className : "text-input" , type : "number" , step : DEFAULT _GRID _SIZE , value : modelPositionDraft . x , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setModelPositionDraft ( ( draft ) => ( { ... draft , x : nextValue } ) ) ;
} , onBlur : applyModelInstanceChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyModelInstanceChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyModelInstanceChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyModelInstanceChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Y" } ) , _jsx ( "input" , { "data-testid" : "model-instance-position-y" , className : "text-input" , type : "number" , step : DEFAULT _GRID _SIZE , value : modelPositionDraft . y , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setModelPositionDraft ( ( draft ) => ( { ... draft , y : nextValue } ) ) ;
} , onBlur : applyModelInstanceChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyModelInstanceChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyModelInstanceChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyModelInstanceChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Z" } ) , _jsx ( "input" , { "data-testid" : "model-instance-position-z" , className : "text-input" , type : "number" , step : DEFAULT _GRID _SIZE , value : modelPositionDraft . z , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setModelPositionDraft ( ( draft ) => ( { ... draft , z : nextValue } ) ) ;
} , onBlur : applyModelInstanceChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyModelInstanceChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyModelInstanceChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyModelInstanceChange ) } ) ] } ) ] } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Rotation" } ) , _jsxs ( "div" , { className : "vector-inputs" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "X" } ) , _jsx ( "input" , { "data-testid" : "model-instance-rotation-x" , className : "text-input" , type : "number" , step : "1" , value : modelRotationDraft . x , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setModelRotationDraft ( ( draft ) => ( { ... draft , x : nextValue } ) ) ;
} , onBlur : applyModelInstanceChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyModelInstanceChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyModelInstanceChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyModelInstanceChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Y" } ) , _jsx ( "input" , { "data-testid" : "model-instance-rotation-y" , className : "text-input" , type : "number" , step : "1" , value : modelRotationDraft . y , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setModelRotationDraft ( ( draft ) => ( { ... draft , y : nextValue } ) ) ;
} , onBlur : applyModelInstanceChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyModelInstanceChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyModelInstanceChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyModelInstanceChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Z" } ) , _jsx ( "input" , { "data-testid" : "model-instance-rotation-z" , className : "text-input" , type : "number" , step : "1" , value : modelRotationDraft . z , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setModelRotationDraft ( ( draft ) => ( { ... draft , z : nextValue } ) ) ;
} , onBlur : applyModelInstanceChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyModelInstanceChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyModelInstanceChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyModelInstanceChange ) } ) ] } ) ] } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Scale" } ) , _jsxs ( "div" , { className : "vector-inputs" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "X" } ) , _jsx ( "input" , { "data-testid" : "model-instance-scale-x" , className : "text-input" , type : "number" , min : "0.001" , step : "0.1" , value : modelScaleDraft . x , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setModelScaleDraft ( ( draft ) => ( { ... draft , x : nextValue } ) ) ;
} , onBlur : applyModelInstanceChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyModelInstanceChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyModelInstanceChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyModelInstanceChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Y" } ) , _jsx ( "input" , { "data-testid" : "model-instance-scale-y" , className : "text-input" , type : "number" , min : "0.001" , step : "0.1" , value : modelScaleDraft . y , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setModelScaleDraft ( ( draft ) => ( { ... draft , y : nextValue } ) ) ;
} , onBlur : applyModelInstanceChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyModelInstanceChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyModelInstanceChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyModelInstanceChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Z" } ) , _jsx ( "input" , { "data-testid" : "model-instance-scale-z" , className : "text-input" , type : "number" , min : "0.001" , step : "0.1" , value : modelScaleDraft . z , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setModelScaleDraft ( ( draft ) => ( { ... draft , z : nextValue } ) ) ;
} , onBlur : applyModelInstanceChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyModelInstanceChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyModelInstanceChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyModelInstanceChange ) } ) ] } ) ] } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Collision" } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Mode" } ) , _jsx ( "select" , { "data-testid" : "model-instance-collision-mode" , className : "select-input" , value : selectedModelInstance . collision . mode , onChange : ( event ) => {
store . executeCommand ( createUpsertModelInstanceCommand ( {
modelInstance : {
... selectedModelInstance ,
collision : {
... selectedModelInstance . collision ,
mode : event . target . value
}
} ,
label : "Set model collision mode"
} ) ) ;
} , children : MODEL _INSTANCE _COLLISION _MODES . map ( ( mode ) => ( _jsx ( "option" , { value : mode , children : mode } , mode ) ) ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "input" , { "data-testid" : "model-instance-collision-visible" , type : "checkbox" , checked : selectedModelInstance . collision . visible , onChange : ( event ) => {
store . executeCommand ( createUpsertModelInstanceCommand ( {
modelInstance : {
... selectedModelInstance ,
collision : {
... selectedModelInstance . collision ,
visible : event . target . checked
}
} ,
label : event . target . checked ? "Show model collision debug" : "Hide model collision debug"
} ) ) ;
} } ) , _jsx ( "span" , { className : "label" , children : "Show generated collision debug" } ) ] } ) , _jsx ( "div" , { className : "material-summary" , children : getModelInstanceCollisionModeDescription ( selectedModelInstance . collision . mode ) } ) ] } ) , selectedModelAssetRecord !== null && selectedModelAssetRecord . metadata . animationNames . length > 0 && ( _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Animation" } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Clip" } ) , _jsxs ( "select" , { className : "select-input" , value : selectedModelInstance . animationClipName ? ? "" , onChange : ( e ) => {
const clipName = e . target . value || undefined ;
store . executeCommand ( createUpsertModelInstanceCommand ( {
modelInstance : { ... selectedModelInstance , animationClipName : clipName } ,
label : "Set animation clip"
} ) ) ;
} , children : [ _jsx ( "option" , { value : "" , children : "\u2014 none \u2014" } ) , selectedModelAssetRecord . metadata . animationNames . map ( ( name ) => ( _jsx ( "option" , { value : name , children : name } , name ) ) ) ] } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "input" , { type : "checkbox" , checked : selectedModelInstance . animationAutoplay ? ? false , onChange : ( e ) => {
store . executeCommand ( createUpsertModelInstanceCommand ( {
modelInstance : { ... selectedModelInstance , animationAutoplay : e . target . checked } ,
label : "Set animation autoplay"
} ) ) ;
} } ) , _jsx ( "span" , { className : "label" , children : "Autoplay on scene load" } ) ] } ) ] } ) ) , _jsx ( "div" , { className : "inline-actions" , children : _jsx ( "button" , { className : "toolbar__button" , type : "button" , "data-testid" : "apply-model-instance" , onClick : applyModelInstanceChange , children : "Apply Transform" } ) } ) ] } ) ) : selectedEntity !== null ? ( _jsxs ( _Fragment , { children : [ _jsxs ( "div" , { className : "stat-card" , children : [ _jsx ( "div" , { className : "label" , children : "Entity Kind" } ) , _jsx ( "div" , { className : "value" , children : getEntityKindLabel ( selectedEntity . kind ) } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Position" } ) , _jsxs ( "div" , { className : "vector-inputs" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "X" } ) , _jsx ( "input" , { "data-testid" : selectedEntity . kind === "playerStart" ? "player-start-position-x" : ` ${ selectedEntity . kind } -position-x ` , className : "text-input" , type : "number" , step : DEFAULT _GRID _SIZE , value : entityPositionDraft . x , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setEntityPositionDraft ( ( draft ) => ( { ... draft , x : nextValue } ) ) ;
} , onBlur : applySelectedEntityDraftChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySelectedEntityDraftChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySelectedEntityDraftChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySelectedEntityDraftChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Y" } ) , _jsx ( "input" , { "data-testid" : selectedEntity . kind === "playerStart" ? "player-start-position-y" : ` ${ selectedEntity . kind } -position-y ` , className : "text-input" , type : "number" , step : DEFAULT _GRID _SIZE , value : entityPositionDraft . y , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setEntityPositionDraft ( ( draft ) => ( { ... draft , y : nextValue } ) ) ;
} , onBlur : applySelectedEntityDraftChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySelectedEntityDraftChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySelectedEntityDraftChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySelectedEntityDraftChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Z" } ) , _jsx ( "input" , { "data-testid" : selectedEntity . kind === "playerStart" ? "player-start-position-z" : ` ${ selectedEntity . kind } -position-z ` , className : "text-input" , type : "number" , step : DEFAULT _GRID _SIZE , value : entityPositionDraft . z , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setEntityPositionDraft ( ( draft ) => ( { ... draft , z : nextValue } ) ) ;
} , onBlur : applySelectedEntityDraftChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySelectedEntityDraftChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySelectedEntityDraftChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySelectedEntityDraftChange ) } ) ] } ) ] } ) ] } ) , selectedPointLight !== null ? ( _jsxs ( _Fragment , { children : [ _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Light" } ) , _jsxs ( "div" , { className : "vector-inputs vector-inputs--two" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Color" } ) , _jsx ( "input" , { "data-testid" : "point-light-color" , className : "color-input" , type : "color" , value : pointLightColorDraft , onChange : ( event ) => {
const nextColorHex = event . currentTarget . value ;
setPointLightColorDraft ( nextColorHex ) ;
scheduleDraftCommit ( ( ) => applyPointLightChange ( { colorHex : nextColorHex } ) ) ;
} } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Intensity" } ) , _jsx ( "input" , { "data-testid" : "point-light-intensity" , className : "text-input" , type : "number" , min : "0" , step : "0.1" , value : pointLightIntensityDraft , onChange : ( event ) => setPointLightIntensityDraft ( event . currentTarget . value ) , onBlur : ( ) => applyPointLightChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyPointLightChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyPointLightChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyPointLightChange ) } ) ] } ) ] } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Range" } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Distance" } ) , _jsx ( "input" , { "data-testid" : "point-light-distance" , className : "text-input" , type : "number" , min : "0.1" , step : "0.1" , value : pointLightDistanceDraft , onChange : ( event ) => setPointLightDistanceDraft ( event . currentTarget . value ) , onBlur : ( ) => applyPointLightChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyPointLightChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyPointLightChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyPointLightChange ) } ) ] } ) ] } ) ] } ) ) : null , selectedSpotLight !== null ? ( _jsxs ( _Fragment , { children : [ _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Light" } ) , _jsxs ( "div" , { className : "vector-inputs vector-inputs--two" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Color" } ) , _jsx ( "input" , { "data-testid" : "spot-light-color" , className : "color-input" , type : "color" , value : spotLightColorDraft , onChange : ( event ) => {
const nextColorHex = event . currentTarget . value ;
setSpotLightColorDraft ( nextColorHex ) ;
scheduleDraftCommit ( ( ) => applySpotLightChange ( { colorHex : nextColorHex } ) ) ;
} } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Intensity" } ) , _jsx ( "input" , { "data-testid" : "spot-light-intensity" , className : "text-input" , type : "number" , min : "0" , step : "0.1" , value : spotLightIntensityDraft , onChange : ( event ) => setSpotLightIntensityDraft ( event . currentTarget . value ) , onBlur : ( ) => applySpotLightChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySpotLightChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySpotLightChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySpotLightChange ) } ) ] } ) ] } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Range" } ) , _jsxs ( "div" , { className : "vector-inputs vector-inputs--two" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Distance" } ) , _jsx ( "input" , { "data-testid" : "spot-light-distance" , className : "text-input" , type : "number" , min : "0.1" , step : "0.1" , value : spotLightDistanceDraft , onChange : ( event ) => setSpotLightDistanceDraft ( event . currentTarget . value ) , onBlur : ( ) => applySpotLightChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySpotLightChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySpotLightChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySpotLightChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Angle" } ) , _jsx ( "input" , { "data-testid" : "spot-light-angle" , className : "text-input" , type : "number" , min : "1" , max : "179" , step : "1" , value : spotLightAngleDraft , onChange : ( event ) => setSpotLightAngleDraft ( event . currentTarget . value ) , onBlur : ( ) => applySpotLightChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySpotLightChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySpotLightChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySpotLightChange ) } ) ] } ) ] } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Direction" } ) , _jsxs ( "div" , { className : "vector-inputs" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "X" } ) , _jsx ( "input" , { "data-testid" : "spot-light-direction-x" , className : "text-input" , type : "number" , step : "0.1" , value : spotLightDirectionDraft . x , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setSpotLightDirectionDraft ( ( draft ) => ( { ... draft , x : nextValue } ) ) ;
} , onBlur : ( ) => applySpotLightChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySpotLightChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySpotLightChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySpotLightChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Y" } ) , _jsx ( "input" , { "data-testid" : "spot-light-direction-y" , className : "text-input" , type : "number" , step : "0.1" , value : spotLightDirectionDraft . y , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setSpotLightDirectionDraft ( ( draft ) => ( { ... draft , y : nextValue } ) ) ;
} , onBlur : ( ) => applySpotLightChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySpotLightChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySpotLightChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySpotLightChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Z" } ) , _jsx ( "input" , { "data-testid" : "spot-light-direction-z" , className : "text-input" , type : "number" , step : "0.1" , value : spotLightDirectionDraft . z , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setSpotLightDirectionDraft ( ( draft ) => ( { ... draft , z : nextValue } ) ) ;
} , onBlur : ( ) => applySpotLightChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySpotLightChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySpotLightChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySpotLightChange ) } ) ] } ) ] } ) ] } ) ] } ) ) : null , selectedPlayerStart !== null ? ( _jsxs ( _Fragment , { children : [ _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Yaw" } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Degrees" } ) , _jsx ( "input" , { "data-testid" : "player-start-yaw" , className : "text-input" , type : "number" , step : "1" , value : playerStartYawDraft , onChange : ( event ) => setPlayerStartYawDraft ( event . currentTarget . value ) , onBlur : ( ) => applyPlayerStartChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyPlayerStartChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyPlayerStartChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyPlayerStartChange ) } ) ] } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Player Collider" } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Mode" } ) , _jsx ( "select" , { "data-testid" : "player-start-collider-mode" , className : "select-input" , value : playerStartColliderModeDraft , onChange : ( event ) => {
const nextMode = event . currentTarget . value ;
setPlayerStartColliderModeDraft ( nextMode ) ;
scheduleDraftCommit ( ( ) => applyPlayerStartChange ( { colliderMode : nextMode } ) ) ;
} , children : PLAYER _START _COLLIDER _MODES . map ( ( mode ) => ( _jsx ( "option" , { value : mode , children : mode } , mode ) ) ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Eye Height" } ) , _jsx ( "input" , { "data-testid" : "player-start-eye-height" , className : "text-input" , type : "number" , min : "0.01" , step : "0.1" , value : playerStartEyeHeightDraft , onChange : ( event ) => setPlayerStartEyeHeightDraft ( event . currentTarget . value ) , onBlur : ( ) => applyPlayerStartChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyPlayerStartChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyPlayerStartChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyPlayerStartChange ) } ) ] } ) , playerStartColliderModeDraft === "capsule" ? ( _jsxs ( "div" , { className : "vector-inputs" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Radius" } ) , _jsx ( "input" , { "data-testid" : "player-start-capsule-radius" , className : "text-input" , type : "number" , min : "0.01" , step : "0.1" , value : playerStartCapsuleRadiusDraft , onChange : ( event ) => setPlayerStartCapsuleRadiusDraft ( event . currentTarget . value ) , onBlur : ( ) => applyPlayerStartChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyPlayerStartChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyPlayerStartChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyPlayerStartChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Height" } ) , _jsx ( "input" , { "data-testid" : "player-start-capsule-height" , className : "text-input" , type : "number" , min : "0.01" , step : "0.1" , value : playerStartCapsuleHeightDraft , onChange : ( event ) => setPlayerStartCapsuleHeightDraft ( event . currentTarget . value ) , onBlur : ( ) => applyPlayerStartChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyPlayerStartChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyPlayerStartChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyPlayerStartChange ) } ) ] } ) ] } ) ) : null , playerStartColliderModeDraft === "box" ? ( _jsxs ( "div" , { className : "vector-inputs" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Size X" } ) , _jsx ( "input" , { "data-testid" : "player-start-box-size-x" , className : "text-input" , type : "number" , min : "0.01" , step : "0.1" , value : playerStartBoxSizeDraft . x , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setPlayerStartBoxSizeDraft ( ( draft ) => ( { ... draft , x : nextValue } ) ) ;
} , onBlur : ( ) => applyPlayerStartChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyPlayerStartChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyPlayerStartChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyPlayerStartChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Size Y" } ) , _jsx ( "input" , { "data-testid" : "player-start-box-size-y" , className : "text-input" , type : "number" , min : "0.01" , step : "0.1" , value : playerStartBoxSizeDraft . y , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setPlayerStartBoxSizeDraft ( ( draft ) => ( { ... draft , y : nextValue } ) ) ;
} , onBlur : ( ) => applyPlayerStartChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyPlayerStartChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyPlayerStartChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyPlayerStartChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Size Z" } ) , _jsx ( "input" , { "data-testid" : "player-start-box-size-z" , className : "text-input" , type : "number" , min : "0.01" , step : "0.1" , value : playerStartBoxSizeDraft . z , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setPlayerStartBoxSizeDraft ( ( draft ) => ( { ... draft , z : nextValue } ) ) ;
} , onBlur : ( ) => applyPlayerStartChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyPlayerStartChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyPlayerStartChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyPlayerStartChange ) } ) ] } ) ] } ) ) : null , _jsx ( "div" , { className : "material-summary" , children : getPlayerStartColliderModeDescription ( playerStartColliderModeDraft ) } ) ] } ) ] } ) ) : null , selectedSoundEmitter !== null ? ( _jsxs ( _Fragment , { children : [ _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Audio Asset" } ) , _jsxs ( "div" , { className : "stat-card" , children : [ _jsx ( "div" , { className : "value" , children : selectedSoundEmitter . audioAssetId === null
? "Unassigned"
: selectedSoundEmitterAudioAssetRecord ? . sourceName ? ? "Missing Audio Asset" } ) , _jsx ( "div" , { className : "material-summary" , children : selectedSoundEmitter . audioAssetId === null
? "Choose an audio asset to make this emitter playable."
: selectedSoundEmitterAudioAssetRecord === null
? ` This sound emitter references ${ selectedSoundEmitter . audioAssetId } , but the asset is missing or not audio. `
: formatAudioAssetSummary ( selectedSoundEmitterAudioAssetRecord ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Audio" } ) , _jsxs ( "select" , { "data-testid" : "sound-emitter-audio-asset" , className : "text-input" , value : soundEmitterAudioAssetIdDraft , onChange : ( event ) => {
const nextAudioAssetId = event . currentTarget . value . trim ( ) ;
setSoundEmitterAudioAssetIdDraft ( nextAudioAssetId ) ;
scheduleDraftCommit ( ( ) => applySoundEmitterChange ( {
audioAssetId : nextAudioAssetId . length === 0 ? null : nextAudioAssetId
} ) ) ;
} , children : [ _jsx ( "option" , { value : "" , children : "\u2014 none \u2014" } ) , audioAssetList . map ( ( asset ) => ( _jsx ( "option" , { value : asset . id , children : asset . sourceName } , asset . id ) ) ) ] } ) ] } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Volume" } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Amount" } ) , _jsx ( "input" , { "data-testid" : "sound-emitter-volume" , className : "text-input" , type : "number" , min : "0" , step : "0.1" , value : soundEmitterVolumeDraft , onChange : ( event ) => setSoundEmitterVolumeDraft ( event . currentTarget . value ) , onBlur : ( ) => applySoundEmitterChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySoundEmitterChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySoundEmitterChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySoundEmitterChange ) } ) ] } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Distance" } ) , _jsxs ( "div" , { className : "vector-inputs vector-inputs--two" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Ref Distance" } ) , _jsx ( "input" , { "data-testid" : "sound-emitter-ref-distance" , className : "text-input" , type : "number" , min : "0.1" , step : "0.1" , value : soundEmitterRefDistanceDraft , onChange : ( event ) => setSoundEmitterRefDistanceDraft ( event . currentTarget . value ) , onBlur : ( ) => applySoundEmitterChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySoundEmitterChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySoundEmitterChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySoundEmitterChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Max Distance" } ) , _jsx ( "input" , { "data-testid" : "sound-emitter-max-distance" , className : "text-input" , type : "number" , min : "0.1" , step : "0.1" , value : soundEmitterMaxDistanceDraft , onChange : ( event ) => setSoundEmitterMaxDistanceDraft ( event . currentTarget . value ) , onBlur : ( ) => applySoundEmitterChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySoundEmitterChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySoundEmitterChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySoundEmitterChange ) } ) ] } ) ] } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Playback" } ) , _jsxs ( "div" , { className : "vector-inputs vector-inputs--two" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Autoplay" } ) , _jsx ( "input" , { "data-testid" : "sound-emitter-autoplay" , type : "checkbox" , checked : soundEmitterAutoplayDraft , onChange : ( event ) => {
const nextAutoplay = event . currentTarget . checked ;
setSoundEmitterAutoplayDraft ( nextAutoplay ) ;
scheduleDraftCommit ( ( ) => applySoundEmitterChange ( { autoplay : nextAutoplay } ) ) ;
} } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Loop" } ) , _jsx ( "input" , { "data-testid" : "sound-emitter-loop" , type : "checkbox" , checked : soundEmitterLoopDraft , onChange : ( event ) => {
const nextLoop = event . currentTarget . checked ;
setSoundEmitterLoopDraft ( nextLoop ) ;
scheduleDraftCommit ( ( ) => applySoundEmitterChange ( { loop : nextLoop } ) ) ;
} } ) ] } ) ] } ) ] } ) ] } ) ) : null , selectedTriggerVolume !== null ? ( _jsxs ( _Fragment , { children : [ _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Size" } ) , _jsxs ( "div" , { className : "vector-inputs" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "X" } ) , _jsx ( "input" , { "data-testid" : "trigger-volume-size-x" , className : "text-input" , type : "number" , min : DEFAULT _GRID _SIZE , step : DEFAULT _GRID _SIZE , value : triggerVolumeSizeDraft . x , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setTriggerVolumeSizeDraft ( ( draft ) => ( { ... draft , x : nextValue } ) ) ;
} , onBlur : applyTriggerVolumeChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyTriggerVolumeChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyTriggerVolumeChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyTriggerVolumeChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Y" } ) , _jsx ( "input" , { "data-testid" : "trigger-volume-size-y" , className : "text-input" , type : "number" , min : DEFAULT _GRID _SIZE , step : DEFAULT _GRID _SIZE , value : triggerVolumeSizeDraft . y , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setTriggerVolumeSizeDraft ( ( draft ) => ( { ... draft , y : nextValue } ) ) ;
} , onBlur : applyTriggerVolumeChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyTriggerVolumeChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyTriggerVolumeChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyTriggerVolumeChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Z" } ) , _jsx ( "input" , { "data-testid" : "trigger-volume-size-z" , className : "text-input" , type : "number" , min : DEFAULT _GRID _SIZE , step : DEFAULT _GRID _SIZE , value : triggerVolumeSizeDraft . z , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setTriggerVolumeSizeDraft ( ( draft ) => ( { ... draft , z : nextValue } ) ) ;
} , onBlur : applyTriggerVolumeChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyTriggerVolumeChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyTriggerVolumeChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyTriggerVolumeChange ) } ) ] } ) ] } ) ] } ) , renderInteractionLinksSection ( selectedTriggerVolume , selectedTriggerVolumeLinks , "add-trigger-teleport-link" , "add-trigger-visibility-link" , "add-trigger-play-sound-link" , "add-trigger-stop-sound-link" ) ] } ) ) : null , selectedTeleportTarget !== null ? ( _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Yaw" } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Degrees" } ) , _jsx ( "input" , { "data-testid" : "teleport-target-yaw" , className : "text-input" , type : "number" , step : "1" , value : teleportTargetYawDraft , onChange : ( event ) => setTeleportTargetYawDraft ( event . currentTarget . value ) , onBlur : applyTeleportTargetChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyTeleportTargetChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyTeleportTargetChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyTeleportTargetChange ) } ) ] } ) ] } ) ) : null , selectedInteractable !== null ? ( _jsxs ( _Fragment , { children : [ _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Interaction" } ) , _jsxs ( "div" , { className : "vector-inputs vector-inputs--two" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Range" } ) , _jsx ( "input" , { "data-testid" : "interactable-radius" , className : "text-input" , type : "number" , min : "0.1" , step : "0.1" , value : interactableRadiusDraft , onChange : ( event ) => setInteractableRadiusDraft ( event . currentTarget . value ) , onBlur : ( ) => applyInteractableChange ( ) , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyInteractableChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyInteractableChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyInteractableChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Enabled" } ) , _jsx ( "input" , { "data-testid" : "interactable-enabled" , type : "checkbox" , checked : interactableEnabledDraft , onChange : ( event ) => {
const nextEnabled = event . currentTarget . checked ;
setInteractableEnabledDraft ( nextEnabled ) ;
scheduleDraftCommit ( ( ) => applyInteractableChange ( { enabled : nextEnabled } ) ) ;
} } ) ] } ) ] } ) , _jsx ( "div" , { className : "material-summary" , children : "Range defines how close the player must be before the click prompt can activate." } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Prompt" } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Text" } ) , _jsx ( "input" , { "data-testid" : "interactable-prompt" , className : "text-input" , type : "text" , value : interactablePromptDraft , onChange : ( event ) => setInteractablePromptDraft ( event . currentTarget . value ) , onBlur : ( ) => applyInteractableChange ( ) , onKeyDown : ( event ) => {
if ( event . key === "Enter" ) {
applyInteractableChange ( ) ;
}
} } ) ] } ) ] } ) , renderInteractionLinksSection ( selectedInteractable , selectedInteractableLinks , "add-interactable-teleport-link" , "add-interactable-visibility-link" , "add-interactable-play-sound-link" , "add-interactable-stop-sound-link" ) ] } ) ) : null ] } ) ) : selectedBrush === null ? ( _jsx ( "div" , { className : "outliner-empty" , children : "Select a whitebox solid or entity to edit authored properties." } ) ) : ( _jsxs ( _Fragment , { children : [ _jsxs ( "div" , { className : "stat-card" , children : [ _jsx ( "div" , { className : "label" , children : "Whitebox Solid Type" } ) , _jsx ( "div" , { className : "value" , children : "box" } ) ] } ) , _jsxs ( "div" , { className : "stat-card" , children : [ _jsx ( "div" , { className : "label" , children : "Selection Mode" } ) , _jsx ( "div" , { className : "value" , children : getWhiteboxSelectionModeLabel ( whiteboxSelectionMode ) } ) ] } ) , whiteboxSelectionMode !== "object" ? ( _jsx ( "div" , { className : "outliner-empty" , children : whiteboxSelectionMode === "face"
? "Face mode keeps whole-solid transforms out of the way. Select a face to edit its material or UV transform."
: whiteboxSelectionMode === "edge"
? "Edge mode is selection-only in this slice. Edge transforms land next."
: "Vertex mode is selection-only in this slice. Vertex transforms land next." } ) ) : ( _jsxs ( _Fragment , { children : [ _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Center" } ) , _jsxs ( "div" , { className : "vector-inputs" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "X" } ) , _jsx ( "input" , { "data-testid" : "brush-center-x" , className : "text-input" , type : "number" , step : whiteboxVectorInputStep , value : positionDraft . x , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setPositionDraft ( ( draft ) => ( { ... draft , x : nextValue } ) ) ;
} , onBlur : applyPositionChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyPositionChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyPositionChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyPositionChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Y" } ) , _jsx ( "input" , { "data-testid" : "brush-center-y" , className : "text-input" , type : "number" , step : whiteboxVectorInputStep , value : positionDraft . y , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setPositionDraft ( ( draft ) => ( { ... draft , y : nextValue } ) ) ;
} , onBlur : applyPositionChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyPositionChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyPositionChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyPositionChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Z" } ) , _jsx ( "input" , { "data-testid" : "brush-center-z" , className : "text-input" , type : "number" , step : whiteboxVectorInputStep , value : positionDraft . z , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setPositionDraft ( ( draft ) => ( { ... draft , z : nextValue } ) ) ;
} , onBlur : applyPositionChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyPositionChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyPositionChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyPositionChange ) } ) ] } ) ] } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Rotation" } ) , _jsxs ( "div" , { className : "vector-inputs" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "X" } ) , _jsx ( "input" , { "data-testid" : "brush-rotation-x" , className : "text-input" , type : "number" , step : "0.1" , value : rotationDraft . x , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setRotationDraft ( ( draft ) => ( { ... draft , x : nextValue } ) ) ;
} , onBlur : applyRotationChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyRotationChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyRotationChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyRotationChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Y" } ) , _jsx ( "input" , { "data-testid" : "brush-rotation-y" , className : "text-input" , type : "number" , step : "0.1" , value : rotationDraft . y , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setRotationDraft ( ( draft ) => ( { ... draft , y : nextValue } ) ) ;
} , onBlur : applyRotationChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyRotationChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyRotationChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyRotationChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Z" } ) , _jsx ( "input" , { "data-testid" : "brush-rotation-z" , className : "text-input" , type : "number" , step : "0.1" , value : rotationDraft . z , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setRotationDraft ( ( draft ) => ( { ... draft , z : nextValue } ) ) ;
} , onBlur : applyRotationChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applyRotationChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applyRotationChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applyRotationChange ) } ) ] } ) ] } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Size" } ) , _jsxs ( "div" , { className : "vector-inputs" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "X" } ) , _jsx ( "input" , { "data-testid" : "brush-size-x" , className : "text-input" , type : "number" , min : "0.01" , step : whiteboxVectorInputStep , value : sizeDraft . x , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setSizeDraft ( ( draft ) => ( { ... draft , x : nextValue } ) ) ;
} , onBlur : applySizeChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySizeChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySizeChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySizeChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Y" } ) , _jsx ( "input" , { "data-testid" : "brush-size-y" , className : "text-input" , type : "number" , min : "0.01" , step : whiteboxVectorInputStep , value : sizeDraft . y , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setSizeDraft ( ( draft ) => ( { ... draft , y : nextValue } ) ) ;
} , onBlur : applySizeChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySizeChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySizeChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySizeChange ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "Z" } ) , _jsx ( "input" , { "data-testid" : "brush-size-z" , className : "text-input" , type : "number" , min : "0.01" , step : whiteboxVectorInputStep , value : sizeDraft . z , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setSizeDraft ( ( draft ) => ( { ... draft , z : nextValue } ) ) ;
} , onBlur : applySizeChange , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , applySizeChange ) , onKeyUp : ( event ) => handleNumberInputKeyUp ( event , applySizeChange ) , onPointerUp : ( event ) => handleNumberInputPointerUp ( event , applySizeChange ) } ) ] } ) ] } ) ] } ) ] } ) ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Faces" } ) , _jsx ( "div" , { className : "face-grid" , children : BOX _FACE _IDS . map ( ( faceId ) => ( _jsxs ( "button" , { type : "button" , "data-testid" : ` face-button- ${ faceId } ` , className : ` face-chip ${ isBrushFaceSelected ( editorState . selection , selectedBrush . id , faceId ) ? "face-chip--active" : "" } ` , onClick : ( ) => {
store . setWhiteboxSelectionMode ( "face" ) ;
applySelection ( {
kind : "brushFace" ,
brushId : selectedBrush . id ,
faceId
} , "inspector" ) ;
} , children : [ _jsx ( "span" , { className : "face-chip__title" , children : BOX _FACE _LABELS [ faceId ] } ) , _jsx ( "span" , { className : "face-chip__meta" , children : faceId } ) ] } , faceId ) ) ) } ) ] } ) , whiteboxSelectionMode === "edge" ? ( selectedEdgeId === null ? ( _jsx ( "div" , { className : "outliner-empty" , children : "Select an edge in the viewport to inspect it. Edge transforms land in the next slice." } ) ) : ( _jsxs ( "div" , { className : "stat-card" , children : [ _jsx ( "div" , { className : "label" , children : "Active Edge" } ) , _jsx ( "div" , { className : "value" , children : BOX _EDGE _LABELS [ selectedEdgeId ] } ) , _jsx ( "div" , { className : "material-summary" , children : "Edge selection is visible in the viewport. Persistent edge transforms are still deferred." } ) ] } ) ) ) : whiteboxSelectionMode === "vertex" ? ( selectedVertexId === null ? ( _jsx ( "div" , { className : "outliner-empty" , children : "Select a vertex in the viewport to inspect it. Vertex transforms land in the next slice." } ) ) : ( _jsxs ( "div" , { className : "stat-card" , children : [ _jsx ( "div" , { className : "label" , children : "Active Vertex" } ) , _jsx ( "div" , { className : "value" , children : BOX _VERTEX _LABELS [ selectedVertexId ] } ) , _jsx ( "div" , { className : "material-summary" , children : "Vertex selection is visible in the viewport. Persistent vertex transforms are still deferred." } ) ] } ) ) ) : whiteboxSelectionMode !== "face" ? ( _jsx ( "div" , { className : "outliner-empty" , children : "Switch to Face mode or choose a face chip to edit materials and UVs." } ) ) : selectedFace === null || selectedFaceId === null ? ( _jsx ( "div" , { className : "outliner-empty" , children : "Select a face to edit its material and UV transform." } ) ) : ( _jsxs ( _Fragment , { children : [ _jsxs ( "div" , { className : "stat-card" , children : [ _jsx ( "div" , { className : "label" , children : "Active Face" } ) , _jsx ( "div" , { className : "value" , children : BOX _FACE _LABELS [ selectedFaceId ] } ) , _jsxs ( "div" , { className : "material-summary" , "data-testid" : "selected-face-material-name" , children : [ "Material: " , selectedFaceMaterial ? . name ? ? "Fallback face color" ] } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "Material" } ) , _jsx ( "div" , { className : "material-browser" , children : materialList . map ( ( material ) => ( _jsxs ( "button" , { type : "button" , "data-testid" : ` material-button- ${ material . id } ` , className : ` material-item ${ selectedFace . materialId === material . id ? "material-item--active" : "" } ` , onClick : ( ) => applyFaceMaterial ( material . id ) , children : [ _jsx ( "span" , { className : "material-item__preview" , style : getMaterialPreviewStyle ( material ) , "aria-hidden" : "true" } ) , _jsxs ( "span" , { className : "material-item__text" , children : [ _jsx ( "span" , { className : "material-item__title" , children : material . name } ) , _jsx ( "span" , { className : "material-item__meta" , children : material . tags . join ( " | " ) } ) ] } ) ] } , material . id ) ) ) } ) , _jsx ( "div" , { className : "inline-actions" , children : _jsx ( "button" , { className : "toolbar__button" , type : "button" , onClick : clearFaceMaterial , children : "Clear Material" } ) } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "UV Offset" } ) , _jsxs ( "div" , { className : "vector-inputs vector-inputs--two" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "U" } ) , _jsx ( "input" , { "data-testid" : "face-uv-offset-x" , className : "text-input" , type : "number" , step : "0.125" , value : uvOffsetDraft . x , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setUvOffsetDraft ( ( draft ) => ( { ... draft , x : nextValue } ) ) ;
} , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , handleApplyUvDraft ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "V" } ) , _jsx ( "input" , { "data-testid" : "face-uv-offset-y" , className : "text-input" , type : "number" , step : "0.125" , value : uvOffsetDraft . y , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setUvOffsetDraft ( ( draft ) => ( { ... draft , y : nextValue } ) ) ;
} , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , handleApplyUvDraft ) } ) ] } ) ] } ) ] } ) , _jsxs ( "div" , { className : "form-section" , children : [ _jsx ( "div" , { className : "label" , children : "UV Scale" } ) , _jsxs ( "div" , { className : "vector-inputs vector-inputs--two" , children : [ _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "U" } ) , _jsx ( "input" , { "data-testid" : "face-uv-scale-x" , className : "text-input" , type : "number" , min : "0.001" , step : "0.125" , value : uvScaleDraft . x , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setUvScaleDraft ( ( draft ) => ( { ... draft , x : nextValue } ) ) ;
} , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , handleApplyUvDraft ) } ) ] } ) , _jsxs ( "label" , { className : "form-field" , children : [ _jsx ( "span" , { className : "label" , children : "V" } ) , _jsx ( "input" , { "data-testid" : "face-uv-scale-y" , className : "text-input" , type : "number" , min : "0.001" , step : "0.125" , value : uvScaleDraft . y , onChange : ( event ) => {
const nextValue = event . currentTarget . value ;
setUvScaleDraft ( ( draft ) => ( { ... draft , y : nextValue } ) ) ;
} , onKeyDown : ( event ) => handleDraftVectorKeyDown ( event , handleApplyUvDraft ) } ) ] } ) ] } ) ] } ) , _jsxs ( "div" , { className : "inline-actions" , children : [ _jsx ( "button" , { className : "toolbar__button" , type : "button" , "data-testid" : "apply-face-uv" , onClick : handleApplyUvDraft , children : "Apply UV Offset/Scale" } ) , _jsx ( "button" , { className : "toolbar__button" , type : "button" , onClick : handleRotateUv , children : "Rotate 90" } ) , _jsx ( "button" , { className : "toolbar__button" , type : "button" , onClick : ( ) => handleFlipUv ( "u" ) , children : "Flip U" } ) , _jsx ( "button" , { className : "toolbar__button" , type : "button" , onClick : ( ) => handleFlipUv ( "v" ) , children : "Flip V" } ) , _jsx ( "button" , { className : "toolbar__button" , type : "button" , onClick : handleFitUvToFace , children : "Fit To Face" } ) ] } ) , _jsxs ( "div" , { className : "stat-card" , children : [ _jsx ( "div" , { className : "label" , children : "UV Flags" } ) , _jsxs ( "div" , { className : "value" , children : [ "Rotation " , selectedFace . uv . rotationQuarterTurns * 90 , "\u00B0" ] } ) , _jsxs ( "div" , { className : "material-summary" , children : [ "U " , selectedFace . uv . flipU ? "flipped" : "normal" , " \u00B7 V " , selectedFace . uv . flipV ? "flipped" : "normal" ] } ) ] } ) ] } ) ) ] } ) ) ] } ) ) } ) ] } ) , addMenuPosition === null ? null : ( _jsx ( HierarchicalMenu , { title : "Add" , position : addMenuPosition , items : addMenuItems , onClose : closeAddMenu } ) ) , _jsxs ( "footer" , { className : "status-bar" , children : [ _jsxs ( "div" , { className : "status-bar__item" , "data-testid" : "status-message" , children : [ _jsx ( "span" , { className : "status-bar__strong" , children : "Status:" } ) , " " , statusMessage ] } ) , _jsxs ( "div" , { className : "status-bar__item" , "data-testid" : "status-whitebox-selection-mode" , children : [ _jsx ( "span" , { className : "status-bar__strong" , children : "Whitebox:" } ) , " " , getWhiteboxSelectionModeLabel ( whiteboxSelectionMode ) ] } ) , _jsxs ( "div" , { className : "status-bar__item" , "data-testid" : "status-document" , children : [ _jsx ( "span" , { className : "status-bar__strong" , children : "Document:" } ) , " " , documentStatusLabel ] } ) , _jsxs ( "div" , { className : "status-bar__item" , "data-testid" : "status-run-preflight" , children : [ _jsx ( "span" , { className : "status-bar__strong" , children : "Run:" } ) , " " , runReadyLabel ] } ) , _jsxs ( "div" , { className : "status-bar__item" , "data-testid" : "status-warnings" , children : [ _jsx ( "span" , { className : "status-bar__strong" , children : "Warnings:" } ) , " " , warningDiagnostics . length ] } ) , hoveredAssetStatusMessage === null ? null : ( _jsxs ( "div" , { className : "status-bar__item status-bar__item--asset" , "data-testid" : "status-asset-hover" , children : [ _jsx ( "span" , { className : "status-bar__strong" , children : "Asset:" } ) , " " , hoveredAssetStatusMessage ] } ) ) , _jsxs ( "div" , { className : "status-bar__item" , "data-testid" : "status-last-command" , children : [ _jsx ( "span" , { className : "status-bar__strong" , children : "Last:" } ) , " " , lastCommandLabel ] } ) ] } ) , _jsx ( "input" , { ref : importInputRef , className : "visually-hidden" , type : "file" , accept : ".json,application/json" , onChange : handleImportJsonChange } ) , _jsx ( "input" , { ref : importModelInputRef , className : "visually-hidden" , type : "file" , multiple : true , accept : ".glb,.gltf,model/gltf-binary,model/gltf+json,application/octet-stream" , onChange : handleImportModelChange } ) , _jsx ( "input" , { ref : importBackgroundImageInputRef , className : "visually-hidden" , type : "file" , accept : ".avif,.exr,.gif,.hdr,.jpg,.jpeg,.png,.svg,.webp,image/*" , onChange : handleImportBackgroundImageChange } ) , _jsx ( "input" , { ref : importAudioInputRef , className : "visually-hidden" , type : "file" , accept : ".aac,.flac,.m4a,.mp3,.oga,.ogg,.wav,.webm,audio/*" , onChange : handleImportAudioChange } ) ] } ) ) ;
}