diff --git a/src/app/App.tsx b/src/app/App.tsx index 603d8ebd..c84a700d 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -221,7 +221,7 @@ import { createTriggerVolumeEntity, getEntityInstances, getEntityKindLabel, - getPrimaryPlayerStartEntity, + getPrimaryEnabledPlayerStartEntity, normalizeEntityName, normalizeYawDegrees, normalizeInteractablePrompt, @@ -899,6 +899,14 @@ function formatAudioAssetSummary(asset: AudioAssetRecord): string { return details.join(" | "); } +function formatProjectAssetSummary(asset: ProjectAssetRecord): string { + return asset.kind === "model" + ? formatModelAssetSummary(asset) + : asset.kind === "image" + ? formatImageAssetSummary(asset) + : formatAudioAssetSummary(asset); +} + function formatAssetHoverStatus(asset: ProjectAssetRecord): string { const details = [ `${getProjectAssetKindLabel(asset.kind)} asset`, @@ -926,6 +934,23 @@ function getBrushLabel(brush: BoxBrush, index: number): string { return brush.name ?? `Whitebox Box ${index + 1}`; } +function formatAuthoredObjectStateSummary(state: { + visible: boolean; + enabled: boolean; +}): string | null { + const parts: string[] = []; + + if (!state.enabled) { + parts.push("Disabled"); + } + + if (!state.visible) { + parts.push("Hidden"); + } + + return parts.length === 0 ? null : parts.join(" | "); +} + function getBrushLabelById(brushId: string, brushes: BoxBrush[]): string { const brushIndex = brushes.findIndex((brush) => brush.id === brushId); return brushIndex === -1 @@ -1490,7 +1515,7 @@ export function App({ store, initialStatusMessage }: AppProps) { editorState.document.entities, editorState.document.assets ); - const primaryPlayerStart = getPrimaryPlayerStartEntity( + const primaryPlayerStart = getPrimaryEnabledPlayerStartEntity( editorState.document.entities ); const materialList = sortDocumentMaterials(editorState.document.materials); @@ -1550,6 +1575,11 @@ export function App({ store, initialStatusMessage }: AppProps) { const selectedSceneExit = selectedEntity?.kind === "sceneExit" ? selectedEntity : null; const projectAssetList = Object.values(editorState.document.assets); + const projectAssetDisplayList = [...projectAssetList].sort( + (left, right) => + left.kind.localeCompare(right.kind) || + left.sourceName.localeCompare(right.sourceName) + ); const modelAssetList = projectAssetList.filter(isModelAsset); const imageAssetList = projectAssetList.filter(isImageAsset); const audioAssetList = projectAssetList.filter(isAudioAsset); @@ -5146,6 +5176,131 @@ export function App({ store, initialStatusMessage }: AppProps) { `Delete ${label}?\n\nThis can be undone with Undo.` ); + const confirmDeleteProjectAsset = (label: string) => + globalThis.window.confirm( + `Delete ${label} and remove its project-wide references?\n\nThis also deletes the stored binary asset data, and it can be undone with Undo.` + ); + + const handleSetBrushVisible = (brush: BoxBrush, visible: boolean) => { + try { + store.executeCommand( + createSetBoxBrushAuthoredStateCommand({ + brushId: brush.id, + visible + }) + ); + setStatusMessage( + visible + ? `Shown ${getBrushLabelById(brush.id, brushList)} in the editor and runner.` + : `Hidden ${getBrushLabelById(brush.id, brushList)} in the editor and runner.` + ); + } catch (error) { + setStatusMessage(getErrorMessage(error)); + } + }; + + const handleSetBrushEnabled = (brush: BoxBrush, enabled: boolean) => { + try { + store.executeCommand( + createSetBoxBrushAuthoredStateCommand({ + brushId: brush.id, + enabled + }) + ); + setStatusMessage( + enabled + ? `Enabled ${getBrushLabelById(brush.id, brushList)}.` + : `Disabled ${getBrushLabelById(brush.id, brushList)}.` + ); + } catch (error) { + setStatusMessage(getErrorMessage(error)); + } + }; + + const handleSetModelInstanceVisible = ( + modelInstance: ModelInstance, + visible: boolean + ) => { + try { + store.executeCommand( + createSetModelInstanceAuthoredStateCommand({ + modelInstanceId: modelInstance.id, + visible + }) + ); + setStatusMessage( + visible + ? `Shown ${getModelInstanceDisplayLabelById(modelInstance.id, editorState.document.modelInstances, editorState.document.assets)} in the editor and runner.` + : `Hidden ${getModelInstanceDisplayLabelById(modelInstance.id, editorState.document.modelInstances, editorState.document.assets)} in the editor and runner.` + ); + } catch (error) { + setStatusMessage(getErrorMessage(error)); + } + }; + + const handleSetModelInstanceEnabled = ( + modelInstance: ModelInstance, + enabled: boolean + ) => { + try { + store.executeCommand( + createSetModelInstanceAuthoredStateCommand({ + modelInstanceId: modelInstance.id, + enabled + }) + ); + setStatusMessage( + enabled + ? `Enabled ${getModelInstanceDisplayLabelById(modelInstance.id, editorState.document.modelInstances, editorState.document.assets)}.` + : `Disabled ${getModelInstanceDisplayLabelById(modelInstance.id, editorState.document.modelInstances, editorState.document.assets)}.` + ); + } catch (error) { + setStatusMessage(getErrorMessage(error)); + } + }; + + const handleSetEntityVisible = ( + entity: EntityInstance, + visible: boolean + ) => { + try { + store.executeCommand( + createSetEntityAuthoredStateCommand({ + entityId: entity.id, + visible + }) + ); + setStatusMessage( + visible + ? `Shown ${getEntityDisplayLabelById(entity.id, editorState.document.entities, editorState.document.assets)} in the editor and runner.` + : `Hidden ${getEntityDisplayLabelById(entity.id, editorState.document.entities, editorState.document.assets)} in the editor and runner.` + ); + } catch (error) { + setStatusMessage(getErrorMessage(error)); + } + }; + + const handleSetEntityEnabled = ( + entity: EntityInstance, + enabled: boolean + ) => { + try { + store.executeCommand( + createSetEntityAuthoredStateCommand({ + entityId: entity.id, + enabled + }) + ); + setStatusMessage( + enabled + ? `Enabled ${getEntityDisplayLabelById(entity.id, editorState.document.entities, editorState.document.assets)}.` + : `Disabled ${getEntityDisplayLabelById(entity.id, editorState.document.entities, editorState.document.assets)}.` + ); + } catch (error) { + setStatusMessage(getErrorMessage(error)); + } + }; + const handleDeleteBrush = (brushId: string) => { const label = getBrushLabelById(brushId, brushList); @@ -5205,6 +5360,21 @@ export function App({ store, initialStatusMessage }: AppProps) { } }; + const handleDeleteProjectAsset = (asset: ProjectAssetRecord) => { + if (!confirmDeleteProjectAsset(asset.sourceName)) { + return false; + } + + try { + store.executeCommand(createDeleteProjectAssetCommand(asset.id)); + setStatusMessage(`Deleted ${asset.sourceName} and cleaned up project references.`); + return true; + } catch (error) { + setStatusMessage(getErrorMessage(error)); + return false; + } + }; + const handleDeleteSelectedSceneItem = () => { const selectedBrushId = getSingleSelectedBrushId(editorState.selection); @@ -7963,13 +8133,29 @@ export function App({ store, initialStatusMessage }: AppProps) { {brushList.map((brush, brushIndex) => { const label = getBrushLabel(brush, brushIndex); const isSelected = selectedBrush?.id === brush.id; + const authoredStateSummary = formatAuthoredObjectStateSummary( + brush + ); return (