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 (
+ + handleSetBrushEnabled( + brush, + event.currentTarget.checked + ) + } + /> {isSelected ? (
+ {authoredStateSummary === null ? null : ( +
+ {authoredStateSummary} +
+ )}
); })} @@ -8043,13 +8234,29 @@ export function App({ store, initialStatusMessage }: AppProps) { const isSelected = editorState.selection.kind === "modelInstances" && editorState.selection.ids.includes(modelInstance.id); + const authoredStateSummary = formatAuthoredObjectStateSummary( + modelInstance + ); return (
+ + handleSetModelInstanceEnabled( + modelInstance, + event.currentTarget.checked + ) + } + /> {isSelected ? (
+ {authoredStateSummary === null ? null : ( +
+ {authoredStateSummary} +
+ )}
); })} @@ -8131,13 +8343,29 @@ export function App({ store, initialStatusMessage }: AppProps) { const isSelected = editorState.selection.kind === "entities" && editorState.selection.ids.includes(entity.id); + const authoredStateSummary = formatAuthoredObjectStateSummary( + entity + ); return (
+ + handleSetEntityEnabled( + entity, + event.currentTarget.checked + ) + } + /> {isSelected ? (
+ {authoredStateSummary === null ? null : ( +
+ {authoredStateSummary} +
+ )}
); })} )} + +
+
Imported Assets
+ + {projectAssetDisplayList.length === 0 ? ( +
+ No imported assets are stored in this project. +
+ ) : ( +
+ {projectAssetDisplayList.map((asset) => ( +
+
+
+ + {asset.sourceName} + + + {getProjectAssetKindLabel(asset.kind)} + +
+ +
+
+ {formatProjectAssetSummary(asset)} +
+
+ ))} +
+ )} +
@@ -9516,6 +9794,43 @@ export function App({ store, initialStatusMessage }: AppProps) { )} +
+
Authored State
+ + +
+ Hidden model instances stay authored and keep their runtime + collision behavior. Disabled instances are removed from the + editor viewport, picking, and runtime build. +
+
+
Position
@@ -10003,6 +10318,43 @@ export function App({ store, initialStatusMessage }: AppProps) {
+
+
Authored State
+ + +
+ Hidden entities stay active for authored runtime behavior. + Disable them to remove them from the editor viewport, + picking, and runtime build. +
+
+
Position
@@ -12087,7 +12439,7 @@ export function App({ store, initialStatusMessage }: AppProps) { />
+
+
Authored State
+ + +
+ Hidden solids keep their collision and volume behavior. + Disable them to remove them from the editor viewport, + picking, and runtime build. +
+
+ {whiteboxSelectionMode !== "object" ? (
{whiteboxSelectionMode === "face" diff --git a/src/app/app.css b/src/app/app.css index 08eaeae8..b119bcfc 100644 --- a/src/app/app.css +++ b/src/app/app.css @@ -486,6 +486,14 @@ button:disabled { border-color: rgba(207, 123, 66, 0.55); } +.outliner-item--disabled { + border-style: dashed; +} + +.outliner-item--disabled:not(.outliner-item--selected) { + background: rgba(255, 255, 255, 0.02); +} + .outliner-item--compact { gap: 6px; padding: 8px 10px; @@ -499,6 +507,14 @@ button:disabled { min-width: 0; } +.outliner-item__toggle { + flex: 0 0 auto; + width: 16px; + height: 16px; + margin: 0; + accent-color: var(--color-accent); +} + .outliner-item__title { font-size: 0.88rem; font-weight: 700;