From 4a66cccd79f8e983af714c164d716f1e7c4d1098 Mon Sep 17 00:00:00 2001 From: Victor Giers Date: Sat, 11 Apr 2026 02:44:48 +0200 Subject: [PATCH] auto-git: [change] src/app/App.tsx [change] src/app/editor-store.ts [change] src/core/transform-session.ts [change] src/viewport-three/ViewportCanvas.tsx [change] src/viewport-three/ViewportPanel.tsx [change] src/viewport-three/viewport-host.ts [change] tests/unit/viewport-canvas.test.tsx --- src/app/App.tsx | 8283 +++++++++++------ src/app/editor-store.ts | 107 +- src/core/transform-session.ts | 293 +- src/viewport-three/ViewportCanvas.tsx | 101 +- src/viewport-three/ViewportPanel.tsx | 73 +- src/viewport-three/viewport-host.ts | 1890 +++- .../domain/transform-session.command.test.ts | 175 +- .../transform-foundation.integration.test.tsx | 172 +- tests/unit/viewport-canvas.test.tsx | 55 +- 9 files changed, 7825 insertions(+), 3324 deletions(-) diff --git a/src/app/App.tsx b/src/app/App.tsx index 931b9dd6..921d0e56 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -97,7 +97,12 @@ import { type ImportedImageAssetResult, type LoadedImageAsset } from "../assets/image-assets"; -import type { AudioAssetRecord, ImageAssetRecord, ModelAssetRecord, ProjectAssetRecord } from "../assets/project-assets"; +import type { + AudioAssetRecord, + ImageAssetRecord, + ModelAssetRecord, + ProjectAssetRecord +} from "../assets/project-assets"; import { getProjectAssetKindLabel } from "../assets/project-assets"; import { getWhiteboxSelectionModeLabel, @@ -140,9 +145,19 @@ import { type AdvancedRenderingToneMappingMode, type WorldSettings } from "../document/world-settings"; -import { formatSceneDiagnosticSummary, validateSceneDocument } from "../document/scene-document-validation"; -import { getBrowserProjectAssetStorageAccess, type ProjectAssetStorage } from "../assets/project-asset-storage"; -import { DEFAULT_GRID_SIZE, snapPositiveSizeToGrid, snapVec3ToGrid } from "../geometry/grid-snapping"; +import { + formatSceneDiagnosticSummary, + validateSceneDocument +} from "../document/scene-document-validation"; +import { + getBrowserProjectAssetStorageAccess, + type ProjectAssetStorage +} 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, @@ -185,7 +200,10 @@ import { type EntityInstance, type EntityKind } from "../entities/entity-instances"; -import { getEntityDisplayLabelById, getSortedEntityDisplayLabels } from "../entities/entity-labels"; +import { + getEntityDisplayLabelById, + getSortedEntityDisplayLabels +} from "../entities/entity-labels"; import { areInteractionLinksEqual, createPlayAnimationInteractionLink, @@ -198,11 +216,18 @@ import { type InteractionLink, type InteractionTriggerKind } from "../interactions/interaction-links"; -import { STARTER_MATERIAL_LIBRARY, type MaterialDef } from "../materials/starter-material-library"; +import { + STARTER_MATERIAL_LIBRARY, + type MaterialDef +} from "../materials/starter-material-library"; import { RunnerCanvas } from "../runner-web/RunnerCanvas"; import type { FirstPersonTelemetry } from "../runtime-three/navigation-controller"; import type { RuntimeInteractionPrompt } from "../runtime-three/runtime-interaction-system"; -import { buildRuntimeSceneFromDocument, type RuntimeNavigationMode, type RuntimeSceneDefinition } from "../runtime-three/runtime-scene-build"; +import { + buildRuntimeSceneFromDocument, + type RuntimeNavigationMode, + type RuntimeSceneDefinition +} from "../runtime-three/runtime-scene-build"; import { validateRuntimeSceneBuild } from "../runtime-three/runtime-scene-validation"; import { EditorAutosaveController } from "../serialization/editor-autosave"; import { Panel } from "../shared-ui/Panel"; @@ -211,11 +236,18 @@ import { PROJECT_PACKAGE_FILE_EXTENSION, saveProjectPackage } from "../serialization/project-package"; -import { HierarchicalMenu, type HierarchicalMenuItem, type HierarchicalMenuPosition } from "../shared-ui/HierarchicalMenu"; +import { + HierarchicalMenu, + type HierarchicalMenuItem, + type HierarchicalMenuPosition +} from "../shared-ui/HierarchicalMenu"; import { createWorldBackgroundStyle } from "../shared-ui/world-background-style"; import { ViewportPanel } from "../viewport-three/ViewportPanel"; import type { CreationViewportToolPreview } from "../viewport-three/viewport-transient-state"; -import { getViewportViewModeLabel, type ViewportViewMode } from "../viewport-three/viewport-view-modes"; +import { + getViewportViewModeLabel, + type ViewportViewMode +} from "../viewport-three/viewport-view-modes"; import { VIEWPORT_LAYOUT_MODES, VIEWPORT_PANEL_IDS, @@ -246,9 +278,14 @@ interface Vec3Draft { z: string; } -type InteractionSourceEntity = Extract; +type InteractionSourceEntity = Extract< + EntityInstance, + { kind: "triggerVolume" | "interactable" } +>; -function getModelInstanceCollisionModeDescription(mode: ModelInstanceCollisionMode): string { +function getModelInstanceCollisionModeDescription( + mode: ModelInstanceCollisionMode +): string { switch (mode) { case "none": return "No generated collider is built for this model instance."; @@ -263,7 +300,9 @@ function getModelInstanceCollisionModeDescription(mode: ModelInstanceCollisionMo } } -function getPlayerStartColliderModeDescription(mode: PlayerStartColliderMode): string { +function getPlayerStartColliderModeDescription( + mode: PlayerStartColliderMode +): string { switch (mode) { case "capsule": return "Uses a capsule player collider for standard grounded first-person traversal."; @@ -274,7 +313,9 @@ function getPlayerStartColliderModeDescription(mode: PlayerStartColliderMode): s } } -const STARTER_MATERIAL_ORDER = new Map(STARTER_MATERIAL_LIBRARY.map((material, index) => [material.id, index])); +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; @@ -285,9 +326,14 @@ function formatVec3(vector: Vec3): string { return `${vector.x}, ${vector.y}, ${vector.z}`; } -function resolveOptionalPositiveNumber(value: string, fallback: number): number { +function resolveOptionalPositiveNumber( + value: string, + fallback: number +): number { const parsedValue = Number(value); - return Number.isFinite(parsedValue) && parsedValue > 0 ? parsedValue : fallback; + return Number.isFinite(parsedValue) && parsedValue > 0 + ? parsedValue + : fallback; } function getWhiteboxInputStep(enabled: boolean, step: number): NumberInputStep { @@ -299,17 +345,24 @@ function formatDiagnosticCount(count: number, label: string): string { } function clampViewportQuadSplitValue(value: number): number { - return Math.min(MAX_VIEWPORT_QUAD_SPLIT, Math.max(MIN_VIEWPORT_QUAD_SPLIT, value)); + return Math.min( + MAX_VIEWPORT_QUAD_SPLIT, + Math.max(MIN_VIEWPORT_QUAD_SPLIT, value) + ); } -function createViewportQuadPanelsStyle(viewportQuadSplit: ViewportQuadSplit): CSSProperties { +function createViewportQuadPanelsStyle( + viewportQuadSplit: ViewportQuadSplit +): CSSProperties { return { "--viewport-quad-split-x": String(viewportQuadSplit.x), "--viewport-quad-split-y": String(viewportQuadSplit.y) } as CSSProperties; } -function getViewportQuadResizeCursor(resizeMode: ViewportQuadResizeMode): string { +function getViewportQuadResizeCursor( + resizeMode: ViewportQuadResizeMode +): string { switch (resizeMode) { case "vertical": return "col-resize"; @@ -375,7 +428,11 @@ function readVec3Draft(draft: Vec3Draft, label: string): Vec3 { z: Number(draft.z) }; - if (!Number.isFinite(vector.x) || !Number.isFinite(vector.y) || !Number.isFinite(vector.z)) { + if ( + !Number.isFinite(vector.x) || + !Number.isFinite(vector.y) || + !Number.isFinite(vector.z) + ) { throw new Error(`${label} values must be finite numbers.`); } @@ -400,7 +457,9 @@ function readNonNegativeNumberDraft(source: string, label: string): number { 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.`); + throw new Error( + `${label} must be a finite number greater than or equal to zero.` + ); } return value; @@ -430,7 +489,9 @@ function readWaterFoamContactLimitDraft(source: string): number { const value = readPositiveIntegerDraft(source, "Water foam contact limit"); if (value > MAX_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT) { - throw new Error(`Water foam contact limit must be ${MAX_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT} or less.`); + throw new Error( + `Water foam contact limit must be ${MAX_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT} or less.` + ); } return value; @@ -466,7 +527,11 @@ function maybeSnapVec3(vector: Vec3, enabled: boolean, step: number): Vec3 { }; } -function maybeSnapPositiveSize(size: Vec3, enabled: boolean, step: number): Vec3 { +function maybeSnapPositiveSize( + size: Vec3, + enabled: boolean, + step: number +): Vec3 { const clampComponent = (value: number) => Math.max(0.01, Math.abs(value)); if (!enabled) { @@ -494,7 +559,10 @@ function areFaceUvStatesEqual(left: FaceUvState, right: FaceUvState): boolean { ); } -function getSelectedBoxBrush(selection: EditorSelection, brushes: BoxBrush[]): BoxBrush | null { +function getSelectedBoxBrush( + selection: EditorSelection, + brushes: BoxBrush[] +): BoxBrush | null { const selectedBrushId = getSingleSelectedBrushId(selection); if (selectedBrushId === null) { @@ -504,7 +572,10 @@ function getSelectedBoxBrush(selection: EditorSelection, brushes: BoxBrush[]): B return brushes.find((brush) => brush.id === selectedBrushId) ?? null; } -function getSelectedEntity(selection: EditorSelection, entities: EntityInstance[]): EntityInstance | null { +function getSelectedEntity( + selection: EditorSelection, + entities: EntityInstance[] +): EntityInstance | null { const selectedEntityId = getSingleSelectedEntityId(selection); if (selectedEntityId === null) { @@ -514,14 +585,21 @@ function getSelectedEntity(selection: EditorSelection, entities: EntityInstance[ return entities.find((entity) => entity.id === selectedEntityId) ?? null; } -function getSelectedModelInstance(selection: EditorSelection, modelInstances: ModelInstance[]): ModelInstance | null { +function getSelectedModelInstance( + selection: EditorSelection, + modelInstances: ModelInstance[] +): ModelInstance | null { const selectedModelInstanceId = getSingleSelectedModelInstanceId(selection); if (selectedModelInstanceId === null) { return null; } - return modelInstances.find((modelInstance) => modelInstance.id === selectedModelInstanceId) ?? null; + return ( + modelInstances.find( + (modelInstance) => modelInstance.id === selectedModelInstanceId + ) ?? null + ); } function isModelAsset(asset: ProjectAssetRecord): asset is ModelAssetRecord { @@ -570,7 +648,9 @@ function formatModelAssetSummary(asset: ModelAssetRecord): string { ]; if (asset.metadata.animationNames.length > 0) { - details.push(`${asset.metadata.animationNames.length} animation${asset.metadata.animationNames.length === 1 ? "" : "s"}`); + details.push( + `${asset.metadata.animationNames.length} animation${asset.metadata.animationNames.length === 1 ? "" : "s"}` + ); } return details.join(" | "); @@ -588,9 +668,15 @@ function formatImageAssetSummary(asset: ImageAssetRecord): string { function formatAudioAssetSummary(asset: AudioAssetRecord): string { 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`, + 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) ]; @@ -626,10 +712,15 @@ function getBrushLabel(brush: BoxBrush, index: number): string { function getBrushLabelById(brushId: string, brushes: BoxBrush[]): string { const brushIndex = brushes.findIndex((brush) => brush.id === brushId); - return brushIndex === -1 ? "Whitebox Box" : getBrushLabel(brushes[brushIndex], brushIndex); + return brushIndex === -1 + ? "Whitebox Box" + : getBrushLabel(brushes[brushIndex], brushIndex); } -function getSelectedBrushLabel(selection: EditorSelection, brushes: BoxBrush[]): string { +function getSelectedBrushLabel( + selection: EditorSelection, + brushes: BoxBrush[] +): string { const selectedBrushId = getSingleSelectedBrushId(selection); if (selectedBrushId === null) { @@ -707,7 +798,9 @@ function getInteractionActionLabel(link: InteractionLink): string { } } -function getVisibilityModeSelectValue(visible: boolean | undefined): "toggle" | "show" | "hide" { +function getVisibilityModeSelectValue( + visible: boolean | undefined +): "toggle" | "show" | "hide" { if (visible === true) { return "show"; } @@ -719,7 +812,9 @@ function getVisibilityModeSelectValue(visible: boolean | undefined): "toggle" | return "toggle"; } -function readVisibilityModeSelectValue(value: "toggle" | "show" | "hide"): boolean | undefined { +function readVisibilityModeSelectValue( + value: "toggle" | "show" | "hide" +): boolean | undefined { switch (value) { case "toggle": return undefined; @@ -730,7 +825,10 @@ function readVisibilityModeSelectValue(value: "toggle" | "show" | "hide"): boole } } -function getDefaultTriggerVolumeLinkTrigger(triggerOnEnter: boolean, triggerOnExit: boolean): InteractionTriggerKind { +function getDefaultTriggerVolumeLinkTrigger( + triggerOnEnter: boolean, + triggerOnExit: boolean +): InteractionTriggerKind { if (triggerOnEnter) { return "enter"; } @@ -742,17 +840,29 @@ function getDefaultTriggerVolumeLinkTrigger(triggerOnEnter: boolean, triggerOnEx return "enter"; } -function isInteractionSourceEntity(entity: EntityInstance | null): entity is InteractionSourceEntity { - return entity !== null && (entity.kind === "triggerVolume" || entity.kind === "interactable"); +function isInteractionSourceEntity( + entity: EntityInstance | null +): entity is InteractionSourceEntity { + return ( + entity !== null && + (entity.kind === "triggerVolume" || entity.kind === "interactable") + ); } -function isSoundEmitterEntity(entity: EntityInstance | null): entity is Extract { +function isSoundEmitterEntity( + entity: EntityInstance | null +): entity is Extract { return entity !== null && entity.kind === "soundEmitter"; } -function getDefaultInteractionLinkTrigger(sourceEntity: InteractionSourceEntity): InteractionTriggerKind { +function getDefaultInteractionLinkTrigger( + sourceEntity: InteractionSourceEntity +): InteractionTriggerKind { return sourceEntity.kind === "triggerVolume" - ? getDefaultTriggerVolumeLinkTrigger(sourceEntity.triggerOnEnter, sourceEntity.triggerOnExit) + ? getDefaultTriggerVolumeLinkTrigger( + sourceEntity.triggerOnEnter, + sourceEntity.triggerOnExit + ) : "click"; } @@ -793,23 +903,35 @@ function selectionCanBeDuplicated(selection: EditorSelection): boolean { } function isCommitIncrementKey(key: string): boolean { - return key === "ArrowUp" || key === "ArrowDown" || key === "PageUp" || key === "PageDown"; + return ( + key === "ArrowUp" || + key === "ArrowDown" || + key === "PageUp" || + key === "PageDown" + ); } function blurActiveTextEntry() { const activeElement = document.activeElement; - if (!(activeElement instanceof HTMLElement) || !isTextEntryTarget(activeElement)) { + if ( + !(activeElement instanceof HTMLElement) || + !isTextEntryTarget(activeElement) + ) { return; } activeElement.blur(); } -function sortDocumentMaterials(materials: Record): MaterialDef[] { +function sortDocumentMaterials( + materials: Record +): MaterialDef[] { 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; + 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; @@ -848,7 +970,9 @@ function getMaterialPreviewStyle(material: MaterialDef): CSSProperties { } } -function rotateQuarterTurns(rotationQuarterTurns: FaceUvRotationQuarterTurns): FaceUvRotationQuarterTurns { +function rotateQuarterTurns( + rotationQuarterTurns: FaceUvRotationQuarterTurns +): FaceUvRotationQuarterTurns { return ((rotationQuarterTurns + 1) % 4) as FaceUvRotationQuarterTurns; } @@ -894,7 +1018,9 @@ function formatWorldBackgroundLabel(world: WorldSettings): string { return "Image"; } -function formatAdvancedRenderingShadowTypeLabel(type: AdvancedRenderingShadowType): string { +function formatAdvancedRenderingShadowTypeLabel( + type: AdvancedRenderingShadowType +): string { switch (type) { case "basic": return "Basic"; @@ -905,7 +1031,9 @@ function formatAdvancedRenderingShadowTypeLabel(type: AdvancedRenderingShadowTyp } } -function formatAdvancedRenderingToneMappingLabel(mode: AdvancedRenderingToneMappingMode): string { +function formatAdvancedRenderingToneMappingLabel( + mode: AdvancedRenderingToneMappingMode +): string { switch (mode) { case "none": return "None"; @@ -920,7 +1048,9 @@ function formatAdvancedRenderingToneMappingLabel(mode: AdvancedRenderingToneMapp } } -function formatAdvancedRenderingWaterReflectionModeLabel(mode: AdvancedRenderingWaterReflectionMode): string { +function formatAdvancedRenderingWaterReflectionModeLabel( + mode: AdvancedRenderingWaterReflectionMode +): string { switch (mode) { case "none": return "Nothing"; @@ -969,178 +1099,375 @@ export function App({ store, initialStatusMessage }: AppProps) { 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 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 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 selectedFace = + selectedBrush !== null && selectedFaceId !== null + ? selectedBrush.faces[selectedFaceId] + : null; const selectedFaceMaterial = - selectedFace !== null && selectedFace.materialId !== null ? editorState.document.materials[selectedFace.materialId] ?? null : null; + 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; + 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; + : (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; + 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 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); + : 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") as Array<{ + selectedInteractable === null + ? [] + : getInteractionLinksForSource( + editorState.document.interactionLinks, + selectedInteractable.id + ); + const teleportTargetOptions = entityDisplayList.filter( + ({ entity }) => entity.kind === "teleportTarget" + ); + const soundEmitterOptions = entityDisplayList.filter( + ({ entity }) => entity.kind === "soundEmitter" + ) as Array<{ entity: Extract; label: string; }>; - const playableSoundEmitterOptions = soundEmitterOptions.filter(({ entity }) => { - if (entity.audioAssetId === null) { - return false; - } + const playableSoundEmitterOptions = soundEmitterOptions.filter( + ({ entity }) => { + if (entity.audioAssetId === null) { + return false; + } - return editorState.document.assets[entity.audioAssetId]?.kind === "audio"; - }); + 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 [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 [boxVolumeModeDraft, setBoxVolumeModeDraft] = useState("none"); - const [boxVolumeWaterColorDraft, setBoxVolumeWaterColorDraft] = useState("#4da6d9"); - const [boxVolumeWaterSurfaceOpacityDraft, setBoxVolumeWaterSurfaceOpacityDraft] = useState("0.55"); - const [boxVolumeWaterWaveStrengthDraft, setBoxVolumeWaterWaveStrengthDraft] = useState("0.35"); - const [boxVolumeWaterFoamContactLimitDraft, setBoxVolumeWaterFoamContactLimitDraft] = useState( - String(DEFAULT_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT) + const [positionDraft, setPositionDraft] = useState( + createVec3Draft(DEFAULT_BOX_BRUSH_CENTER) ); - const [boxVolumeWaterSurfaceDisplacementEnabledDraft, setBoxVolumeWaterSurfaceDisplacementEnabledDraft] = useState(false); - const [boxVolumeFogColorDraft, setBoxVolumeFogColorDraft] = useState("#9cb7c7"); - const [boxVolumeFogDensityDraft, setBoxVolumeFogDensityDraft] = useState("0.08"); - const [boxVolumeFogPaddingDraft, setBoxVolumeFogPaddingDraft] = useState("0.2"); + const [rotationDraft, setRotationDraft] = useState( + createVec3Draft(DEFAULT_BOX_BRUSH_ROTATION_DEGREES) + ); + const [sizeDraft, setSizeDraft] = useState( + createVec3Draft(DEFAULT_BOX_BRUSH_SIZE) + ); + const [boxVolumeModeDraft, setBoxVolumeModeDraft] = + useState("none"); + const [boxVolumeWaterColorDraft, setBoxVolumeWaterColorDraft] = + useState("#4da6d9"); + const [ + boxVolumeWaterSurfaceOpacityDraft, + setBoxVolumeWaterSurfaceOpacityDraft + ] = useState("0.55"); + const [boxVolumeWaterWaveStrengthDraft, setBoxVolumeWaterWaveStrengthDraft] = + useState("0.35"); + const [ + boxVolumeWaterFoamContactLimitDraft, + setBoxVolumeWaterFoamContactLimitDraft + ] = useState(String(DEFAULT_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT)); + const [ + boxVolumeWaterSurfaceDisplacementEnabledDraft, + setBoxVolumeWaterSurfaceDisplacementEnabledDraft + ] = useState(false); + const [boxVolumeFogColorDraft, setBoxVolumeFogColorDraft] = + useState("#9cb7c7"); + const [boxVolumeFogDensityDraft, setBoxVolumeFogDensityDraft] = + useState("0.08"); + const [boxVolumeFogPaddingDraft, setBoxVolumeFogPaddingDraft] = + useState("0.2"); const [whiteboxSnapEnabled, setWhiteboxSnapEnabled] = useState(true); - const [whiteboxSnapStepDraft, setWhiteboxSnapStepDraft] = useState(String(DEFAULT_GRID_SIZE)); - const [viewportGridVisible, setViewportGridVisible] = useState(true); - 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 [whiteboxSnapStepDraft, setWhiteboxSnapStepDraft] = useState( + String(DEFAULT_GRID_SIZE) ); - const [advancedRenderingShadowBiasDraft, setAdvancedRenderingShadowBiasDraft] = useState( + const [viewportGridVisible, setViewportGridVisible] = useState(true); + 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 [ + advancedRenderingAmbientOcclusionIntensityDraft, + setAdvancedRenderingAmbientOcclusionIntensityDraft + ] = useState( + String( + editorState.document.world.advancedRendering.ambientOcclusion.intensity + ) ); - const [advancedRenderingAmbientOcclusionRadiusDraft, setAdvancedRenderingAmbientOcclusionRadiusDraft] = useState( + const [ + advancedRenderingAmbientOcclusionRadiusDraft, + setAdvancedRenderingAmbientOcclusionRadiusDraft + ] = useState( String(editorState.document.world.advancedRendering.ambientOcclusion.radius) ); - const [advancedRenderingAmbientOcclusionSamplesDraft, setAdvancedRenderingAmbientOcclusionSamplesDraft] = useState( - String(editorState.document.world.advancedRendering.ambientOcclusion.samples) + const [ + advancedRenderingAmbientOcclusionSamplesDraft, + setAdvancedRenderingAmbientOcclusionSamplesDraft + ] = useState( + String( + editorState.document.world.advancedRendering.ambientOcclusion.samples + ) ); - const [advancedRenderingBloomIntensityDraft, setAdvancedRenderingBloomIntensityDraft] = useState( + const [ + advancedRenderingBloomIntensityDraft, + setAdvancedRenderingBloomIntensityDraft + ] = useState( String(editorState.document.world.advancedRendering.bloom.intensity) ); - const [advancedRenderingBloomThresholdDraft, setAdvancedRenderingBloomThresholdDraft] = useState( + const [ + advancedRenderingBloomThresholdDraft, + setAdvancedRenderingBloomThresholdDraft + ] = useState( String(editorState.document.world.advancedRendering.bloom.threshold) ); - const [advancedRenderingBloomRadiusDraft, setAdvancedRenderingBloomRadiusDraft] = useState( + const [ + advancedRenderingBloomRadiusDraft, + setAdvancedRenderingBloomRadiusDraft + ] = useState( String(editorState.document.world.advancedRendering.bloom.radius) ); - const [advancedRenderingToneMappingExposureDraft, setAdvancedRenderingToneMappingExposureDraft] = useState( + const [ + advancedRenderingToneMappingExposureDraft, + setAdvancedRenderingToneMappingExposureDraft + ] = useState( String(editorState.document.world.advancedRendering.toneMapping.exposure) ); - const [advancedRenderingDepthOfFieldFocusDistanceDraft, setAdvancedRenderingDepthOfFieldFocusDistanceDraft] = useState( - String(editorState.document.world.advancedRendering.depthOfField.focusDistance) + const [ + advancedRenderingDepthOfFieldFocusDistanceDraft, + setAdvancedRenderingDepthOfFieldFocusDistanceDraft + ] = useState( + String( + editorState.document.world.advancedRendering.depthOfField.focusDistance + ) ); - const [advancedRenderingDepthOfFieldFocalLengthDraft, setAdvancedRenderingDepthOfFieldFocalLengthDraft] = useState( - String(editorState.document.world.advancedRendering.depthOfField.focalLength) + const [ + advancedRenderingDepthOfFieldFocalLengthDraft, + setAdvancedRenderingDepthOfFieldFocalLengthDraft + ] = useState( + String( + editorState.document.world.advancedRendering.depthOfField.focalLength + ) ); - const [advancedRenderingDepthOfFieldBokehScaleDraft, setAdvancedRenderingDepthOfFieldBokehScaleDraft] = useState( + 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 [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 [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: number; selection: EditorSelection; panelId: ViewportPanelId }>({ + const [firstPersonTelemetry, setFirstPersonTelemetry] = + useState(null); + const [runtimeInteractionPrompt, setRuntimeInteractionPrompt] = + useState(null); + const [loadedModelAssets, setLoadedModelAssets] = useState< + Record + >({}); + const [loadedImageAssets, setLoadedImageAssets] = useState< + Record + >({}); + const [loadedAudioAssets, setLoadedAudioAssets] = useState< + Record + >({}); + const [focusRequest, setFocusRequest] = useState<{ + id: number; + selection: EditorSelection; + panelId: ViewportPanelId; + }>({ id: 0, panelId: "topLeft", selection: { @@ -1162,17 +1489,29 @@ export function App({ store, initialStatusMessage }: AppProps) { x: Math.round(window.innerWidth * 0.5), y: Math.round(window.innerHeight * 0.5) }); - const [viewportQuadResizeMode, setViewportQuadResizeMode] = useState(null); + 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 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"); + documentValidation.errors.length === 0 + ? "Valid" + : formatDiagnosticCount(documentValidation.errors.length, "error"); const lastCommandLabel = editorState.lastCommandLabel ?? "No commands yet"; const runReadyLabel = blockingDiagnostics.length > 0 @@ -1181,14 +1520,34 @@ export function App({ store, initialStatusMessage }: AppProps) { ? "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); + 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); @@ -1212,7 +1571,9 @@ export function App({ store, initialStatusMessage }: AppProps) { setRotationDraft(createVec3Draft(DEFAULT_BOX_BRUSH_ROTATION_DEGREES)); setSizeDraft(createVec3Draft(DEFAULT_BOX_BRUSH_SIZE)); setBoxVolumeModeDraft("none"); - setBoxVolumeWaterFoamContactLimitDraft(String(DEFAULT_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT)); + setBoxVolumeWaterFoamContactLimitDraft( + String(DEFAULT_BOX_BRUSH_WATER_FOAM_CONTACT_LIMIT) + ); setBoxVolumeWaterSurfaceDisplacementEnabledDraft(false); return; } @@ -1225,10 +1586,18 @@ export function App({ store, initialStatusMessage }: AppProps) { if (selectedBrush.volume.mode === "water") { setBoxVolumeWaterColorDraft(selectedBrush.volume.water.colorHex); - setBoxVolumeWaterSurfaceOpacityDraft(String(selectedBrush.volume.water.surfaceOpacity)); - setBoxVolumeWaterWaveStrengthDraft(String(selectedBrush.volume.water.waveStrength)); - setBoxVolumeWaterFoamContactLimitDraft(String(selectedBrush.volume.water.foamContactLimit)); - setBoxVolumeWaterSurfaceDisplacementEnabledDraft(selectedBrush.volume.water.surfaceDisplacementEnabled); + setBoxVolumeWaterSurfaceOpacityDraft( + String(selectedBrush.volume.water.surfaceOpacity) + ); + setBoxVolumeWaterWaveStrengthDraft( + String(selectedBrush.volume.water.waveStrength) + ); + setBoxVolumeWaterFoamContactLimitDraft( + String(selectedBrush.volume.water.foamContactLimit) + ); + setBoxVolumeWaterSurfaceDisplacementEnabledDraft( + selectedBrush.volume.water.surfaceDisplacementEnabled + ); } if (selectedBrush.volume.mode === "fog") { @@ -1264,13 +1633,25 @@ export function App({ store, initialStatusMessage }: AppProps) { 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 ?? ""); + 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)); + setSoundEmitterRefDistanceDraft( + String(DEFAULT_SOUND_EMITTER_REF_DISTANCE) + ); + setSoundEmitterMaxDistanceDraft( + String(DEFAULT_SOUND_EMITTER_MAX_DISTANCE) + ); setSoundEmitterAutoplayDraft(false); setSoundEmitterLoopDraft(false); setTriggerVolumeSizeDraft(createVec3Draft(DEFAULT_TRIGGER_VOLUME_SIZE)); @@ -1300,9 +1681,15 @@ export function App({ store, initialStatusMessage }: AppProps) { 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)); + setPlayerStartCapsuleRadiusDraft( + String(selectedEntity.collider.capsuleRadius) + ); + setPlayerStartCapsuleHeightDraft( + String(selectedEntity.collider.capsuleHeight) + ); + setPlayerStartBoxSizeDraft( + createVec3Draft(selectedEntity.collider.boxSize) + ); break; case "soundEmitter": setSoundEmitterAudioAssetIdDraft(selectedEntity.audioAssetId ?? ""); @@ -1329,47 +1716,79 @@ export function App({ store, initialStatusMessage }: AppProps) { useEffect(() => { if (selectedModelInstance === null) { setModelPositionDraft(createVec3Draft(DEFAULT_MODEL_INSTANCE_POSITION)); - setModelRotationDraft(createVec3Draft(DEFAULT_MODEL_INSTANCE_ROTATION_DEGREES)); + setModelRotationDraft( + createVec3Draft(DEFAULT_MODEL_INSTANCE_ROTATION_DEGREES) + ); setModelScaleDraft(createVec3Draft(DEFAULT_MODEL_INSTANCE_SCALE)); return; } setModelPositionDraft(createVec3Draft(selectedModelInstance.position)); - setModelRotationDraft(createVec3Draft(selectedModelInstance.rotationDegrees)); + setModelRotationDraft( + createVec3Draft(selectedModelInstance.rotationDegrees) + ); setModelScaleDraft(createVec3Draft(selectedModelInstance.scale)); }, [selectedModelInstance]); useEffect(() => { - setAmbientLightIntensityDraft(String(editorState.document.world.ambientLight.intensity)); + 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)); + setBackgroundEnvironmentIntensityDraft( + String(editorState.document.world.background.environmentIntensity) + ); } }, [editorState.document.world.background]); useEffect(() => { - setSunLightIntensityDraft(String(editorState.document.world.sunLight.intensity)); + setSunLightIntensityDraft( + String(editorState.document.world.sunLight.intensity) + ); }, [editorState.document.world.sunLight.intensity]); useEffect(() => { - setSunDirectionDraft(createVec3Draft(editorState.document.world.sunLight.direction)); + 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)); + 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(() => { @@ -1488,9 +1907,15 @@ export function App({ store, initialStatusMessage }: AppProps) { 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 previousLoadedModelAssetIds = new Set( + Object.keys(previousLoadedModelAssets) + ); + const previousLoadedImageAssetIds = new Set( + Object.keys(previousLoadedImageAssets) + ); + const previousLoadedAudioAssetIds = new Set( + Object.keys(previousLoadedAudioAssets) + ); const nextLoadedModelAssets: Record = {}; const nextLoadedImageAssets: Record = {}; const nextLoadedAudioAssets: Record = {}; @@ -1524,15 +1949,23 @@ export function App({ store, initialStatusMessage }: AppProps) { const cachedLoadedAsset = previousLoadedModelAssets[asset.id]; - if (cachedLoadedAsset !== undefined && cachedLoadedAsset.storageKey === asset.storageKey) { + if ( + cachedLoadedAsset !== undefined && + cachedLoadedAsset.storageKey === asset.storageKey + ) { nextLoadedModelAssets[asset.id] = cachedLoadedAsset; continue; } try { - nextLoadedModelAssets[asset.id] = await loadModelAssetFromStorage(projectAssetStorage, asset); + nextLoadedModelAssets[asset.id] = await loadModelAssetFromStorage( + projectAssetStorage, + asset + ); } catch (error) { - syncErrorMessages.push(`Model asset ${asset.sourceName} could not be restored: ${getErrorMessage(error)}`); + syncErrorMessages.push( + `Model asset ${asset.sourceName} could not be restored: ${getErrorMessage(error)}` + ); } continue; @@ -1543,15 +1976,23 @@ export function App({ store, initialStatusMessage }: AppProps) { const cachedLoadedAsset = previousLoadedImageAssets[asset.id]; - if (cachedLoadedAsset !== undefined && cachedLoadedAsset.storageKey === asset.storageKey) { + if ( + cachedLoadedAsset !== undefined && + cachedLoadedAsset.storageKey === asset.storageKey + ) { nextLoadedImageAssets[asset.id] = cachedLoadedAsset; continue; } try { - nextLoadedImageAssets[asset.id] = await loadImageAssetFromStorage(projectAssetStorage, asset); + nextLoadedImageAssets[asset.id] = await loadImageAssetFromStorage( + projectAssetStorage, + asset + ); } catch (error) { - syncErrorMessages.push(`Image asset ${asset.sourceName} could not be restored: ${getErrorMessage(error)}`); + syncErrorMessages.push( + `Image asset ${asset.sourceName} could not be restored: ${getErrorMessage(error)}` + ); } continue; } @@ -1561,15 +2002,23 @@ export function App({ store, initialStatusMessage }: AppProps) { const cachedLoadedAsset = previousLoadedAudioAssets[asset.id]; - if (cachedLoadedAsset !== undefined && cachedLoadedAsset.storageKey === asset.storageKey) { + if ( + cachedLoadedAsset !== undefined && + cachedLoadedAsset.storageKey === asset.storageKey + ) { nextLoadedAudioAssets[asset.id] = cachedLoadedAsset; continue; } try { - nextLoadedAudioAssets[asset.id] = await loadAudioAssetFromStorage(projectAssetStorage, asset); + nextLoadedAudioAssets[asset.id] = await loadAudioAssetFromStorage( + projectAssetStorage, + asset + ); } catch (error) { - syncErrorMessages.push(`Audio asset ${asset.sourceName} could not be restored: ${getErrorMessage(error)}`); + syncErrorMessages.push( + `Audio asset ${asset.sourceName} could not be restored: ${getErrorMessage(error)}` + ); } } } @@ -1612,7 +2061,9 @@ export function App({ store, initialStatusMessage }: AppProps) { setLoadedModelAssets(nextLoadedModelAssets); setLoadedImageAssets(nextLoadedImageAssets); setLoadedAudioAssets(nextLoadedAudioAssets); - setAssetStatusMessage(syncErrorMessages.length === 0 ? null : syncErrorMessages.join(" | ")); + setAssetStatusMessage( + syncErrorMessages.length === 0 ? null : syncErrorMessages.join(" | ") + ); }; void syncAssets(); @@ -1620,7 +2071,11 @@ export function App({ store, initialStatusMessage }: AppProps) { return () => { cancelled = true; }; - }, [editorState.document.assets, projectAssetStorage, projectAssetStorageReady]); + }, [ + editorState.document.assets, + projectAssetStorage, + projectAssetStorageReady + ]); useEffect(() => { if (editorState.toolMode === "play") { @@ -1634,11 +2089,17 @@ export function App({ store, initialStatusMessage }: AppProps) { }; const hoveredViewportPanelElement = - event.target instanceof Element ? event.target.closest("[data-viewport-panel-id]") : null; - const hoveredPanelId = hoveredViewportPanelElement?.dataset.viewportPanelId; + 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 === "topLeft" || + hoveredPanelId === "topRight" || + hoveredPanelId === "bottomLeft" || + hoveredPanelId === "bottomRight" ? hoveredPanelId : null ); @@ -1649,7 +2110,8 @@ export function App({ store, initialStatusMessage }: AppProps) { return; } - const hasPrimaryModifier = (event.metaKey || event.ctrlKey) && !event.altKey; + const hasPrimaryModifier = + (event.metaKey || event.ctrlKey) && !event.altKey; if (hasPrimaryModifier && event.code === "KeyR" && !event.shiftKey) { event.preventDefault(); @@ -1768,8 +2230,17 @@ export function App({ store, initialStatusMessage }: AppProps) { } const isDeletionKey = event.key === "Delete" || event.key === "Backspace"; - const isDeleteShortcut = !event.altKey && !event.ctrlKey && !event.metaKey && (event.code === "KeyX" || isDeletionKey); - const isDuplicateShortcut = event.shiftKey && !event.altKey && !event.ctrlKey && !event.metaKey && event.code === "KeyD"; + const isDeleteShortcut = + !event.altKey && + !event.ctrlKey && + !event.metaKey && + (event.code === "KeyX" || isDeletionKey); + const isDuplicateShortcut = + event.shiftKey && + !event.altKey && + !event.ctrlKey && + !event.metaKey && + event.code === "KeyD"; if (addMenuPosition !== null) { if (isDeletionKey) { @@ -1803,14 +2274,21 @@ export function App({ store, initialStatusMessage }: AppProps) { if ( event.code !== "NumpadComma" && - !(event.key === "," && event.location === globalThis.KeyboardEvent.DOM_KEY_LOCATION_NUMPAD) + !( + event.key === "," && + event.location === globalThis.KeyboardEvent.DOM_KEY_LOCATION_NUMPAD + ) ) { return; } event.preventDefault(); - if (editorState.selection.kind === "none" && brushList.length === 0 && entityList.length === 0) { + if ( + editorState.selection.kind === "none" && + brushList.length === 0 && + entityList.length === 0 + ) { setStatusMessage("Nothing authored yet to frame in the viewport."); return; } @@ -1820,7 +2298,11 @@ export function App({ store, initialStatusMessage }: AppProps) { panelId: activePanelId, selection: editorState.selection })); - setStatusMessage(editorState.selection.kind === "none" ? "Framed the authored scene in the viewport." : "Framed the current selection."); + setStatusMessage( + editorState.selection.kind === "none" + ? "Framed the authored scene in the viewport." + : "Framed the current selection." + ); }; document.addEventListener("pointermove", handleWindowPointerMove); @@ -1862,7 +2344,9 @@ export function App({ store, initialStatusMessage }: AppProps) { const previousCursor = document.body.style.cursor; const previousUserSelect = document.body.style.userSelect; - document.body.style.cursor = getViewportQuadResizeCursor(viewportQuadResizeMode); + document.body.style.cursor = getViewportQuadResizeCursor( + viewportQuadResizeMode + ); document.body.style.userSelect = "none"; const handlePointerMove = (event: globalThis.PointerEvent) => { @@ -1883,11 +2367,15 @@ export function App({ store, initialStatusMessage }: AppProps) { }; if (viewportQuadResizeMode !== "horizontal") { - nextViewportQuadSplit.x = clampViewportQuadSplitValue((event.clientX - rect.left) / rect.width); + nextViewportQuadSplit.x = clampViewportQuadSplitValue( + (event.clientX - rect.left) / rect.width + ); } if (viewportQuadResizeMode !== "vertical") { - nextViewportQuadSplit.y = clampViewportQuadSplitValue((event.clientY - rect.top) / rect.height); + nextViewportQuadSplit.y = clampViewportQuadSplitValue( + (event.clientY - rect.top) / rect.height + ); } store.setViewportQuadSplit(nextViewportQuadSplit); @@ -1924,7 +2412,9 @@ export function App({ store, initialStatusMessage }: AppProps) { return; } - const pointerCaptured = activeNavigationMode === "firstPerson" && firstPersonTelemetry?.pointerLocked === true; + const pointerCaptured = + activeNavigationMode === "firstPerson" && + firstPersonTelemetry?.pointerLocked === true; if (pointerCaptured) { return; @@ -1952,7 +2442,10 @@ export function App({ store, initialStatusMessage }: AppProps) { setStatusMessage(`Scene renamed to ${normalizedName}.`); }; - const requestViewportFocus = (selection: EditorSelection, status?: string) => { + const requestViewportFocus = ( + selection: EditorSelection, + status?: string + ) => { setFocusRequest((current) => ({ id: current.id + 1, panelId: activePanelId, @@ -1974,7 +2467,9 @@ export function App({ store, initialStatusMessage }: AppProps) { setAddMenuPosition(null); }; - const handleOpenAddMenuFromButton = (event: ReactMouseEvent) => { + const handleOpenAddMenuFromButton = ( + event: ReactMouseEvent + ) => { const rect = event.currentTarget.getBoundingClientRect(); openAddMenuAt({ @@ -1990,7 +2485,9 @@ export function App({ store, initialStatusMessage }: AppProps) { blurActiveTextEntry(); store.setViewportLayoutMode(nextLayoutMode); - setStatusMessage(`Switched the viewport to ${getViewportLayoutModeLabel(nextLayoutMode)}.`); + setStatusMessage( + `Switched the viewport to ${getViewportLayoutModeLabel(nextLayoutMode)}.` + ); }; const handleActivateViewportPanel = (panelId: ViewportPanelId) => { @@ -2003,7 +2500,10 @@ export function App({ store, initialStatusMessage }: AppProps) { setStatusMessage("Activated the viewport panel."); }; - const handleSetViewportPanelViewMode = (panelId: ViewportPanelId, nextViewMode: ViewportViewMode) => { + const handleSetViewportPanelViewMode = ( + panelId: ViewportPanelId, + nextViewMode: ViewportViewMode + ) => { if (editorState.viewportPanels[panelId].viewMode === nextViewMode) { return; } @@ -2011,36 +2511,58 @@ export function App({ store, initialStatusMessage }: AppProps) { blurActiveTextEntry(); store.setViewportPanelViewMode(panelId, nextViewMode); - setStatusMessage(`Set the viewport panel to ${getViewportViewModeLabel(nextViewMode)} view.`); + setStatusMessage( + `Set the viewport panel to ${getViewportViewModeLabel(nextViewMode)} view.` + ); }; - const handleSetViewportPanelDisplayMode = (panelId: ViewportPanelId, nextDisplayMode: ViewportDisplayMode) => { + const handleSetViewportPanelDisplayMode = ( + panelId: ViewportPanelId, + nextDisplayMode: ViewportDisplayMode + ) => { if (editorState.viewportPanels[panelId].displayMode === nextDisplayMode) { return; } blurActiveTextEntry(); store.setViewportPanelDisplayMode(panelId, nextDisplayMode); - setStatusMessage(`Set the viewport panel to ${getViewportDisplayModeLabel(nextDisplayMode)} display.`); + setStatusMessage( + `Set the viewport panel to ${getViewportDisplayModeLabel(nextDisplayMode)} display.` + ); }; - const beginTransformOperation = (operation: TransformOperation, source: TransformSessionSource) => { + const beginTransformOperation = ( + operation: TransformOperation, + source: TransformSessionSource + ) => { if (editorState.toolMode !== "select") { return; } - const transformSourcePanelId = layoutMode === "quad" ? hoveredViewportPanelId ?? activePanelId : activePanelId; + const transformSourcePanelId = + layoutMode === "quad" + ? (hoveredViewportPanelId ?? activePanelId) + : activePanelId; - const transformTargetResult = resolveTransformTarget(editorState.document, editorState.selection, whiteboxSelectionMode); + 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."); + 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)}.`); + setStatusMessage( + `${getTransformOperationLabel(operation)} is not supported for ${getTransformTargetLabel(transformTarget)}.` + ); return; } @@ -2066,7 +2588,9 @@ export function App({ store, initialStatusMessage }: AppProps) { ); }; - const cancelTransformSession = (status = "Cancelled the current transform.") => { + const cancelTransformSession = ( + status = "Cancelled the current transform." + ) => { if (transformSession.kind === "none") { return; } @@ -2075,7 +2599,9 @@ export function App({ store, initialStatusMessage }: AppProps) { setStatusMessage(status); }; - const commitTransformSession = (activeTransformSession: ActiveTransformSession) => { + const commitTransformSession = ( + activeTransformSession: ActiveTransformSession + ) => { if (!doesTransformSessionChangeTarget(activeTransformSession)) { store.clearTransformSession(); setStatusMessage("No transform change was committed."); @@ -2084,7 +2610,12 @@ export function App({ store, initialStatusMessage }: AppProps) { try { store.clearTransformSession(); - store.executeCommand(createCommitTransformSessionCommand(editorState.document, activeTransformSession)); + store.executeCommand( + createCommitTransformSessionCommand( + editorState.document, + activeTransformSession + ) + ); setStatusMessage( `${getTransformOperationPastTense(activeTransformSession.operation)} ${getTransformTargetLabel(activeTransformSession.target).toLowerCase()}.` ); @@ -2101,7 +2632,9 @@ export function App({ store, initialStatusMessage }: AppProps) { if (!supportsTransformAxisConstraint(transformSession, axis)) { const supportedAxes = (["x", "y", "z"] as const) - .filter((candidateAxis) => supportsTransformAxisConstraint(transformSession, candidateAxis)) + .filter((candidateAxis) => + supportsTransformAxisConstraint(transformSession, candidateAxis) + ) .map((candidateAxis) => candidateAxis.toUpperCase()) .join("/"); setStatusMessage( @@ -2113,9 +2646,16 @@ export function App({ store, initialStatusMessage }: AppProps) { } const nextAxisConstraintSpace = - transformSession.axisConstraint === axis ? (transformSession.axisConstraintSpace === "world" ? "local" : "world") : "world"; + transformSession.axisConstraint === axis + ? transformSession.axisConstraintSpace === "world" + ? "local" + : "world" + : "world"; - if (nextAxisConstraintSpace === "local" && !supportsLocalTransformAxisConstraint(transformSession, axis)) { + if ( + nextAxisConstraintSpace === "local" && + !supportsLocalTransformAxisConstraint(transformSession, axis) + ) { setStatusMessage( `Local ${getTransformAxisLabel(axis)} is not supported for ${getTransformOperationLabel(transformSession.operation).toLowerCase()} on ${getTransformTargetLabel( transformSession.target @@ -2133,7 +2673,8 @@ export function App({ store, initialStatusMessage }: AppProps) { }; const handleViewportQuadResizeStart = - (resizeMode: ViewportQuadResizeMode) => (event: ReactPointerEvent) => { + (resizeMode: ViewportQuadResizeMode) => + (event: ReactPointerEvent) => { if (layoutMode !== "quad") { return; } @@ -2156,11 +2697,15 @@ export function App({ store, initialStatusMessage }: AppProps) { }; if (resizeMode !== "horizontal") { - nextViewportQuadSplit.x = clampViewportQuadSplitValue((event.clientX - rect.left) / rect.width); + nextViewportQuadSplit.x = clampViewportQuadSplitValue( + (event.clientX - rect.left) / rect.width + ); } if (resizeMode !== "vertical") { - nextViewportQuadSplit.y = clampViewportQuadSplitValue((event.clientY - rect.top) / rect.height); + nextViewportQuadSplit.y = clampViewportQuadSplitValue( + (event.clientY - rect.top) / rect.height + ); } store.setViewportQuadSplit(nextViewportQuadSplit); @@ -2169,7 +2714,10 @@ export function App({ store, initialStatusMessage }: AppProps) { setViewportQuadResizeMode(resizeMode); }; - const beginCreation = (toolPreview: CreationViewportToolPreview, status: string) => { + const beginCreation = ( + toolPreview: CreationViewportToolPreview, + status: string + ) => { blurActiveTextEntry(); closeAddMenu(); store.setToolMode("create"); @@ -2200,17 +2748,26 @@ export function App({ store, initialStatusMessage }: AppProps) { const handleWhiteboxSnapToggle = () => { const nextEnabled = !whiteboxSnapEnabled; setWhiteboxSnapEnabled(nextEnabled); - setStatusMessage(nextEnabled ? `Grid snap enabled at ${whiteboxSnapStep}m.` : "Grid snap disabled for whitebox transforms."); + setStatusMessage( + nextEnabled + ? `Grid snap enabled at ${whiteboxSnapStep}m.` + : "Grid snap disabled for whitebox transforms." + ); }; const handleViewportGridToggle = () => { const nextVisible = !viewportGridVisible; setViewportGridVisible(nextVisible); - setStatusMessage(nextVisible ? "Viewport grid enabled." : "Viewport grid hidden."); + setStatusMessage( + nextVisible ? "Viewport grid enabled." : "Viewport grid hidden." + ); }; const handleWhiteboxSnapStepBlur = () => { - const normalizedStep = resolveOptionalPositiveNumber(whiteboxSnapStepDraft, DEFAULT_GRID_SIZE); + const normalizedStep = resolveOptionalPositiveNumber( + whiteboxSnapStepDraft, + DEFAULT_GRID_SIZE + ); setWhiteboxSnapStepDraft(String(normalizedStep)); }; @@ -2232,14 +2789,21 @@ export function App({ store, initialStatusMessage }: AppProps) { blurActiveTextEntry(); store.setSelection(selection); - const suffix = source === "outliner" && options.focusViewport ? " and framed it in the viewport" : ""; + 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}.`); + setStatusMessage( + `${source === "viewport" ? "Viewport" : "Editor"} selection cleared${suffix}.` + ); break; case "brushes": - setStatusMessage(`Selected ${getBrushLabelById(selection.ids[0], brushList)} from the ${source}${suffix}.`); + setStatusMessage( + `Selected ${getBrushLabelById(selection.ids[0], brushList)} from the ${source}${suffix}.` + ); break; case "brushFace": setStatusMessage( @@ -2257,7 +2821,9 @@ export function App({ store, initialStatusMessage }: AppProps) { ); break; case "entities": - setStatusMessage(`Selected ${getEntityDisplayLabelById(selection.ids[0], editorState.document.entities, editorState.document.assets)} from the ${source}${suffix}.`); + setStatusMessage( + `Selected ${getEntityDisplayLabelById(selection.ids[0], editorState.document.entities, editorState.document.assets)} from the ${source}${suffix}.` + ); break; case "modelInstances": setStatusMessage( @@ -2275,13 +2841,23 @@ export function App({ store, initialStatusMessage }: AppProps) { }; const applyPositionChange = () => { - if (selectedBrush === null || editorState.selection.kind !== "brushes" || whiteboxSelectionMode !== "object") { - setStatusMessage("Switch to Object mode and select a whitebox box before moving it."); + 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); + const nextCenter = maybeSnapVec3( + readVec3Draft(positionDraft, "Whitebox box position"), + whiteboxSnapEnabled, + whiteboxSnapStep + ); if (areVec3Equal(nextCenter, selectedBrush.center)) { return; @@ -2301,13 +2877,22 @@ export function App({ store, initialStatusMessage }: AppProps) { }; const applyRotationChange = () => { - if (selectedBrush === null || editorState.selection.kind !== "brushes" || whiteboxSelectionMode !== "object") { - setStatusMessage("Switch to Object mode and select a whitebox box before rotating it."); + 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"); + const nextRotationDegrees = readVec3Draft( + rotationDraft, + "Whitebox box rotation" + ); if (areVec3Equal(nextRotationDegrees, selectedBrush.rotationDegrees)) { return; @@ -2326,13 +2911,23 @@ export function App({ store, initialStatusMessage }: AppProps) { }; const applySizeChange = () => { - if (selectedBrush === null || editorState.selection.kind !== "brushes" || whiteboxSelectionMode !== "object") { - setStatusMessage("Switch to Object mode and select a whitebox box before scaling it."); + 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); + const nextSize = maybeSnapPositiveSize( + readVec3Draft(sizeDraft, "Whitebox box size"), + whiteboxSnapEnabled, + whiteboxSnapStep + ); if (areVec3Equal(nextSize, selectedBrush.size)) { return; @@ -2379,7 +2974,9 @@ export function App({ store, initialStatusMessage }: AppProps) { const applyBoxVolumeModeChange = (mode: BoxBrushVolumeMode) => { if (selectedBrush === null) { - setStatusMessage("Select a whitebox box before changing the volume mode."); + setStatusMessage( + "Select a whitebox box before changing the volume mode." + ); return; } @@ -2402,10 +2999,19 @@ export function App({ store, initialStatusMessage }: AppProps) { mode: "water", water: { colorHex: boxVolumeWaterColorDraft, - surfaceOpacity: readNonNegativeNumberDraft(boxVolumeWaterSurfaceOpacityDraft, "Water surface opacity"), - waveStrength: readNonNegativeNumberDraft(boxVolumeWaterWaveStrengthDraft, "Water wave strength"), - foamContactLimit: readWaterFoamContactLimitDraft(boxVolumeWaterFoamContactLimitDraft), - surfaceDisplacementEnabled: boxVolumeWaterSurfaceDisplacementEnabledDraft + surfaceOpacity: readNonNegativeNumberDraft( + boxVolumeWaterSurfaceOpacityDraft, + "Water surface opacity" + ), + waveStrength: readNonNegativeNumberDraft( + boxVolumeWaterWaveStrengthDraft, + "Water wave strength" + ), + foamContactLimit: readWaterFoamContactLimitDraft( + boxVolumeWaterFoamContactLimitDraft + ), + surfaceDisplacementEnabled: + boxVolumeWaterSurfaceDisplacementEnabledDraft } }; } @@ -2416,8 +3022,14 @@ export function App({ store, initialStatusMessage }: AppProps) { mode: "fog", fog: { colorHex: boxVolumeFogColorDraft, - density: readNonNegativeNumberDraft(boxVolumeFogDensityDraft, "Fog density"), - padding: readNonNegativeNumberDraft(boxVolumeFogPaddingDraft, "Fog padding") + density: readNonNegativeNumberDraft( + boxVolumeFogDensityDraft, + "Fog density" + ), + padding: readNonNegativeNumberDraft( + boxVolumeFogPaddingDraft, + "Fog padding" + ) } }; }, @@ -2437,11 +3049,23 @@ export function App({ store, initialStatusMessage }: AppProps) { ) => ({ colorHex: overrides.colorHex ?? boxVolumeWaterColorDraft, surfaceOpacity: - overrides.surfaceOpacity ?? readNonNegativeNumberDraft(boxVolumeWaterSurfaceOpacityDraft, "Water surface opacity"), - waveStrength: overrides.waveStrength ?? readNonNegativeNumberDraft(boxVolumeWaterWaveStrengthDraft, "Water wave strength"), - foamContactLimit: overrides.foamContactLimit ?? readWaterFoamContactLimitDraft(boxVolumeWaterFoamContactLimitDraft), + overrides.surfaceOpacity ?? + readNonNegativeNumberDraft( + boxVolumeWaterSurfaceOpacityDraft, + "Water surface opacity" + ), + waveStrength: + overrides.waveStrength ?? + readNonNegativeNumberDraft( + boxVolumeWaterWaveStrengthDraft, + "Water wave strength" + ), + foamContactLimit: + overrides.foamContactLimit ?? + readWaterFoamContactLimitDraft(boxVolumeWaterFoamContactLimitDraft), surfaceDisplacementEnabled: - overrides.surfaceDisplacementEnabled ?? boxVolumeWaterSurfaceDisplacementEnabledDraft + overrides.surfaceDisplacementEnabled ?? + boxVolumeWaterSurfaceDisplacementEnabledDraft }); const applyBoxWaterSettings = ( @@ -2492,8 +3116,14 @@ export function App({ store, initialStatusMessage }: AppProps) { mode: "fog", fog: { colorHex: boxVolumeFogColorDraft, - density: readNonNegativeNumberDraft(boxVolumeFogDensityDraft, "Fog density"), - padding: readNonNegativeNumberDraft(boxVolumeFogPaddingDraft, "Fog padding") + density: readNonNegativeNumberDraft( + boxVolumeFogDensityDraft, + "Fog density" + ), + padding: readNonNegativeNumberDraft( + boxVolumeFogPaddingDraft, + "Fog padding" + ) } }), "Set box fog settings", @@ -2511,8 +3141,14 @@ export function App({ store, initialStatusMessage }: AppProps) { mode: "fog", fog: { colorHex, - density: readNonNegativeNumberDraft(boxVolumeFogDensityDraft, "Fog density"), - padding: readNonNegativeNumberDraft(boxVolumeFogPaddingDraft, "Fog padding") + density: readNonNegativeNumberDraft( + boxVolumeFogDensityDraft, + "Fog density" + ), + padding: readNonNegativeNumberDraft( + boxVolumeFogPaddingDraft, + "Fog padding" + ) } }), "Set box fog color", @@ -2520,7 +3156,11 @@ export function App({ store, initialStatusMessage }: AppProps) { ); }; - const commitEntityChange = (currentEntity: EntityInstance, nextEntity: EntityInstance, successMessage: string) => { + const commitEntityChange = ( + currentEntity: EntityInstance, + nextEntity: EntityInstance, + successMessage: string + ) => { if (areEntityInstancesEqual(currentEntity, nextEntity)) { return; } @@ -2534,7 +3174,10 @@ export function App({ store, initialStatusMessage }: AppProps) { setStatusMessage(successMessage); }; - const beginEntityCreation = (kind: EntityKind, options: { audioAssetId?: string | null } = {}) => { + const beginEntityCreation = ( + kind: EntityKind, + options: { audioAssetId?: string | null } = {} + ) => { beginCreation( { kind: "create", @@ -2572,10 +3215,13 @@ export function App({ store, initialStatusMessage }: AppProps) { ); }; - const handleCommitCreation = (creationPreview: CreationViewportToolPreview): boolean => { + const handleCommitCreation = ( + creationPreview: CreationViewportToolPreview + ): boolean => { try { if (creationPreview.target.kind === "box-brush") { - const center = creationPreview.center === null ? undefined : creationPreview.center; + const center = + creationPreview.center === null ? undefined : creationPreview.center; store.executeCommand( createCreateBoxBrushCommand( @@ -2604,17 +3250,22 @@ export function App({ store, initialStatusMessage }: AppProps) { } if (creationPreview.target.kind === "model-instance") { - const asset = editorState.document.assets[creationPreview.target.assetId]; + const asset = + editorState.document.assets[creationPreview.target.assetId]; if (asset === undefined || asset.kind !== "model") { - setStatusMessage("Select a model asset before placing a model instance."); + 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, + creationPreview.center === null + ? createModelInstancePlacementPosition(asset, null) + : creationPreview.center, rotationDegrees: DEFAULT_MODEL_INSTANCE_ROTATION_DEGREES, scale: DEFAULT_MODEL_INSTANCE_SCALE }); @@ -2666,7 +3317,10 @@ export function App({ store, initialStatusMessage }: AppProps) { completeCreation("Placed Player Start."); return true; case "soundEmitter": { - const placedAudioAssetId = creationPreview.target.audioAssetId ?? audioAssetList[0]?.id ?? null; + const placedAudioAssetId = + creationPreview.target.audioAssetId ?? + audioAssetList[0]?.id ?? + null; store.executeCommand( createUpsertEntityCommand({ @@ -2725,7 +3379,11 @@ export function App({ store, initialStatusMessage }: AppProps) { return false; }; - const commitModelInstanceChange = (currentModelInstance: ModelInstance, nextModelInstance: ModelInstance, successMessage: string) => { + const commitModelInstanceChange = ( + currentModelInstance: ModelInstance, + nextModelInstance: ModelInstance, + successMessage: string + ) => { if (areModelInstancesEqual(currentModelInstance, nextModelInstance)) { return; } @@ -2752,28 +3410,41 @@ export function App({ store, initialStatusMessage }: AppProps) { name: selectedModelInstance.name, collision: selectedModelInstance.collision, position: readVec3Draft(modelPositionDraft, "Model instance position"), - rotationDegrees: readVec3Draft(modelRotationDraft, "Model instance rotation"), + rotationDegrees: readVec3Draft( + modelRotationDraft, + "Model instance rotation" + ), scale: readPositiveVec3Draft(modelScaleDraft, "Model instance scale"), animationClipName: selectedModelInstance.animationClipName, animationAutoplay: selectedModelInstance.animationAutoplay }); - commitModelInstanceChange(selectedModelInstance, nextModelInstance, "Updated model instance."); + commitModelInstanceChange( + selectedModelInstance, + nextModelInstance, + "Updated model instance." + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } }; - const applyPlayerStartChange = (overrides: { colliderMode?: PlayerStartColliderMode } = {}) => { + const applyPlayerStartChange = ( + overrides: { colliderMode?: PlayerStartColliderMode } = {} + ) => { 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 snappedPosition = snapVec3ToGrid( + readVec3Draft(entityPositionDraft, "Player Start position"), + DEFAULT_GRID_SIZE + ); const yawDegrees = readYawDegreesDraft(playerStartYawDraft); - const colliderMode = overrides.colliderMode ?? playerStartColliderModeDraft; + const colliderMode = + overrides.colliderMode ?? playerStartColliderModeDraft; const nextEntity = createPlayerStartEntity({ id: selectedPlayerStart.id, name: selectedPlayerStart.name, @@ -2781,14 +3452,30 @@ export function App({ store, initialStatusMessage }: AppProps) { 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") + 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."); + commitEntityChange( + selectedPlayerStart, + nextEntity, + "Updated Player Start." + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } @@ -2804,13 +3491,26 @@ export function App({ store, initialStatusMessage }: AppProps) { const nextEntity = createPointLightEntity({ id: selectedPointLight.id, name: selectedPointLight.name, - position: snapVec3ToGrid(readVec3Draft(entityPositionDraft, "Point Light position"), DEFAULT_GRID_SIZE), + 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") + intensity: readNonNegativeNumberDraft( + pointLightIntensityDraft, + "Point Light intensity" + ), + distance: readPositiveNumberDraft( + pointLightDistanceDraft, + "Point Light distance" + ) }); - commitEntityChange(selectedPointLight, nextEntity, "Updated Point Light."); + commitEntityChange( + selectedPointLight, + nextEntity, + "Updated Point Light." + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } @@ -2826,12 +3526,27 @@ export function App({ store, initialStatusMessage }: AppProps) { 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"), + 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") + intensity: readNonNegativeNumberDraft( + spotLightIntensityDraft, + "Spot Light intensity" + ), + distance: readPositiveNumberDraft( + spotLightDistanceDraft, + "Spot Light distance" + ), + angleDegrees: readPositiveNumberDraft( + spotLightAngleDraft, + "Spot Light angle" + ) }); commitEntityChange(selectedSpotLight, nextEntity, "Updated Spot Light."); @@ -2871,7 +3586,11 @@ export function App({ store, initialStatusMessage }: AppProps) { }; const applySoundEmitterChange = ( - overrides: { audioAssetId?: string | null; autoplay?: boolean; loop?: boolean } = {} + overrides: { + audioAssetId?: string | null; + autoplay?: boolean; + loop?: boolean; + } = {} ) => { if (selectedSoundEmitter === null) { setStatusMessage("Select a Sound Emitter before editing it."); @@ -2889,16 +3608,32 @@ export function App({ store, initialStatusMessage }: AppProps) { const nextEntity = createSoundEmitterEntity({ id: selectedSoundEmitter.id, name: selectedSoundEmitter.name, - position: snapVec3ToGrid(readVec3Draft(entityPositionDraft, "Sound Emitter position"), DEFAULT_GRID_SIZE), + 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"), + 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."); + commitEntityChange( + selectedSoundEmitter, + nextEntity, + "Updated Sound Emitter." + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } @@ -2913,20 +3648,33 @@ export function App({ store, initialStatusMessage }: AppProps) { 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 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), + 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."); + commitEntityChange( + selectedTriggerVolume, + nextEntity, + "Updated Trigger Volume." + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } @@ -2942,11 +3690,18 @@ export function App({ store, initialStatusMessage }: AppProps) { const nextEntity = createTeleportTargetEntity({ id: selectedTeleportTarget.id, name: selectedTeleportTarget.name, - position: snapVec3ToGrid(readVec3Draft(entityPositionDraft, "Teleport Target position"), DEFAULT_GRID_SIZE), + position: snapVec3ToGrid( + readVec3Draft(entityPositionDraft, "Teleport Target position"), + DEFAULT_GRID_SIZE + ), yawDegrees: readYawDegreesDraft(teleportTargetYawDraft) }); - commitEntityChange(selectedTeleportTarget, nextEntity, "Updated Teleport Target."); + commitEntityChange( + selectedTeleportTarget, + nextEntity, + "Updated Teleport Target." + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } @@ -2962,19 +3717,34 @@ export function App({ store, initialStatusMessage }: AppProps) { const nextEntity = createInteractableEntity({ id: selectedInteractable.id, name: selectedInteractable.name, - position: snapVec3ToGrid(readVec3Draft(entityPositionDraft, "Interactable position"), DEFAULT_GRID_SIZE), - radius: readPositiveNumberDraft(interactableRadiusDraft, "Interactable radius"), + 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."); + commitEntityChange( + selectedInteractable, + nextEntity, + "Updated Interactable." + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } }; - const commitInteractionLinkChange = (currentLink: InteractionLink, nextLink: InteractionLink, successMessage: string, label = "Update interaction link") => { + const commitInteractionLinkChange = ( + currentLink: InteractionLink, + nextLink: InteractionLink, + successMessage: string, + label = "Update interaction link" + ) => { if (areInteractionLinksEqual(currentLink, nextLink)) { return; } @@ -2988,21 +3758,33 @@ export function App({ store, initialStatusMessage }: AppProps) { setStatusMessage(successMessage); }; - const getInteractionSourceEntityForLink = (link: InteractionLink): InteractionSourceEntity | null => { + const getInteractionSourceEntityForLink = ( + link: InteractionLink + ): InteractionSourceEntity | null => { const sourceEntity = editorState.document.entities[link.sourceEntityId]; - return sourceEntity?.kind === "triggerVolume" || sourceEntity?.kind === "interactable" ? sourceEntity : null; + return sourceEntity?.kind === "triggerVolume" || + sourceEntity?.kind === "interactable" + ? sourceEntity + : null; }; const handleAddTeleportInteractionLink = () => { if (selectedInteractionSource === null) { - setStatusMessage("Select a Trigger Volume or Interactable before adding links."); + 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."); + if ( + defaultTarget === undefined || + defaultTarget.kind !== "teleportTarget" + ) { + setStatusMessage( + "Author a Teleport Target before adding a teleport link." + ); return; } @@ -3016,19 +3798,25 @@ export function App({ store, initialStatusMessage }: AppProps) { label: "Add teleport interaction link" }) ); - setStatusMessage(`Added a teleport link to the selected ${selectedInteractionSource.kind === "triggerVolume" ? "Trigger Volume" : "Interactable"}.`); + 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."); + 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."); + setStatusMessage( + "Author at least one whitebox solid before adding a visibility link." + ); return; } @@ -3042,19 +3830,27 @@ export function App({ store, initialStatusMessage }: AppProps) { label: "Add visibility interaction link" }) ); - setStatusMessage(`Added a visibility link to the selected ${selectedInteractionSource.kind === "triggerVolume" ? "Trigger Volume" : "Interactable"}.`); + setStatusMessage( + `Added a visibility link to the selected ${selectedInteractionSource.kind === "triggerVolume" ? "Trigger Volume" : "Interactable"}.` + ); }; - const handleAddSoundInteractionLink = (actionType: "playSound" | "stopSound") => { + const handleAddSoundInteractionLink = ( + actionType: "playSound" | "stopSound" + ) => { if (selectedInteractionSource === null) { - setStatusMessage("Select a Trigger Volume or Interactable before adding links."); + 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."); + setStatusMessage( + "Author a Sound Emitter with an audio asset before adding sound links." + ); return; } @@ -3062,19 +3858,26 @@ export function App({ store, initialStatusMessage }: AppProps) { actionType === "playSound" ? createPlaySoundInteractionLink({ sourceEntityId: selectedInteractionSource.id, - trigger: getDefaultInteractionLinkTrigger(selectedInteractionSource), + trigger: getDefaultInteractionLinkTrigger( + selectedInteractionSource + ), targetSoundEmitterId: defaultTarget.id }) : createStopSoundInteractionLink({ sourceEntityId: selectedInteractionSource.id, - trigger: getDefaultInteractionLinkTrigger(selectedInteractionSource), + trigger: getDefaultInteractionLinkTrigger( + selectedInteractionSource + ), targetSoundEmitterId: defaultTarget.id }); store.executeCommand( createUpsertInteractionLinkCommand({ link, - label: actionType === "playSound" ? "Add play sound link" : "Add stop sound link" + label: + actionType === "playSound" + ? "Add play sound link" + : "Add stop sound link" }) ); setStatusMessage( @@ -3092,7 +3895,9 @@ export function App({ store, initialStatusMessage }: AppProps) { }; const confirmDeleteSceneItem = (label: string) => - globalThis.window.confirm(`Delete ${label}?\n\nThis can be undone with Undo.`); + globalThis.window.confirm( + `Delete ${label}?\n\nThis can be undone with Undo.` + ); const handleDeleteBrush = (brushId: string) => { const label = getBrushLabelById(brushId, brushList); @@ -3112,7 +3917,11 @@ export function App({ store, initialStatusMessage }: AppProps) { }; const handleDeleteEntity = (entityId: string) => { - const label = getEntityDisplayLabelById(entityId, editorState.document.entities, editorState.document.assets); + const label = getEntityDisplayLabelById( + entityId, + editorState.document.entities, + editorState.document.assets + ); if (!confirmDeleteSceneItem(label)) { return false; @@ -3162,7 +3971,9 @@ export function App({ store, initialStatusMessage }: AppProps) { return handleDeleteEntity(selectedEntityId); } - const selectedModelInstanceId = getSingleSelectedModelInstanceId(editorState.selection); + const selectedModelInstanceId = getSingleSelectedModelInstanceId( + editorState.selection + ); if (selectedModelInstanceId !== null) { return handleDeleteModelInstance(selectedModelInstanceId); @@ -3182,16 +3993,28 @@ export function App({ store, initialStatusMessage }: AppProps) { const duplicatedState = store.getState(); const duplicatedSelection = duplicatedState.selection; const canGrabDuplicatedSelection = - (duplicatedSelection.kind === "brushes" || duplicatedSelection.kind === "entities" || duplicatedSelection.kind === "modelInstances") && + (duplicatedSelection.kind === "brushes" || + duplicatedSelection.kind === "entities" || + duplicatedSelection.kind === "modelInstances") && duplicatedSelection.ids.length === 1; if (canGrabDuplicatedSelection) { - const transformSourcePanelId = layoutMode === "quad" ? hoveredViewportPanelId ?? activePanelId : activePanelId; - const transformTargetResult = resolveTransformTarget(duplicatedState.document, duplicatedSelection, whiteboxSelectionMode); + const transformSourcePanelId = + layoutMode === "quad" + ? (hoveredViewportPanelId ?? activePanelId) + : activePanelId; + const transformTargetResult = resolveTransformTarget( + duplicatedState.document, + duplicatedSelection, + whiteboxSelectionMode + ); const transformTarget = transformTargetResult.target; if (transformTarget === null) { - setStatusMessage(transformTargetResult.message ?? "Duplicated selection, but could not start move transform."); + setStatusMessage( + transformTargetResult.message ?? + "Duplicated selection, but could not start move transform." + ); return true; } @@ -3224,7 +4047,10 @@ export function App({ store, initialStatusMessage }: AppProps) { } }; - const updateInteractionLinkTrigger = (link: InteractionLink, trigger: InteractionTriggerKind) => { + const updateInteractionLinkTrigger = ( + link: InteractionLink, + trigger: InteractionTriggerKind + ) => { const sourceEntity = getInteractionSourceEntityForLink(link); if (sourceEntity?.kind === "interactable" && trigger !== "click") { @@ -3233,7 +4059,9 @@ export function App({ store, initialStatusMessage }: AppProps) { } if (sourceEntity?.kind === "triggerVolume" && trigger === "click") { - setStatusMessage("Trigger Volume links may only use enter or exit triggers."); + setStatusMessage( + "Trigger Volume links may only use enter or exit triggers." + ); return; } @@ -3293,10 +4121,17 @@ export function App({ store, initialStatusMessage }: AppProps) { break; } - commitInteractionLinkChange(link, nextLink, `Updated ${getInteractionTriggerLabel(trigger).toLowerCase()} trigger link.`); + commitInteractionLinkChange( + link, + nextLink, + `Updated ${getInteractionTriggerLabel(trigger).toLowerCase()} trigger link.` + ); }; - const updateInteractionLinkActionType = (link: InteractionLink, actionType: InteractionLink["action"]["type"]) => { + const updateInteractionLinkActionType = ( + link: InteractionLink, + actionType: InteractionLink["action"]["type"] + ) => { const sourceEntity = getInteractionSourceEntityForLink(link); if (sourceEntity === null || link.action.type === actionType) { @@ -3306,8 +4141,13 @@ export function App({ store, initialStatusMessage }: AppProps) { 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."); + if ( + defaultTarget === undefined || + defaultTarget.kind !== "teleportTarget" + ) { + setStatusMessage( + "Author a Teleport Target before switching this link to teleport." + ); return; } @@ -3326,17 +4166,23 @@ export function App({ store, initialStatusMessage }: AppProps) { if (actionType === "playAnimation") { const targetModelInstance = - (link.action.type === "playAnimation" || link.action.type === "stopAnimation" - ? editorState.document.modelInstances[link.action.targetModelInstanceId] + (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."); + 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] ?? "") : ""; + const firstClip = + asset?.kind === "model" ? (asset.metadata.animationNames[0] ?? "") : ""; if (firstClip === "") { setStatusMessage("The model instance has no animation clips."); @@ -3359,12 +4205,17 @@ export function App({ store, initialStatusMessage }: AppProps) { if (actionType === "stopAnimation") { const targetModelInstance = - (link.action.type === "playAnimation" || link.action.type === "stopAnimation" - ? editorState.document.modelInstances[link.action.targetModelInstanceId] + (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."); + setStatusMessage( + "Place a model instance before switching this link to stop animation." + ); return; } @@ -3387,8 +4238,13 @@ export function App({ store, initialStatusMessage }: AppProps) { ? 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."); + if ( + targetSoundEmitter === undefined || + targetSoundEmitter.kind !== "soundEmitter" + ) { + setStatusMessage( + "Author a Sound Emitter with an audio asset before switching this link to sound playback." + ); return; } @@ -3422,7 +4278,9 @@ export function App({ store, initialStatusMessage }: AppProps) { const defaultBrush = visibilityBrushOptions[0]?.brush; if (defaultBrush === undefined) { - setStatusMessage("Author at least one whitebox solid before switching this link to visibility."); + setStatusMessage( + "Author at least one whitebox solid before switching this link to visibility." + ); return; } @@ -3438,7 +4296,10 @@ export function App({ store, initialStatusMessage }: AppProps) { ); }; - const updateTeleportInteractionLinkTarget = (link: InteractionLink, targetEntityId: string) => { + const updateTeleportInteractionLinkTarget = ( + link: InteractionLink, + targetEntityId: string + ) => { if (link.action.type !== "teleportPlayer") { return; } @@ -3455,7 +4316,10 @@ export function App({ store, initialStatusMessage }: AppProps) { ); }; - const updateVisibilityInteractionLinkTarget = (link: InteractionLink, targetBrushId: string) => { + const updateVisibilityInteractionLinkTarget = ( + link: InteractionLink, + targetBrushId: string + ) => { if (link.action.type !== "toggleVisibility") { return; } @@ -3473,7 +4337,10 @@ export function App({ store, initialStatusMessage }: AppProps) { ); }; - const updateVisibilityInteractionMode = (link: InteractionLink, mode: "toggle" | "show" | "hide") => { + const updateVisibilityInteractionMode = ( + link: InteractionLink, + mode: "toggle" | "show" | "hide" + ) => { if (link.action.type !== "toggleVisibility") { return; } @@ -3491,7 +4358,10 @@ export function App({ store, initialStatusMessage }: AppProps) { ); }; - const updateSoundInteractionLinkTarget = (link: InteractionLink, targetSoundEmitterId: string) => { + const updateSoundInteractionLinkTarget = ( + link: InteractionLink, + targetSoundEmitterId: string + ) => { if (link.action.type !== "playSound" && link.action.type !== "stopSound") { return; } @@ -3521,8 +4391,14 @@ export function App({ store, initialStatusMessage }: AppProps) { } }; - const updateAnimationInteractionLinkTarget = (link: InteractionLink, targetModelInstanceId: string) => { - if (link.action.type !== "playAnimation" && link.action.type !== "stopAnimation") { + const updateAnimationInteractionLinkTarget = ( + link: InteractionLink, + targetModelInstanceId: string + ) => { + if ( + link.action.type !== "playAnimation" && + link.action.type !== "stopAnimation" + ) { return; } @@ -3553,7 +4429,10 @@ export function App({ store, initialStatusMessage }: AppProps) { } }; - const updatePlayAnimationLinkClip = (link: InteractionLink, clipName: string) => { + const updatePlayAnimationLinkClip = ( + link: InteractionLink, + clipName: string + ) => { if (link.action.type !== "playAnimation") { return; } @@ -3572,7 +4451,10 @@ export function App({ store, initialStatusMessage }: AppProps) { ); }; - const updatePlayAnimationLinkLoop = (link: InteractionLink, loop: boolean) => { + const updatePlayAnimationLinkLoop = ( + link: InteractionLink, + loop: boolean + ) => { if (link.action.type !== "playAnimation") { return; } @@ -3591,16 +4473,22 @@ export function App({ store, initialStatusMessage }: AppProps) { ); }; - const handleAddPlayAnimationLink = (sourceEntity: InteractionSourceEntity) => { + const handleAddPlayAnimationLink = ( + sourceEntity: InteractionSourceEntity + ) => { const firstInstance = modelInstanceDisplayList[0]; if (firstInstance === undefined) { - setStatusMessage("Place a model instance before adding an animation link."); + 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] ?? "") : ""; + 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."); @@ -3621,11 +4509,15 @@ export function App({ store, initialStatusMessage }: AppProps) { setStatusMessage("Added a play animation link."); }; - const handleAddStopAnimationLink = (sourceEntity: InteractionSourceEntity) => { + const handleAddStopAnimationLink = ( + sourceEntity: InteractionSourceEntity + ) => { const firstInstance = modelInstanceDisplayList[0]; if (firstInstance === undefined) { - setStatusMessage("Place a model instance before adding an animation link."); + setStatusMessage( + "Place a model instance before adding an animation link." + ); return; } @@ -3654,7 +4546,9 @@ export function App({ store, initialStatusMessage }: AppProps) {
Links
{links.length === 0 ? (
- {sourceEntity.kind === "triggerVolume" ? "No trigger links authored yet." : "No click links authored yet."} + {sourceEntity.kind === "triggerVolume" + ? "No trigger links authored yet." + : "No click links authored yet."}
) : (
@@ -3662,7 +4556,9 @@ export function App({ store, initialStatusMessage }: AppProps) {
{`Link ${index + 1}`} - {getInteractionActionLabel(link)} + + {getInteractionActionLabel(link)} +
@@ -3674,7 +4570,12 @@ export function App({ store, initialStatusMessage }: AppProps) { data-testid={`interaction-link-trigger-${link.id}`} className="text-input" value={link.trigger} - onChange={(event) => updateInteractionLinkTrigger(link, event.currentTarget.value as InteractionTriggerKind)} + onChange={(event) => + updateInteractionLinkTrigger( + link, + event.currentTarget.value as InteractionTriggerKind + ) + } > @@ -3695,10 +4596,18 @@ export function App({ store, initialStatusMessage }: AppProps) { data-testid={`interaction-link-action-${link.id}`} className="text-input" value={link.action.type} - onChange={(event) => updateInteractionLinkActionType(link, event.currentTarget.value as InteractionLink["action"]["type"])} + onChange={(event) => + updateInteractionLinkActionType( + link, + event.currentTarget + .value as InteractionLink["action"]["type"] + ) + } > - + @@ -3716,7 +4625,12 @@ export function App({ store, initialStatusMessage }: AppProps) { data-testid={`interaction-link-teleport-target-${link.id}`} className="text-input" value={link.action.targetEntityId} - onChange={(event) => updateTeleportInteractionLinkTarget(link, event.currentTarget.value)} + onChange={(event) => + updateTeleportInteractionLinkTarget( + link, + event.currentTarget.value + ) + } > {teleportTargetOptions.map(({ entity, label }) => (
@@ -3800,12 +4756,18 @@ export function App({ store, initialStatusMessage }: AppProps) { type="checkbox" data-testid={`interaction-link-play-anim-loop-${link.id}`} checked={link.action.loop !== false} - onChange={(event) => updatePlayAnimationLinkLoop(link, event.currentTarget.checked)} + onChange={(event) => + updatePlayAnimationLinkLoop( + link, + event.currentTarget.checked + ) + } /> Loop
- ) : link.action.type === "playSound" || link.action.type === "stopSound" ? ( + ) : link.action.type === "playSound" || + link.action.type === "stopSound" ? (
@@ -3853,7 +4830,6 @@ export function App({ store, initialStatusMessage }: AppProps) { Delete Link
- ))} @@ -3916,7 +4892,11 @@ export function App({ store, initialStatusMessage }: AppProps) { ); - const applyWorldSettings = (nextWorld: WorldSettings, label: string, successMessage: string) => { + const applyWorldSettings = ( + nextWorld: WorldSettings, + label: string, + successMessage: string + ) => { if (areWorldSettingsEqual(editorState.document.world, nextWorld)) { return; } @@ -3944,25 +4924,37 @@ export function App({ store, initialStatusMessage }: AppProps) { applyWorldSettings(nextWorld, label, successMessage); }; - const applyWorldBackgroundMode = (mode: WorldBackgroundMode, imageAssetId?: string) => { + const applyWorldBackgroundMode = ( + mode: WorldBackgroundMode, + imageAssetId?: string + ) => { if (mode === "image") { const currentBackgroundAssetId = - editorState.document.world.background.mode === "image" ? editorState.document.world.background.assetId : null; + editorState.document.world.background.mode === "image" + ? editorState.document.world.background.assetId + : null; const nextImageAssetId = imageAssetId ?? - (currentBackgroundAssetId !== null && editorState.document.assets[currentBackgroundAssetId]?.kind === "image" + (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."); + setStatusMessage( + "Import an image asset before using an image background." + ); return; } applyWorldSettings( { ...editorState.document.world, - background: changeWorldBackgroundMode(editorState.document.world.background, "image", nextImageAssetId) + background: changeWorldBackgroundMode( + editorState.document.world.background, + "image", + nextImageAssetId + ) }, "Set world background image", `World background set to ${editorState.document.assets[nextImageAssetId]?.sourceName ?? nextImageAssetId}.` @@ -3973,10 +4965,15 @@ export function App({ store, initialStatusMessage }: AppProps) { applyWorldSettings( { ...editorState.document.world, - background: changeWorldBackgroundMode(editorState.document.world.background, mode) + 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." + mode === "solid" + ? "World background set to a solid color." + : "World background set to a vertical gradient." ); }; @@ -3998,7 +4995,10 @@ export function App({ store, initialStatusMessage }: AppProps) { ); }; - const applyWorldGradientColor = (edge: "top" | "bottom", colorHex: string) => { + const applyWorldGradientColor = ( + edge: "top" | "bottom", + colorHex: string + ) => { if (editorState.document.world.background.mode !== "verticalGradient") { return; } @@ -4017,8 +5017,12 @@ export function App({ store, initialStatusMessage }: AppProps) { 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." + 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." ); }; @@ -4027,7 +5031,10 @@ export function App({ store, initialStatusMessage }: AppProps) { return; } - const intensity = readNonNegativeNumberDraft(backgroundEnvironmentIntensityDraft, "Environment intensity"); + const intensity = readNonNegativeNumberDraft( + backgroundEnvironmentIntensityDraft, + "Environment intensity" + ); applyWorldSettings( { ...editorState.document.world, @@ -4062,7 +5069,10 @@ export function App({ store, initialStatusMessage }: AppProps) { ...editorState.document.world, ambientLight: { ...editorState.document.world.ambientLight, - intensity: readNonNegativeNumberDraft(ambientLightIntensityDraft, "Ambient light intensity") + intensity: readNonNegativeNumberDraft( + ambientLightIntensityDraft, + "Ambient light intensity" + ) } }, "Set world ambient light intensity", @@ -4094,7 +5104,10 @@ export function App({ store, initialStatusMessage }: AppProps) { ...editorState.document.world, sunLight: { ...editorState.document.world.sunLight, - intensity: readNonNegativeNumberDraft(sunLightIntensityDraft, "Sun intensity") + intensity: readNonNegativeNumberDraft( + sunLightIntensityDraft, + "Sun intensity" + ) } }, "Set world sun intensity", @@ -4142,30 +5155,51 @@ export function App({ store, initialStatusMessage }: AppProps) { const applyAdvancedRenderingShadowsEnabled = (enabled: boolean) => { applyAdvancedRenderingSettings( "Set advanced rendering shadows", - enabled ? "Advanced rendering shadows enabled." : "Advanced rendering shadows disabled.", + enabled + ? "Advanced rendering shadows enabled." + : "Advanced rendering shadows disabled.", (advancedRendering) => { advancedRendering.shadows.enabled = enabled; } ); }; - const applyAdvancedRenderingShadowMapSize = (shadowMapSize: AdvancedRenderingShadowMapSize) => { - applyAdvancedRenderingSettings("Set advanced rendering shadow map size", "Updated the shadow map size.", (advancedRendering) => { - advancedRendering.shadows.mapSize = shadowMapSize; - }); + const applyAdvancedRenderingShadowMapSize = ( + shadowMapSize: AdvancedRenderingShadowMapSize + ) => { + applyAdvancedRenderingSettings( + "Set advanced rendering shadow map size", + "Updated the shadow map size.", + (advancedRendering) => { + advancedRendering.shadows.mapSize = shadowMapSize; + } + ); }; - const applyAdvancedRenderingShadowType = (shadowType: AdvancedRenderingShadowType) => { - applyAdvancedRenderingSettings("Set advanced rendering shadow type", "Updated the shadow map type.", (advancedRendering) => { - advancedRendering.shadows.type = shadowType; - }); + const applyAdvancedRenderingShadowType = ( + shadowType: AdvancedRenderingShadowType + ) => { + 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"); - }); + applyAdvancedRenderingSettings( + "Set advanced rendering shadow bias", + "Updated the shadow bias.", + (advancedRendering) => { + advancedRendering.shadows.bias = readFiniteNumberDraft( + advancedRenderingShadowBiasDraft, + "Shadow bias" + ); + } + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } @@ -4187,10 +5221,11 @@ export function App({ store, initialStatusMessage }: AppProps) { "Set ambient occlusion intensity", "Updated the ambient occlusion intensity.", (advancedRendering) => { - advancedRendering.ambientOcclusion.intensity = readNonNegativeNumberDraft( - advancedRenderingAmbientOcclusionIntensityDraft, - "Ambient occlusion intensity" - ); + advancedRendering.ambientOcclusion.intensity = + readNonNegativeNumberDraft( + advancedRenderingAmbientOcclusionIntensityDraft, + "Ambient occlusion intensity" + ); } ); } catch (error) { @@ -4200,12 +5235,17 @@ export function App({ store, initialStatusMessage }: AppProps) { const applyAdvancedRenderingAmbientOcclusionRadius = () => { try { - applyAdvancedRenderingSettings("Set ambient occlusion radius", "Updated the ambient occlusion radius.", (advancedRendering) => { - advancedRendering.ambientOcclusion.radius = readNonNegativeNumberDraft( - advancedRenderingAmbientOcclusionRadiusDraft, - "Ambient occlusion radius" - ); - }); + applyAdvancedRenderingSettings( + "Set ambient occlusion radius", + "Updated the ambient occlusion radius.", + (advancedRendering) => { + advancedRendering.ambientOcclusion.radius = + readNonNegativeNumberDraft( + advancedRenderingAmbientOcclusionRadiusDraft, + "Ambient occlusion radius" + ); + } + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } @@ -4213,12 +5253,16 @@ export function App({ store, initialStatusMessage }: AppProps) { const applyAdvancedRenderingAmbientOcclusionSamples = () => { try { - applyAdvancedRenderingSettings("Set ambient occlusion samples", "Updated the ambient occlusion samples.", (advancedRendering) => { - advancedRendering.ambientOcclusion.samples = readPositiveIntegerDraft( - advancedRenderingAmbientOcclusionSamplesDraft, - "Ambient occlusion samples" - ); - }); + applyAdvancedRenderingSettings( + "Set ambient occlusion samples", + "Updated the ambient occlusion samples.", + (advancedRendering) => { + advancedRendering.ambientOcclusion.samples = readPositiveIntegerDraft( + advancedRenderingAmbientOcclusionSamplesDraft, + "Ambient occlusion samples" + ); + } + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } @@ -4236,9 +5280,16 @@ export function App({ store, initialStatusMessage }: AppProps) { const applyAdvancedRenderingBloomIntensity = () => { try { - applyAdvancedRenderingSettings("Set bloom intensity", "Updated the bloom intensity.", (advancedRendering) => { - advancedRendering.bloom.intensity = readNonNegativeNumberDraft(advancedRenderingBloomIntensityDraft, "Bloom intensity"); - }); + applyAdvancedRenderingSettings( + "Set bloom intensity", + "Updated the bloom intensity.", + (advancedRendering) => { + advancedRendering.bloom.intensity = readNonNegativeNumberDraft( + advancedRenderingBloomIntensityDraft, + "Bloom intensity" + ); + } + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } @@ -4246,9 +5297,16 @@ export function App({ store, initialStatusMessage }: AppProps) { const applyAdvancedRenderingBloomThreshold = () => { try { - applyAdvancedRenderingSettings("Set bloom threshold", "Updated the bloom threshold.", (advancedRendering) => { - advancedRendering.bloom.threshold = readNonNegativeNumberDraft(advancedRenderingBloomThresholdDraft, "Bloom threshold"); - }); + applyAdvancedRenderingSettings( + "Set bloom threshold", + "Updated the bloom threshold.", + (advancedRendering) => { + advancedRendering.bloom.threshold = readNonNegativeNumberDraft( + advancedRenderingBloomThresholdDraft, + "Bloom threshold" + ); + } + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } @@ -4256,28 +5314,45 @@ export function App({ store, initialStatusMessage }: AppProps) { const applyAdvancedRenderingBloomRadius = () => { try { - applyAdvancedRenderingSettings("Set bloom radius", "Updated the bloom radius.", (advancedRendering) => { - advancedRendering.bloom.radius = readNonNegativeNumberDraft(advancedRenderingBloomRadiusDraft, "Bloom radius"); - }); + applyAdvancedRenderingSettings( + "Set bloom radius", + "Updated the bloom radius.", + (advancedRendering) => { + advancedRendering.bloom.radius = readNonNegativeNumberDraft( + advancedRenderingBloomRadiusDraft, + "Bloom radius" + ); + } + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } }; - const applyAdvancedRenderingToneMappingMode = (mode: AdvancedRenderingToneMappingMode) => { - applyAdvancedRenderingSettings("Set tone mapping mode", "Updated the tone mapping mode.", (advancedRendering) => { - advancedRendering.toneMapping.mode = mode; - }); + const applyAdvancedRenderingToneMappingMode = ( + mode: AdvancedRenderingToneMappingMode + ) => { + 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" - ); - }); + applyAdvancedRenderingSettings( + "Set tone mapping exposure", + "Updated the tone mapping exposure.", + (advancedRendering) => { + advancedRendering.toneMapping.exposure = readPositiveNumberDraft( + advancedRenderingToneMappingExposureDraft, + "Tone mapping exposure" + ); + } + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } @@ -4295,12 +5370,17 @@ export function App({ store, initialStatusMessage }: AppProps) { const applyAdvancedRenderingDepthOfFieldFocusDistance = () => { try { - applyAdvancedRenderingSettings("Set focus distance", "Updated the focus distance.", (advancedRendering) => { - advancedRendering.depthOfField.focusDistance = readNonNegativeNumberDraft( - advancedRenderingDepthOfFieldFocusDistanceDraft, - "Focus distance" - ); - }); + applyAdvancedRenderingSettings( + "Set focus distance", + "Updated the focus distance.", + (advancedRendering) => { + advancedRendering.depthOfField.focusDistance = + readNonNegativeNumberDraft( + advancedRenderingDepthOfFieldFocusDistanceDraft, + "Focus distance" + ); + } + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } @@ -4308,12 +5388,16 @@ export function App({ store, initialStatusMessage }: AppProps) { const applyAdvancedRenderingDepthOfFieldFocalLength = () => { try { - applyAdvancedRenderingSettings("Set focal length", "Updated the focal length.", (advancedRendering) => { - advancedRendering.depthOfField.focalLength = readPositiveNumberDraft( - advancedRenderingDepthOfFieldFocalLengthDraft, - "Focal length" - ); - }); + applyAdvancedRenderingSettings( + "Set focal length", + "Updated the focal length.", + (advancedRendering) => { + advancedRendering.depthOfField.focalLength = readPositiveNumberDraft( + advancedRenderingDepthOfFieldFocalLengthDraft, + "Focal length" + ); + } + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } @@ -4321,21 +5405,29 @@ export function App({ store, initialStatusMessage }: AppProps) { const applyAdvancedRenderingDepthOfFieldBokehScale = () => { try { - applyAdvancedRenderingSettings("Set bokeh scale", "Updated the bokeh scale.", (advancedRendering) => { - advancedRendering.depthOfField.bokehScale = readPositiveNumberDraft( - advancedRenderingDepthOfFieldBokehScaleDraft, - "Bokeh scale" - ); - }); + applyAdvancedRenderingSettings( + "Set bokeh scale", + "Updated the bokeh scale.", + (advancedRendering) => { + advancedRendering.depthOfField.bokehScale = readPositiveNumberDraft( + advancedRenderingDepthOfFieldBokehScaleDraft, + "Bokeh scale" + ); + } + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } }; const applyAdvancedRenderingFogPath = (path: BoxVolumeRenderPath) => { - applyAdvancedRenderingSettings("Set fog render path", `Fog render path set to ${formatBoxVolumeRenderPathLabel(path)}.`, (advancedRendering) => { - advancedRendering.fogPath = path; - }); + applyAdvancedRenderingSettings( + "Set fog render path", + `Fog render path set to ${formatBoxVolumeRenderPathLabel(path)}.`, + (advancedRendering) => { + advancedRendering.fogPath = path; + } + ); }; const applyAdvancedRenderingWaterPath = (path: BoxVolumeRenderPath) => { @@ -4348,7 +5440,9 @@ export function App({ store, initialStatusMessage }: AppProps) { ); }; - const applyAdvancedRenderingWaterReflectionMode = (mode: AdvancedRenderingWaterReflectionMode) => { + const applyAdvancedRenderingWaterReflectionMode = ( + mode: AdvancedRenderingWaterReflectionMode + ) => { applyAdvancedRenderingSettings( "Set water reflection mode", `Water reflection mode set to ${formatAdvancedRenderingWaterReflectionModeLabel(mode)}.`, @@ -4377,7 +5471,11 @@ export function App({ store, initialStatusMessage }: AppProps) { name: brushNameDraft }) ); - setStatusMessage(nextName === undefined ? "Cleared the authored brush name." : `Renamed brush to ${nextName}.`); + setStatusMessage( + nextName === undefined + ? "Cleared the authored brush name." + : `Renamed brush to ${nextName}.` + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } @@ -4402,7 +5500,11 @@ export function App({ store, initialStatusMessage }: AppProps) { name: entityNameDraft }) ); - setStatusMessage(nextName === undefined ? "Cleared the authored entity name." : `Renamed entity to ${nextName}.`); + setStatusMessage( + nextName === undefined + ? "Cleared the authored entity name." + : `Renamed entity to ${nextName}.` + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } @@ -4427,7 +5529,11 @@ export function App({ store, initialStatusMessage }: AppProps) { name: modelInstanceNameDraft }) ); - setStatusMessage(nextName === undefined ? "Cleared the authored model instance name." : `Renamed model instance to ${nextName}.`); + setStatusMessage( + nextName === undefined + ? "Cleared the authored model instance name." + : `Renamed model instance to ${nextName}.` + ); } catch (error) { setStatusMessage(getErrorMessage(error)); } @@ -4450,7 +5556,10 @@ export function App({ store, initialStatusMessage }: AppProps) { } }; - const handleDraftVectorKeyDown = (event: ReactKeyboardEvent, applyChange: () => void) => { + const handleDraftVectorKeyDown = ( + event: ReactKeyboardEvent, + applyChange: () => void + ) => { if (event.key === "Enter") { applyChange(); } @@ -4462,11 +5571,17 @@ export function App({ store, initialStatusMessage }: AppProps) { }, 0); }; - const handleNumberInputPointerUp = (_event: ReactPointerEvent, applyChange: () => void) => { + const handleNumberInputPointerUp = ( + _event: ReactPointerEvent, + applyChange: () => void + ) => { scheduleDraftCommit(applyChange); }; - const handleNumberInputKeyUp = (event: ReactKeyboardEvent, applyChange: () => void) => { + const handleNumberInputKeyUp = ( + event: ReactKeyboardEvent, + applyChange: () => void + ) => { if (!isCommitIncrementKey(event.key)) { return; } @@ -4477,12 +5592,19 @@ export function App({ store, initialStatusMessage }: AppProps) { const handleSaveProject = async () => { try { if (!projectAssetStorageReady && projectAssetList.length > 0) { - throw new Error("Project save failed: project asset storage is still initializing for this asset-backed scene."); + throw new Error( + "Project save failed: project asset storage is still initializing for this asset-backed scene." + ); } - const projectBytes = await saveProjectPackage(editorState.document, projectAssetStorage); + const projectBytes = await saveProjectPackage( + editorState.document, + projectAssetStorage + ); const blobBytes = new Uint8Array(projectBytes); - const blob = new Blob([blobBytes.buffer as ArrayBuffer], { type: "application/zip" }); + const blob = new Blob([blobBytes.buffer as ArrayBuffer], { + type: "application/zip" + }); const objectUrl = URL.createObjectURL(blob); const anchor = document.createElement("a"); @@ -4501,7 +5623,9 @@ export function App({ store, initialStatusMessage }: AppProps) { importProjectInputRef.current?.click(); }; - const handleLoadProjectChange = async (event: ChangeEvent) => { + const handleLoadProjectChange = async ( + event: ChangeEvent + ) => { const input = event.currentTarget; const file = input.files?.[0]; @@ -4511,7 +5635,10 @@ export function App({ store, initialStatusMessage }: AppProps) { try { const projectBytes = new Uint8Array(await file.arrayBuffer()); - const nextDocument = await loadProjectPackage(projectBytes, projectAssetStorage); + const nextDocument = await loadProjectPackage( + projectBytes, + projectAssetStorage + ); store.replaceDocument(nextDocument); setStatusMessage(`Loaded project ${file.name}.`); } catch (error) { @@ -4533,7 +5660,9 @@ export function App({ store, initialStatusMessage }: AppProps) { importAudioInputRef.current?.click(); }; - const handleImportModelChange = async (event: ChangeEvent) => { + const handleImportModelChange = async ( + event: ChangeEvent + ) => { const input = event.currentTarget; const files = Array.from(input.files ?? []); @@ -4542,7 +5671,9 @@ export function App({ store, initialStatusMessage }: AppProps) { } if (projectAssetStorage === null) { - setAssetStatusMessage("Imported model assets require project asset storage. IndexedDB is unavailable in this browser."); + setAssetStatusMessage( + "Imported model assets require project asset storage. IndexedDB is unavailable in this browser." + ); input.value = ""; return; } @@ -4550,9 +5681,10 @@ export function App({ store, initialStatusMessage }: AppProps) { let importedModelForCleanup: ImportedModelAssetResult | null = null; try { - const importedModel = files.length === 1 - ? await importModelAssetFromFile(files[0], projectAssetStorage) - : await importModelAssetFromFiles(files, projectAssetStorage); + const importedModel = + files.length === 1 + ? await importModelAssetFromFile(files[0], projectAssetStorage) + : await importModelAssetFromFiles(files, projectAssetStorage); importedModelForCleanup = importedModel; store.executeCommand( @@ -4572,10 +5704,14 @@ export function App({ store, initialStatusMessage }: AppProps) { [importedModel.asset.id]: importedModel.loadedAsset })); setAssetStatusMessage(null); - setStatusMessage(`Imported ${importedModel.asset.sourceName} and placed a model instance.`); + setStatusMessage( + `Imported ${importedModel.asset.sourceName} and placed a model instance.` + ); } catch (error) { if (importedModelForCleanup !== null) { - await projectAssetStorage.deleteAsset(importedModelForCleanup.asset.storageKey).catch(() => undefined); + await projectAssetStorage + .deleteAsset(importedModelForCleanup.asset.storageKey) + .catch(() => undefined); disposeModelTemplate(importedModelForCleanup.loadedAsset.template); } @@ -4587,7 +5723,9 @@ export function App({ store, initialStatusMessage }: AppProps) { } }; - const handleImportBackgroundImageChange = async (event: ChangeEvent) => { + const handleImportBackgroundImageChange = async ( + event: ChangeEvent + ) => { const input = event.currentTarget; const file = input.files?.[0]; @@ -4596,7 +5734,9 @@ export function App({ store, initialStatusMessage }: AppProps) { } if (projectAssetStorage === null) { - setAssetStatusMessage("Imported background images require project asset storage. IndexedDB is unavailable in this browser."); + setAssetStatusMessage( + "Imported background images require project asset storage. IndexedDB is unavailable in this browser." + ); input.value = ""; return; } @@ -4604,7 +5744,10 @@ export function App({ store, initialStatusMessage }: AppProps) { let importedImageForCleanup: ImportedImageAssetResult | null = null; try { - const importedImage = await importBackgroundImageAssetFromFile(file, projectAssetStorage); + const importedImage = await importBackgroundImageAssetFromFile( + file, + projectAssetStorage + ); importedImageForCleanup = importedImage; store.executeCommand( @@ -4612,7 +5755,11 @@ export function App({ store, initialStatusMessage }: AppProps) { asset: importedImage.asset, world: { ...editorState.document.world, - background: changeWorldBackgroundMode(editorState.document.world.background, "image", importedImage.asset.id) + background: changeWorldBackgroundMode( + editorState.document.world.background, + "image", + importedImage.asset.id + ) }, label: `Import ${importedImage.asset.sourceName} as background` }) @@ -4627,10 +5774,14 @@ export function App({ store, initialStatusMessage }: AppProps) { [importedImage.asset.id]: importedImage.loadedAsset })); setAssetStatusMessage(null); - setStatusMessage(`Imported ${importedImage.asset.sourceName} and set it as the world background.`); + 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); + await projectAssetStorage + .deleteAsset(importedImageForCleanup.asset.storageKey) + .catch(() => undefined); disposeLoadedImageAsset(importedImageForCleanup.loadedAsset); } @@ -4642,7 +5793,9 @@ export function App({ store, initialStatusMessage }: AppProps) { } }; - const handleImportAudioChange = async (event: ChangeEvent) => { + const handleImportAudioChange = async ( + event: ChangeEvent + ) => { const input = event.currentTarget; const file = input.files?.[0]; @@ -4651,15 +5804,23 @@ export function App({ store, initialStatusMessage }: AppProps) { } if (projectAssetStorage === null) { - setAssetStatusMessage("Imported audio assets require project asset storage. IndexedDB is unavailable in this browser."); + setAssetStatusMessage( + "Imported audio assets require project asset storage. IndexedDB is unavailable in this browser." + ); input.value = ""; return; } - let importedAudioForCleanup: { asset: AudioAssetRecord; loadedAsset: LoadedAudioAsset } | null = null; + let importedAudioForCleanup: { + asset: AudioAssetRecord; + loadedAsset: LoadedAudioAsset; + } | null = null; try { - const importedAudio = await importAudioAssetFromFile(file, projectAssetStorage); + const importedAudio = await importAudioAssetFromFile( + file, + projectAssetStorage + ); importedAudioForCleanup = importedAudio; store.executeCommand( @@ -4678,10 +5839,14 @@ export function App({ store, initialStatusMessage }: AppProps) { [importedAudio.asset.id]: importedAudio.loadedAsset })); setAssetStatusMessage(null); - setStatusMessage(`Imported ${importedAudio.asset.sourceName} and registered it as an audio asset.`); + 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); + await projectAssetStorage + .deleteAsset(importedAudioForCleanup.asset.storageKey) + .catch(() => undefined); } const message = getErrorMessage(error); @@ -4693,13 +5858,19 @@ export function App({ store, initialStatusMessage }: AppProps) { }; const applyFaceMaterial = (materialId: string) => { - if (selectedBrush === null || selectedFaceId === null || selectedFace === null) { + 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.`); + setStatusMessage( + `${BOX_FACE_LABELS[selectedFaceId]} already uses that material.` + ); return; } @@ -4711,20 +5882,30 @@ export function App({ store, initialStatusMessage }: AppProps) { materialId }) ); - setStatusMessage(`Applied ${editorState.document.materials[materialId]?.name ?? materialId} to ${BOX_FACE_LABELS[selectedFaceId]}.`); + 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."); + 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.`); + setStatusMessage( + `${BOX_FACE_LABELS[selectedFaceId]} already uses the fallback face material.` + ); return; } @@ -4735,11 +5916,21 @@ export function App({ store, initialStatusMessage }: AppProps) { materialId: null }) ); - setStatusMessage(`Cleared the authored material on ${BOX_FACE_LABELS[selectedFaceId]}.`); + setStatusMessage( + `Cleared the authored material on ${BOX_FACE_LABELS[selectedFaceId]}.` + ); }; - const applyFaceUvState = (uvState: FaceUvState, label: string, successMessage: string) => { - if (selectedBrush === null || selectedFaceId === null || selectedFace === null) { + const applyFaceUvState = ( + uvState: FaceUvState, + label: string, + successMessage: string + ) => { + if ( + selectedBrush === null || + selectedFaceId === null || + selectedFace === null + ) { setStatusMessage("Select a single box face before editing UVs."); return; } @@ -4794,7 +5985,9 @@ export function App({ store, initialStatusMessage }: AppProps) { applyFaceUvState( { ...selectedFace.uv, - rotationQuarterTurns: rotateQuarterTurns(selectedFace.uv.rotationQuarterTurns) + rotationQuarterTurns: rotateQuarterTurns( + selectedFace.uv.rotationQuarterTurns + ) }, "Rotate face UV 90 degrees", "Rotated face UVs 90 degrees." @@ -4833,15 +6026,20 @@ export function App({ store, initialStatusMessage }: AppProps) { const handleEnterPlayMode = () => { if (blockingDiagnostics.length > 0) { - setStatusMessage(`Run mode blocked: ${formatSceneDiagnosticSummary(blockingDiagnostics)}`); + setStatusMessage( + `Run mode blocked: ${formatSceneDiagnosticSummary(blockingDiagnostics)}` + ); return; } try { - const nextRuntimeScene = buildRuntimeSceneFromDocument(editorState.document, { - navigationMode: preferredNavigationMode, - loadedModelAssets - }); + const nextRuntimeScene = buildRuntimeSceneFromDocument( + editorState.document, + { + navigationMode: preferredNavigationMode, + loadedModelAssets + } + ); const nextNavigationMode = preferredNavigationMode; setRuntimeScene(nextRuntimeScene); @@ -4873,24 +6071,38 @@ export function App({ store, initialStatusMessage }: AppProps) { setStatusMessage("Returned to editor mode."); }; - const handleSetPreferredNavigationMode = (navigationMode: RuntimeNavigationMode) => { + const handleSetPreferredNavigationMode = ( + navigationMode: RuntimeNavigationMode + ) => { setPreferredNavigationMode(navigationMode); if (navigationMode === "firstPerson" && primaryPlayerStart === null) { - setStatusMessage("First Person selected. Author a Player Start before running, or switch back to Orbit Visitor."); + 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."); + setStatusMessage( + navigationMode === "firstPerson" + ? "Runner switched to first-person navigation." + : "Runner switched to Orbit Visitor." + ); } }; - const createAssetMenuHoverHandler = (assetId: string) => (hovered: boolean) => { - setHoveredAssetId((current) => (hovered ? assetId : current === assetId ? null : current)); - }; + const createAssetMenuHoverHandler = + (assetId: string) => (hovered: boolean) => { + setHoveredAssetId((current) => + hovered ? assetId : current === assetId ? null : current + ); + }; - const createDisabledMenuAction = (label: string, testId: string): HierarchicalMenuItem => ({ + const createDisabledMenuAction = ( + label: string, + testId: string + ): HierarchicalMenuItem => ({ kind: "action", label, testId, @@ -4920,7 +6132,10 @@ export function App({ store, initialStatusMessage }: AppProps) { kind: "action", label: "Sound Emitter", testId: "add-menu-sound-emitter", - onSelect: () => beginEntityCreation("soundEmitter", { audioAssetId: audioAssetList[0]?.id ?? null }) + onSelect: () => + beginEntityCreation("soundEmitter", { + audioAssetId: audioAssetList[0]?.id ?? null + }) }, { kind: "action", @@ -4972,7 +6187,12 @@ export function App({ store, initialStatusMessage }: AppProps) { testId: "add-menu-assets-models", children: modelAssetList.length === 0 - ? [createDisabledMenuAction("No imported 3D models", "add-menu-assets-models-empty")] + ? [ + createDisabledMenuAction( + "No imported 3D models", + "add-menu-assets-models-empty" + ) + ] : modelAssetList.map((asset) => ({ kind: "action" as const, label: asset.sourceName, @@ -4987,7 +6207,12 @@ export function App({ store, initialStatusMessage }: AppProps) { testId: "add-menu-assets-environments", children: imageAssetList.length === 0 - ? [createDisabledMenuAction("No imported environments", "add-menu-assets-environments-empty")] + ? [ + createDisabledMenuAction( + "No imported environments", + "add-menu-assets-environments-empty" + ) + ] : imageAssetList.map((asset) => ({ kind: "action" as const, label: asset.sourceName, @@ -5002,12 +6227,20 @@ export function App({ store, initialStatusMessage }: AppProps) { testId: "add-menu-assets-audio", children: audioAssetList.length === 0 - ? [createDisabledMenuAction("No imported audio", "add-menu-assets-audio-empty")] + ? [ + createDisabledMenuAction( + "No imported audio", + "add-menu-assets-audio-empty" + ) + ] : audioAssetList.map((asset) => ({ kind: "action" as const, label: asset.sourceName, testId: `add-menu-audio-asset-${asset.id}`, - onSelect: () => beginEntityCreation("soundEmitter", { audioAssetId: asset.id }), + onSelect: () => + beginEntityCreation("soundEmitter", { + audioAssetId: asset.id + }), onHoverChange: createAssetMenuHoverHandler(asset.id) })) } @@ -5043,16 +6276,21 @@ export function App({ store, initialStatusMessage }: AppProps) { } ]; - const viewportPanelsStyle = layoutMode === "quad" ? createViewportQuadPanelsStyle(editorState.viewportQuadSplit) : undefined; + const viewportPanelsStyle = + layoutMode === "quad" + ? createViewportQuadPanelsStyle(editorState.viewportQuadSplit) + : undefined; if (editorState.toolMode === "play" && runtimeScene !== null) { return (
-
-
WebEditor3D
-
Slice 3.1 GLB/GLTF import and unified creation
+
+
WebEditor3D
+
+ Slice 3.1 GLB/GLTF import and unified creation
+
@@ -5075,7 +6313,12 @@ export function App({ store, initialStatusMessage }: AppProps) {
-
@@ -5102,21 +6345,39 @@ export function App({ store, initialStatusMessage }: AppProps) {
Navigation
-
{activeNavigationMode === "firstPerson" ? "First Person" : "Orbit Visitor"}
+
+ {activeNavigationMode === "firstPerson" + ? "First Person" + : "Orbit Visitor"} +
Spawn Source
-
{runtimeScene.spawn.source === "playerStart" ? "Player Start" : "Fallback"}
+
+ {runtimeScene.spawn.source === "playerStart" + ? "Player Start" + : "Fallback"} +
Pointer Lock
- {activeNavigationMode === "firstPerson" ? (firstPersonTelemetry?.pointerLocked ? "active" : "idle") : "not used"} + {activeNavigationMode === "firstPerson" + ? firstPersonTelemetry?.pointerLocked + ? "active" + : "idle" + : "not used"}
Grounded
-
{firstPersonTelemetry?.grounded ? "yes" : activeNavigationMode === "firstPerson" ? "no" : "n/a"}
+
+ {firstPersonTelemetry?.grounded + ? "yes" + : activeNavigationMode === "firstPerson" + ? "no" + : "n/a"} +
Locomotion
@@ -5132,31 +6393,59 @@ export function App({ store, initialStatusMessage }: AppProps) {
Water Volume
-
{activeNavigationMode === "firstPerson" ? (firstPersonTelemetry?.inWaterVolume ? "inside" : "outside") : "n/a"}
+
+ {activeNavigationMode === "firstPerson" + ? firstPersonTelemetry?.inWaterVolume + ? "inside" + : "outside" + : "n/a"} +
Fog Volume
-
{activeNavigationMode === "firstPerson" ? (firstPersonTelemetry?.inFogVolume ? "inside" : "outside") : "n/a"}
+
+ {activeNavigationMode === "firstPerson" + ? firstPersonTelemetry?.inFogVolume + ? "inside" + : "outside" + : "n/a"} +
FPS Feet Position
- {formatRunnerFeetPosition(firstPersonTelemetry?.feetPosition ?? runtimeScene.spawn.position)} + {formatRunnerFeetPosition( + firstPersonTelemetry?.feetPosition ?? + runtimeScene.spawn.position + )}
-
- Spawn: {runtimeScene.spawn.source === "playerStart" ? "Player Start" : "Fallback"} at{" "} - {formatRunnerFeetPosition(runtimeScene.spawn.position)} +
+ Spawn:{" "} + {runtimeScene.spawn.source === "playerStart" + ? "Player Start" + : "Fallback"}{" "} + at {formatRunnerFeetPosition(runtimeScene.spawn.position)}
Interaction
- {activeNavigationMode === "firstPerson" ? (runtimeInteractionPrompt === null ? "No target" : "Ready") : "Not available"} + {activeNavigationMode === "firstPerson" + ? runtimeInteractionPrompt === null + ? "No target" + : "Ready" + : "Not available"}
-
+
{activeNavigationMode === "firstPerson" ? runtimeInteractionPrompt === null ? "Aim at an authored Interactable and click when a prompt appears." @@ -5165,10 +6454,16 @@ export function App({ store, initialStatusMessage }: AppProps) {
- {runtimeMessage === null ? null :
{runtimeMessage}
} + {runtimeMessage === null ? null : ( +
{runtimeMessage}
+ )} {activeNavigationMode === "firstPerson" ? ( -
- Mouse click activates the current prompt target. Keyboard/controller fallback is not active yet. +
+ Mouse click activates the current prompt target. + Keyboard/controller fallback is not active yet.
) : null} @@ -5181,7 +6476,9 @@ export function App({ store, initialStatusMessage }: AppProps) {
Spawn:{" "} - {runtimeScene.spawn.source === "playerStart" ? "Authored Player Start" : "Fallback runtime spawn"} + {runtimeScene.spawn.source === "playerStart" + ? "Authored Player Start" + : "Fallback runtime spawn"}
@@ -5220,15 +6517,29 @@ export function App({ store, initialStatusMessage }: AppProps) { > Add - -
-
+
{VIEWPORT_LAYOUT_MODES.map((mode) => (
-
+
{WHITEBOX_SELECTION_MODES.map((mode) => (
- -
@@ -5379,14 +6729,23 @@ export function App({ store, initialStatusMessage }: AppProps) { )} {projectAssetStorageReady && projectAssetStorage === null ? ( -
Project asset storage is unavailable. Imported assets cannot be persisted.
+
+ Project asset storage is unavailable. Imported assets cannot be + persisted. +
) : null}
Whitebox Solids
{brushList.length === 0 ? ( -
Use Add > Whitebox Box and click in the viewport to create the first solid.
+
+ Use Add > Whitebox Box and click in the viewport to create + the first solid. +
) : ( -
+
{brushList.map((brush, brushIndex) => { const label = getBrushLabel(brush, brushIndex); const isSelected = selectedBrush?.id === brush.id; @@ -5404,7 +6763,9 @@ export function App({ store, initialStatusMessage }: AppProps) { type="text" value={brushNameDraft} placeholder={`Whitebox Box ${brushIndex + 1}`} - onChange={(event) => setBrushNameDraft(event.currentTarget.value)} + onChange={(event) => + setBrushNameDraft(event.currentTarget.value) + } onBlur={applyBrushNameChange} onFocus={(event) => event.currentTarget.select()} onKeyDown={(event) => @@ -5431,7 +6792,9 @@ export function App({ store, initialStatusMessage }: AppProps) { ) } > - {label} + + {label} + )} )} @@ -5524,12 +6907,16 @@ export function App({ store, initialStatusMessage }: AppProps) {
Entities
- {entityDisplayList.length === 0 ?
No entities authored yet.
: null} + {entityDisplayList.length === 0 ? ( +
No entities authored yet.
+ ) : null} {entityDisplayList.length === 0 ? null : (
{entityDisplayList.map(({ entity, label }) => { - const isSelected = editorState.selection.kind === "entities" && editorState.selection.ids.includes(entity.id); + const isSelected = + editorState.selection.kind === "entities" && + editorState.selection.ids.includes(entity.id); return (
setEntityNameDraft(event.currentTarget.value)} + onChange={(event) => + setEntityNameDraft(event.currentTarget.value) + } onBlur={applyEntityNameChange} onFocus={(event) => event.currentTarget.select()} onKeyDown={(event) => handleInlineNameInputKeyDown(event, () => { - setEntityNameDraft(selectedEntity?.name ?? ""); + setEntityNameDraft( + selectedEntity?.name ?? "" + ); }) } /> @@ -5571,7 +6962,9 @@ export function App({ store, initialStatusMessage }: AppProps) { ) } > - {label} + + {label} + )} + + +
+
+ + {editorState.document.world.background.mode === "image" && ( +
+
Environment Intensity
+
)} -
-
-
Background Mode
-
- - - -
-
+ {editorState.document.world.background.mode !== "image" && ( +
+
Background Colors
+ {editorState.document.world.background.mode === "solid" ? ( + + ) : ( +
+ + +
+ )} +
+ )} - {editorState.document.world.background.mode === "image" && (
-
Environment Intensity
- -
- )} - - {editorState.document.world.background.mode !== "image" && ( -
-
Background Colors
- {editorState.document.world.background.mode === "solid" ? ( +
Ambient Light
+
- ) : ( -
- - -
- )} -
- )} - -
-
Ambient Light
-
- - -
-
- -
-
Sun Light
-
- - + +
-
- - - +
+
Sun Light
+
+ + +
+ +
+ + + +
-
-
-
Advanced Rendering
- +
+
Advanced Rendering
+ - - {!advancedRendering.enabled ? null : ( - <> -
-
Shadows
- -
- +
+ + +
-
- -
- -
-
Ambient Occlusion
- -
- +
+ +
+
Ambient Occlusion
+ +
+ + +
+ +
+ +
+
Bloom
+ +
+ + +
-
- -
- -
-
Bloom
- -
- -
- -
-
-
Tone Mapping
-
- + +
+
+ +
+
Depth of Field
+ +
+ + +
-
-
- -
-
Depth of Field
- -
- - -
- -
- -
-
Volume Rendering Paths
-
- - -
- {advancedRendering.waterPath === "quality" ? ( - - ) : null} -
- - )} -
- - ) : ( - -
-
Selection
-
- {describeSelection(editorState.selection, brushList, editorState.document.modelInstances, editorState.document.assets, editorState.document.entities)} +
+ +
+
Volume Rendering Paths
+
+ + +
+ {advancedRendering.waterPath === "quality" ? ( + + ) : null} +
+ + )}
-
- - {selectedModelInstance !== null ? ( - <> -
-
Model Asset
-
{selectedModelAsset?.sourceName ?? "Missing Asset"}
-
- {selectedModelAssetRecord === null - ? "This model instance references an asset that is missing from the registry." - : formatModelAssetSummary(selectedModelAssetRecord)} -
- {selectedModelAssetRecord === null ? null : ( -
{formatModelBoundingBoxLabel(selectedModelAssetRecord)}
+ + ) : ( + +
+
Selection
+
+ {describeSelection( + editorState.selection, + brushList, + editorState.document.modelInstances, + editorState.document.assets, + editorState.document.entities )}
+
-
-
Position
-
- - - + {selectedModelInstance !== null ? ( + <> +
+
Model Asset
+
+ {selectedModelAsset?.sourceName ?? "Missing Asset"} +
+
+ {selectedModelAssetRecord === null + ? "This model instance references an asset that is missing from the registry." + : formatModelAssetSummary(selectedModelAssetRecord)} +
+ {selectedModelAssetRecord === null ? null : ( +
+ {formatModelBoundingBoxLabel(selectedModelAssetRecord)} +
+ )}
-
-
-
Rotation
-
- - - -
-
- -
-
Scale
-
- - - -
-
- -
-
Collision
- - -
{getModelInstanceCollisionModeDescription(selectedModelInstance.collision.mode)}
-
- - {selectedModelAssetRecord !== null && selectedModelAssetRecord.metadata.animationNames.length > 0 && (
-
Animation
+
Position
+
+ + + +
+
+ +
+
Rotation
+
+ + + +
+
+ +
+
Scale
+
+ + + +
+
+ +
+
Collision
+
+ {getModelInstanceCollisionModeDescription( + selectedModelInstance.collision.mode + )} +
- )} -
- -
- - ) : selectedEntity !== null ? ( - <> -
-
Entity Kind
-
{getEntityKindLabel(selectedEntity.kind)}
-
- -
-
Position
-
- - - -
-
- - {selectedPointLight !== null ? ( - <> -
-
Light
-
+ {selectedModelAssetRecord !== null && + selectedModelAssetRecord.metadata.animationNames.length > + 0 && ( +
+
Animation
- -
-
- -
-
Range
- -
- - ) : null} - - {selectedSpotLight !== null ? ( - <> -
-
Light
-
- - -
-
- -
-
Range
-
- - -
-
- -
-
Direction
-
- - - -
-
- - ) : null} - - {selectedPlayerStart !== null ? ( - <> -
-
Yaw
- -
- -
-
Player Collider
- - - - {playerStartColliderModeDraft === "capsule" ? ( -
- - -
- ) : null} - - {playerStartColliderModeDraft === "box" ? ( -
- - - -
- ) : null} - -
{getPlayerStartColliderModeDescription(playerStartColliderModeDraft)}
-
- - ) : null} - - {selectedSoundEmitter !== null ? ( - <> -
-
Audio Asset
-
-
- {selectedSoundEmitter.audioAssetId === null - ? "Unassigned" - : selectedSoundEmitterAudioAssetRecord?.sourceName ?? "Missing Audio Asset"} -
-
- {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)} -
-
- -
- -
-
Volume
- -
- -
-
Distance
-
- - -
-
- -
-
Playback
-
- - -
-
- - ) : null} - - {selectedTriggerVolume !== null ? ( - <> -
-
Size
-
- - - -
-
- - {renderInteractionLinksSection( - selectedTriggerVolume, - selectedTriggerVolumeLinks, - "add-trigger-teleport-link", - "add-trigger-visibility-link", - "add-trigger-play-sound-link", - "add-trigger-stop-sound-link" - )} - - ) : null} - - {selectedTeleportTarget !== null ? ( -
-
Yaw
- -
- ) : null} - - {selectedInteractable !== null ? ( - <> -
-
Interaction
-
- - -
-
Range defines how close the player must be before the click prompt can activate.
-
- -
-
Prompt
- + +
+ )} + +
+ +
+ + ) : selectedEntity !== null ? ( + <> +
+
Entity Kind
+
+ {getEntityKindLabel(selectedEntity.kind)} +
+
+ +
+
Position
+
+ + +
- - {renderInteractionLinksSection( - selectedInteractable, - selectedInteractableLinks, - "add-interactable-teleport-link", - "add-interactable-visibility-link", - "add-interactable-play-sound-link", - "add-interactable-stop-sound-link" - )} - - ) : null} - - ) : selectedBrush === null ? ( -
Select a whitebox solid or entity to edit authored properties.
- ) : ( - <> -
-
Whitebox Solid Type
-
box
-
- -
-
Selection Mode
-
{getWhiteboxSelectionModeLabel(whiteboxSelectionMode)}
-
- - {whiteboxSelectionMode !== "object" ? ( -
- {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."}
- ) : ( - <> -
-
Center
-
- - - -
-
-
-
Rotation
-
- - - -
-
- -
-
Size
-
- - - -
-
- -
-
Volume Mode
- - - {boxVolumeModeDraft === "water" ? ( - <> -
- - -
+ {selectedPointLight !== null ? ( + <> +
+
Light
+
- - - - ) : null} + +
+
- {boxVolumeModeDraft === "fog" ? ( - <> -
+
+
Range
+ +
+ + ) : null} + + {selectedSpotLight !== null ? ( + <> +
+
Light
+
+ + +
+
+ +
+
Range
+
+ + +
+
+ +
+
Direction
+
+ + + +
+
+ + ) : null} + + {selectedPlayerStart !== null ? ( + <> +
+
Yaw
+ +
+ +
+
Player Collider
+ + + + {playerStartColliderModeDraft === "capsule" ? ( +
+
+ ) : null} + + {playerStartColliderModeDraft === "box" ? ( +
+ + + +
+ ) : null} + +
+ {getPlayerStartColliderModeDescription( + playerStartColliderModeDraft + )} +
+
+ + ) : null} + + {selectedSoundEmitter !== null ? ( + <> +
+
Audio Asset
+
+
+ {selectedSoundEmitter.audioAssetId === null + ? "Unassigned" + : (selectedSoundEmitterAudioAssetRecord?.sourceName ?? + "Missing Audio Asset")} +
+
+ {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 + )} +
+
+ +
+ +
+
Volume
+ +
+ +
+
Distance
+
+ + +
+
+ +
+
Playback
+
+ + +
+
+ + ) : null} + + {selectedTriggerVolume !== null ? ( + <> +
+
Size
+
+ + + +
+
+ + {renderInteractionLinksSection( + selectedTriggerVolume, + selectedTriggerVolumeLinks, + "add-trigger-teleport-link", + "add-trigger-visibility-link", + "add-trigger-play-sound-link", + "add-trigger-stop-sound-link" + )} + + ) : null} + + {selectedTeleportTarget !== null ? ( +
+
Yaw
+ +
+ ) : null} + + {selectedInteractable !== null ? ( + <> +
+
Interaction
+
+ + +
+
+ Range defines how close the player must be before the + click prompt can activate. +
+
+ +
+
Prompt
+ +
+ + {renderInteractionLinksSection( + selectedInteractable, + selectedInteractableLinks, + "add-interactable-teleport-link", + "add-interactable-visibility-link", + "add-interactable-play-sound-link", + "add-interactable-stop-sound-link" + )} + + ) : null} + + ) : selectedBrush === null ? ( +
+ Select a whitebox solid or entity to edit authored properties. +
+ ) : ( + <> +
+
Whitebox Solid Type
+
box
+
+ +
+
Selection Mode
+
+ {getWhiteboxSelectionModeLabel(whiteboxSelectionMode)} +
+
+ + {whiteboxSelectionMode !== "object" ? ( +
+ {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."} +
+ ) : ( + <> +
+
Center
+
+ + + +
+
+ +
+
Rotation
+
+ + + +
+
+ +
+
Size
+
+ + + +
+
+ +
+
Volume Mode
+ + + {boxVolumeModeDraft === "water" ? ( + <> +
+ + +
+ + + + + ) : null} + + {boxVolumeModeDraft === "fog" ? ( + <> +
+ + +
+ -
+ + ) : null} +
+ + )} + +
+
Faces
+
+ {BOX_FACE_IDS.map((faceId) => ( + + ))} +
+
+ + {whiteboxSelectionMode === "edge" ? ( + selectedEdgeId === null ? ( +
+ Select an edge in the viewport to inspect it. Edge + transforms land in the next slice. +
+ ) : ( +
+
Active Edge
+
+ {BOX_EDGE_LABELS[selectedEdgeId]} +
+
+ Edge selection is visible in the viewport. Persistent + edge transforms are still deferred. +
+
+ ) + ) : whiteboxSelectionMode === "vertex" ? ( + selectedVertexId === null ? ( +
+ Select a vertex in the viewport to inspect it. Vertex + transforms land in the next slice. +
+ ) : ( +
+
Active Vertex
+
+ {BOX_VERTEX_LABELS[selectedVertexId]} +
+
+ Vertex selection is visible in the viewport. + Persistent vertex transforms are still deferred. +
+
+ ) + ) : whiteboxSelectionMode !== "face" ? ( +
+ Switch to Face mode or choose a face chip to edit + materials and UVs. +
+ ) : selectedFace === null || selectedFaceId === null ? ( +
+ Select a face to edit its material and UV transform. +
+ ) : ( + <> +
+
Active Face
+
+ {BOX_FACE_LABELS[selectedFaceId]} +
+
+ Material:{" "} + {selectedFaceMaterial?.name ?? "Fallback face color"} +
+
+ +
+
Material
+
+ {materialList.map((material) => ( + + ))} +
+
+ +
+
+ +
+
UV Offset
+
- - ) : null} -
- - )} - -
-
Faces
-
- {BOX_FACE_IDS.map((faceId) => ( - - ))} -
-
- - {whiteboxSelectionMode === "edge" ? ( - selectedEdgeId === null ? ( -
Select an edge in the viewport to inspect it. Edge transforms land in the next slice.
- ) : ( -
-
Active Edge
-
{BOX_EDGE_LABELS[selectedEdgeId]}
-
Edge selection is visible in the viewport. Persistent edge transforms are still deferred.
-
- ) - ) : whiteboxSelectionMode === "vertex" ? ( - selectedVertexId === null ? ( -
Select a vertex in the viewport to inspect it. Vertex transforms land in the next slice.
- ) : ( -
-
Active Vertex
-
{BOX_VERTEX_LABELS[selectedVertexId]}
-
Vertex selection is visible in the viewport. Persistent vertex transforms are still deferred.
-
- ) - ) : whiteboxSelectionMode !== "face" ? ( -
Switch to Face mode or choose a face chip to edit materials and UVs.
- ) : selectedFace === null || selectedFaceId === null ? ( -
- Select a face to edit its material and UV transform. -
- ) : ( - <> -
-
Active Face
-
{BOX_FACE_LABELS[selectedFaceId]}
-
- Material: {selectedFaceMaterial?.name ?? "Fallback face color"} + +
-
-
-
Material
-
- {materialList.map((material) => ( - - ))} +
+
UV Scale
+
+ + +
+
- + + + +
-
-
-
UV Offset
-
- - +
+
UV Flags
+
+ Rotation {selectedFace.uv.rotationQuarterTurns * 90}° +
+
+ U {selectedFace.uv.flipU ? "flipped" : "normal"} · V{" "} + {selectedFace.uv.flipV ? "flipped" : "normal"} +
-
- -
-
UV Scale
-
- - -
-
- -
- - - - - -
- -
-
UV Flags
-
Rotation {selectedFace.uv.rotationQuarterTurns * 90}°
-
- U {selectedFace.uv.flipU ? "flipped" : "normal"} · V {selectedFace.uv.flipV ? "flipped" : "normal"} -
-
- - )} - - )} - + + )} + + )} + )}
{addMenuPosition === null ? null : ( - + )}