Add functionality for deleting project assets and related commands
This commit is contained in:
157
src/assets/delete-project-asset.ts
Normal file
157
src/assets/delete-project-asset.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import type { ProjectDocument, ProjectScene } from "../document/scene-document";
|
||||
import { createDefaultWorldSettings } from "../document/world-settings";
|
||||
import type { InteractionLink } from "../interactions/interaction-links";
|
||||
|
||||
import type { ProjectAssetRecord } from "./project-assets";
|
||||
|
||||
function removeInvalidatedInteractionLinks(
|
||||
interactionLinks: ProjectScene["interactionLinks"],
|
||||
removedModelInstanceIds: ReadonlySet<string>,
|
||||
silencedSoundEmitterIds: ReadonlySet<string>
|
||||
): ProjectScene["interactionLinks"] {
|
||||
let didChange = false;
|
||||
const nextInteractionLinks: ProjectScene["interactionLinks"] = {};
|
||||
|
||||
for (const [linkId, link] of Object.entries(interactionLinks)) {
|
||||
const shouldRemove = (() => {
|
||||
switch (link.action.type) {
|
||||
case "playAnimation":
|
||||
case "stopAnimation":
|
||||
return removedModelInstanceIds.has(link.action.targetModelInstanceId);
|
||||
case "playSound":
|
||||
case "stopSound":
|
||||
return silencedSoundEmitterIds.has(link.action.targetSoundEmitterId);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
})();
|
||||
|
||||
if (shouldRemove) {
|
||||
didChange = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
nextInteractionLinks[linkId] = link;
|
||||
}
|
||||
|
||||
return didChange ? nextInteractionLinks : interactionLinks;
|
||||
}
|
||||
|
||||
function cleanupSceneForDeletedAsset(
|
||||
scene: ProjectScene,
|
||||
asset: ProjectAssetRecord
|
||||
): ProjectScene {
|
||||
let nextWorld = scene.world;
|
||||
let nextModelInstances = scene.modelInstances;
|
||||
let nextEntities = scene.entities;
|
||||
const removedModelInstanceIds = new Set<string>();
|
||||
const silencedSoundEmitterIds = new Set<string>();
|
||||
|
||||
if (
|
||||
asset.kind === "image" &&
|
||||
scene.world.background.mode === "image" &&
|
||||
scene.world.background.assetId === asset.id
|
||||
) {
|
||||
nextWorld = {
|
||||
...scene.world,
|
||||
background: createDefaultWorldSettings().background
|
||||
};
|
||||
}
|
||||
|
||||
if (asset.kind === "model") {
|
||||
const remainingModelInstances: ProjectScene["modelInstances"] = {};
|
||||
|
||||
for (const [modelInstanceId, modelInstance] of Object.entries(
|
||||
scene.modelInstances
|
||||
)) {
|
||||
if (modelInstance.assetId === asset.id) {
|
||||
removedModelInstanceIds.add(modelInstanceId);
|
||||
continue;
|
||||
}
|
||||
|
||||
remainingModelInstances[modelInstanceId] = modelInstance;
|
||||
}
|
||||
|
||||
if (removedModelInstanceIds.size > 0) {
|
||||
nextModelInstances = remainingModelInstances;
|
||||
}
|
||||
}
|
||||
|
||||
if (asset.kind === "audio") {
|
||||
const updatedEntities: ProjectScene["entities"] = {};
|
||||
let didChangeEntities = false;
|
||||
|
||||
for (const [entityId, entity] of Object.entries(scene.entities)) {
|
||||
if (entity.kind === "soundEmitter" && entity.audioAssetId === asset.id) {
|
||||
silencedSoundEmitterIds.add(entityId);
|
||||
updatedEntities[entityId] = {
|
||||
...entity,
|
||||
audioAssetId: null,
|
||||
autoplay: false
|
||||
};
|
||||
didChangeEntities = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
updatedEntities[entityId] = entity;
|
||||
}
|
||||
|
||||
if (didChangeEntities) {
|
||||
nextEntities = updatedEntities;
|
||||
}
|
||||
}
|
||||
|
||||
const nextInteractionLinks = removeInvalidatedInteractionLinks(
|
||||
scene.interactionLinks,
|
||||
removedModelInstanceIds,
|
||||
silencedSoundEmitterIds
|
||||
);
|
||||
|
||||
if (
|
||||
nextWorld === scene.world &&
|
||||
nextModelInstances === scene.modelInstances &&
|
||||
nextEntities === scene.entities &&
|
||||
nextInteractionLinks === scene.interactionLinks
|
||||
) {
|
||||
return scene;
|
||||
}
|
||||
|
||||
return {
|
||||
...scene,
|
||||
world: nextWorld,
|
||||
modelInstances: nextModelInstances,
|
||||
entities: nextEntities,
|
||||
interactionLinks: nextInteractionLinks
|
||||
};
|
||||
}
|
||||
|
||||
export function deleteProjectAssetFromProjectDocument(
|
||||
projectDocument: ProjectDocument,
|
||||
assetId: string
|
||||
): ProjectDocument {
|
||||
const asset = projectDocument.assets[assetId];
|
||||
|
||||
if (asset === undefined) {
|
||||
throw new Error(`Project asset ${assetId} does not exist.`);
|
||||
}
|
||||
|
||||
const nextAssets = {
|
||||
...projectDocument.assets
|
||||
};
|
||||
delete nextAssets[assetId];
|
||||
|
||||
const nextScenes: ProjectDocument["scenes"] = {};
|
||||
let didChangeScenes = false;
|
||||
|
||||
for (const [sceneId, scene] of Object.entries(projectDocument.scenes)) {
|
||||
const nextScene = cleanupSceneForDeletedAsset(scene, asset);
|
||||
nextScenes[sceneId] = nextScene;
|
||||
didChangeScenes ||= nextScene !== scene;
|
||||
}
|
||||
|
||||
return {
|
||||
...projectDocument,
|
||||
assets: nextAssets,
|
||||
scenes: didChangeScenes ? nextScenes : projectDocument.scenes
|
||||
};
|
||||
}
|
||||
83
src/commands/delete-project-asset-command.ts
Normal file
83
src/commands/delete-project-asset-command.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
import { cloneEditorSelection, type EditorSelection } from "../core/selection";
|
||||
import type { ToolMode } from "../core/tool-mode";
|
||||
import { deleteProjectAssetFromProjectDocument } from "../assets/delete-project-asset";
|
||||
|
||||
import type { EditorCommand } from "./command";
|
||||
|
||||
function selectionTargetsMissingObject(
|
||||
selection: EditorSelection,
|
||||
activeScene: ReturnType<EditorCommand["execute"]> extends never ? never : { modelInstances: Record<string, unknown>; entities: Record<string, unknown> }
|
||||
): boolean {
|
||||
if (selection.kind === "modelInstances") {
|
||||
return selection.ids.some((id) => activeScene.modelInstances[id] === undefined);
|
||||
}
|
||||
|
||||
if (selection.kind === "entities") {
|
||||
return selection.ids.some((id) => activeScene.entities[id] === undefined);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function createDeleteProjectAssetCommand(assetId: string): EditorCommand {
|
||||
let previousSelection: EditorSelection | null = null;
|
||||
let previousToolMode: ToolMode | null = null;
|
||||
let previousProjectDocument = null;
|
||||
let assetLabel: string | null = null;
|
||||
|
||||
return {
|
||||
id: createOpaqueId("command"),
|
||||
label: "Delete project asset",
|
||||
execute(context) {
|
||||
const currentProjectDocument = context.getProjectDocument();
|
||||
const currentAsset = currentProjectDocument.assets[assetId];
|
||||
|
||||
if (currentAsset === undefined) {
|
||||
throw new Error(`Project asset ${assetId} does not exist.`);
|
||||
}
|
||||
|
||||
if (previousProjectDocument === null) {
|
||||
previousProjectDocument = currentProjectDocument;
|
||||
}
|
||||
|
||||
if (previousSelection === null) {
|
||||
previousSelection = cloneEditorSelection(context.getSelection());
|
||||
}
|
||||
|
||||
if (previousToolMode === null) {
|
||||
previousToolMode = context.getToolMode();
|
||||
}
|
||||
|
||||
assetLabel = currentAsset.sourceName;
|
||||
const nextProjectDocument = deleteProjectAssetFromProjectDocument(
|
||||
currentProjectDocument,
|
||||
assetId
|
||||
);
|
||||
context.setProjectDocument(nextProjectDocument);
|
||||
|
||||
const activeScene = nextProjectDocument.scenes[nextProjectDocument.activeSceneId];
|
||||
|
||||
if (selectionTargetsMissingObject(context.getSelection(), activeScene)) {
|
||||
context.setSelection({ kind: "none" });
|
||||
}
|
||||
|
||||
context.setToolMode("select");
|
||||
},
|
||||
undo(context) {
|
||||
if (previousProjectDocument === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.setProjectDocument(previousProjectDocument);
|
||||
|
||||
if (previousSelection !== null) {
|
||||
context.setSelection(previousSelection);
|
||||
}
|
||||
|
||||
if (previousToolMode !== null) {
|
||||
context.setToolMode(previousToolMode);
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
74
src/commands/set-box-brush-authored-state-command.ts
Normal file
74
src/commands/set-box-brush-authored-state-command.ts
Normal file
@@ -0,0 +1,74 @@
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
|
||||
import { getBoxBrushOrThrow, replaceBrush } from "./brush-command-helpers";
|
||||
import type { EditorCommand } from "./command";
|
||||
|
||||
interface SetBoxBrushAuthoredStateCommandOptions {
|
||||
brushId: string;
|
||||
visible?: boolean;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
function createCommandLabel(options: SetBoxBrushAuthoredStateCommandOptions): string {
|
||||
if (options.enabled !== undefined && options.visible === undefined) {
|
||||
return options.enabled ? "Enable box brush" : "Disable box brush";
|
||||
}
|
||||
|
||||
if (options.visible !== undefined && options.enabled === undefined) {
|
||||
return options.visible ? "Show box brush" : "Hide box brush";
|
||||
}
|
||||
|
||||
return "Update box brush state";
|
||||
}
|
||||
|
||||
export function createSetBoxBrushAuthoredStateCommand(
|
||||
options: SetBoxBrushAuthoredStateCommandOptions
|
||||
): EditorCommand {
|
||||
if (options.visible === undefined && options.enabled === undefined) {
|
||||
throw new Error("Box brush authored state command requires at least one change.");
|
||||
}
|
||||
|
||||
let previousVisible: boolean | null = null;
|
||||
let previousEnabled: boolean | null = null;
|
||||
|
||||
return {
|
||||
id: createOpaqueId("command"),
|
||||
label: createCommandLabel(options),
|
||||
execute(context) {
|
||||
const currentDocument = context.getDocument();
|
||||
const brush = getBoxBrushOrThrow(currentDocument, options.brushId);
|
||||
|
||||
if (previousVisible === null) {
|
||||
previousVisible = brush.visible;
|
||||
}
|
||||
|
||||
if (previousEnabled === null) {
|
||||
previousEnabled = brush.enabled;
|
||||
}
|
||||
|
||||
context.setDocument(
|
||||
replaceBrush(currentDocument, {
|
||||
...brush,
|
||||
visible: options.visible ?? brush.visible,
|
||||
enabled: options.enabled ?? brush.enabled
|
||||
})
|
||||
);
|
||||
},
|
||||
undo(context) {
|
||||
if (previousVisible === null || previousEnabled === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDocument = context.getDocument();
|
||||
const brush = getBoxBrushOrThrow(currentDocument, options.brushId);
|
||||
|
||||
context.setDocument(
|
||||
replaceBrush(currentDocument, {
|
||||
...brush,
|
||||
visible: previousVisible,
|
||||
enabled: previousEnabled
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
90
src/commands/set-entity-authored-state-command.ts
Normal file
90
src/commands/set-entity-authored-state-command.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
import { cloneEntityInstance } from "../entities/entity-instances";
|
||||
|
||||
import type { EditorCommand } from "./command";
|
||||
|
||||
interface SetEntityAuthoredStateCommandOptions {
|
||||
entityId: string;
|
||||
visible?: boolean;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
function createCommandLabel(options: SetEntityAuthoredStateCommandOptions): string {
|
||||
if (options.enabled !== undefined && options.visible === undefined) {
|
||||
return options.enabled ? "Enable entity" : "Disable entity";
|
||||
}
|
||||
|
||||
if (options.visible !== undefined && options.enabled === undefined) {
|
||||
return options.visible ? "Show entity" : "Hide entity";
|
||||
}
|
||||
|
||||
return "Update entity state";
|
||||
}
|
||||
|
||||
export function createSetEntityAuthoredStateCommand(
|
||||
options: SetEntityAuthoredStateCommandOptions
|
||||
): EditorCommand {
|
||||
if (options.visible === undefined && options.enabled === undefined) {
|
||||
throw new Error("Entity authored state command requires at least one change.");
|
||||
}
|
||||
|
||||
let previousVisible: boolean | null = null;
|
||||
let previousEnabled: boolean | null = null;
|
||||
|
||||
return {
|
||||
id: createOpaqueId("command"),
|
||||
label: createCommandLabel(options),
|
||||
execute(context) {
|
||||
const currentDocument = context.getDocument();
|
||||
const entity = currentDocument.entities[options.entityId];
|
||||
|
||||
if (entity === undefined) {
|
||||
throw new Error(`Entity ${options.entityId} does not exist.`);
|
||||
}
|
||||
|
||||
if (previousVisible === null) {
|
||||
previousVisible = entity.visible;
|
||||
}
|
||||
|
||||
if (previousEnabled === null) {
|
||||
previousEnabled = entity.enabled;
|
||||
}
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
entities: {
|
||||
...currentDocument.entities,
|
||||
[entity.id]: cloneEntityInstance({
|
||||
...entity,
|
||||
visible: options.visible ?? entity.visible,
|
||||
enabled: options.enabled ?? entity.enabled
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
undo(context) {
|
||||
if (previousVisible === null || previousEnabled === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDocument = context.getDocument();
|
||||
const entity = currentDocument.entities[options.entityId];
|
||||
|
||||
if (entity === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
entities: {
|
||||
...currentDocument.entities,
|
||||
[entity.id]: cloneEntityInstance({
|
||||
...entity,
|
||||
visible: previousVisible,
|
||||
enabled: previousEnabled
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
92
src/commands/set-model-instance-authored-state-command.ts
Normal file
92
src/commands/set-model-instance-authored-state-command.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import { createOpaqueId } from "../core/ids";
|
||||
import { cloneModelInstance } from "../assets/model-instances";
|
||||
|
||||
import type { EditorCommand } from "./command";
|
||||
|
||||
interface SetModelInstanceAuthoredStateCommandOptions {
|
||||
modelInstanceId: string;
|
||||
visible?: boolean;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
function createCommandLabel(
|
||||
options: SetModelInstanceAuthoredStateCommandOptions
|
||||
): string {
|
||||
if (options.enabled !== undefined && options.visible === undefined) {
|
||||
return options.enabled ? "Enable model instance" : "Disable model instance";
|
||||
}
|
||||
|
||||
if (options.visible !== undefined && options.enabled === undefined) {
|
||||
return options.visible ? "Show model instance" : "Hide model instance";
|
||||
}
|
||||
|
||||
return "Update model instance state";
|
||||
}
|
||||
|
||||
export function createSetModelInstanceAuthoredStateCommand(
|
||||
options: SetModelInstanceAuthoredStateCommandOptions
|
||||
): EditorCommand {
|
||||
if (options.visible === undefined && options.enabled === undefined) {
|
||||
throw new Error("Model instance authored state command requires at least one change.");
|
||||
}
|
||||
|
||||
let previousVisible: boolean | null = null;
|
||||
let previousEnabled: boolean | null = null;
|
||||
|
||||
return {
|
||||
id: createOpaqueId("command"),
|
||||
label: createCommandLabel(options),
|
||||
execute(context) {
|
||||
const currentDocument = context.getDocument();
|
||||
const modelInstance = currentDocument.modelInstances[options.modelInstanceId];
|
||||
|
||||
if (modelInstance === undefined) {
|
||||
throw new Error(`Model instance ${options.modelInstanceId} does not exist.`);
|
||||
}
|
||||
|
||||
if (previousVisible === null) {
|
||||
previousVisible = modelInstance.visible;
|
||||
}
|
||||
|
||||
if (previousEnabled === null) {
|
||||
previousEnabled = modelInstance.enabled;
|
||||
}
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
modelInstances: {
|
||||
...currentDocument.modelInstances,
|
||||
[modelInstance.id]: cloneModelInstance({
|
||||
...modelInstance,
|
||||
visible: options.visible ?? modelInstance.visible,
|
||||
enabled: options.enabled ?? modelInstance.enabled
|
||||
})
|
||||
}
|
||||
});
|
||||
},
|
||||
undo(context) {
|
||||
if (previousVisible === null || previousEnabled === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentDocument = context.getDocument();
|
||||
const modelInstance = currentDocument.modelInstances[options.modelInstanceId];
|
||||
|
||||
if (modelInstance === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
context.setDocument({
|
||||
...currentDocument,
|
||||
modelInstances: {
|
||||
...currentDocument.modelInstances,
|
||||
[modelInstance.id]: cloneModelInstance({
|
||||
...modelInstance,
|
||||
visible: previousVisible,
|
||||
enabled: previousEnabled
|
||||
})
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user