Files
webeditor3d/src/assets/delete-project-asset.ts

311 lines
8.6 KiB
TypeScript

import type { ProjectDocument, ProjectScene } from "../document/scene-document";
import { createDefaultWorldSettings } from "../document/world-settings";
import { foliagePrototypeReferencesProjectAsset } from "../foliage/foliage";
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);
case "control":
switch (link.action.effect.type) {
case "setProjectTimePaused":
case "activateCameraRigOverride":
case "clearCameraRigOverride":
case "setActorPresence":
case "playActorAnimation":
case "followActorPath":
return false;
case "playModelAnimation":
case "stopModelAnimation":
case "setModelInstanceVisible":
return removedModelInstanceIds.has(
link.action.effect.target.modelInstanceId
);
case "playSound":
case "stopSound":
return silencedSoundEmitterIds.has(
link.action.effect.target.entityId
);
case "setSoundVolume":
case "setInteractionEnabled":
case "setLightEnabled":
case "setLightIntensity":
case "setLightColor":
case "setAmbientLightIntensity":
case "setAmbientLightColor":
case "setSunLightIntensity":
case "setSunLightColor":
return false;
}
default:
return false;
}
})();
if (shouldRemove) {
didChange = true;
continue;
}
nextInteractionLinks[linkId] = link;
}
return didChange ? nextInteractionLinks : interactionLinks;
}
function cleanupSceneForDeletedAsset(
scene: ProjectScene,
asset: ProjectAssetRecord,
removedFoliagePrototypeIds: ReadonlySet<string>
): ProjectScene {
let nextWorld = scene.world;
let nextModelInstances = scene.modelInstances;
let nextEntities = scene.entities;
let nextFoliageLayers = scene.foliageLayers;
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 === "image") {
const nextTimeOfDay = { ...nextWorld.timeOfDay };
let didChangeTimeOfDay = false;
for (const phase of ["dawn", "dusk"] as const) {
const profile = nextTimeOfDay[phase];
if (
profile.background.mode === "image" &&
profile.background.assetId === asset.id
) {
nextTimeOfDay[phase] = {
...profile,
background: {
...profile.background,
assetId: ""
}
};
didChangeTimeOfDay = true;
}
}
if (didChangeTimeOfDay) {
nextWorld = {
...nextWorld,
timeOfDay: nextTimeOfDay
};
}
}
if (asset.kind === "model") {
const remainingModelInstances: ProjectScene["modelInstances"] = {};
const updatedEntities: ProjectScene["entities"] = {};
let didChangeEntities = false;
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;
}
for (const [entityId, entity] of Object.entries(scene.entities)) {
if (entity.kind === "npc" && entity.modelAssetId === asset.id) {
updatedEntities[entityId] = {
...entity,
modelAssetId: null
};
didChangeEntities = true;
continue;
}
updatedEntities[entityId] = entity;
}
if (didChangeEntities) {
nextEntities = updatedEntities;
}
}
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 (removedFoliagePrototypeIds.size > 0) {
const updatedFoliageLayers: ProjectScene["foliageLayers"] = {};
let didChangeFoliageLayers = false;
for (const [layerId, layer] of Object.entries(scene.foliageLayers)) {
const nextPrototypeIds = layer.prototypeIds.filter(
(prototypeId) => !removedFoliagePrototypeIds.has(prototypeId)
);
if (nextPrototypeIds.length !== layer.prototypeIds.length) {
updatedFoliageLayers[layerId] = {
...layer,
prototypeIds: nextPrototypeIds
};
didChangeFoliageLayers = true;
continue;
}
updatedFoliageLayers[layerId] = layer;
}
if (didChangeFoliageLayers) {
nextFoliageLayers = updatedFoliageLayers;
}
}
if (
nextWorld === scene.world &&
nextModelInstances === scene.modelInstances &&
nextEntities === scene.entities &&
nextFoliageLayers === scene.foliageLayers &&
nextInteractionLinks === scene.interactionLinks
) {
return scene;
}
return {
...scene,
world: nextWorld,
modelInstances: nextModelInstances,
entities: nextEntities,
foliageLayers: nextFoliageLayers,
interactionLinks: nextInteractionLinks
};
}
function cleanupFoliagePrototypesForDeletedAsset(
foliagePrototypes: ProjectDocument["foliagePrototypes"],
asset: ProjectAssetRecord
): {
foliagePrototypes: ProjectDocument["foliagePrototypes"];
removedFoliagePrototypeIds: ReadonlySet<string>;
} {
if (asset.kind !== "model") {
return {
foliagePrototypes,
removedFoliagePrototypeIds: new Set()
};
}
const nextFoliagePrototypes: ProjectDocument["foliagePrototypes"] = {};
const removedFoliagePrototypeIds = new Set<string>();
for (const [prototypeId, prototype] of Object.entries(foliagePrototypes)) {
if (foliagePrototypeReferencesProjectAsset(prototype, asset.id)) {
removedFoliagePrototypeIds.add(prototypeId);
continue;
}
nextFoliagePrototypes[prototypeId] = prototype;
}
return {
foliagePrototypes:
removedFoliagePrototypeIds.size > 0
? nextFoliagePrototypes
: foliagePrototypes,
removedFoliagePrototypeIds
};
}
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 foliageCleanup = cleanupFoliagePrototypesForDeletedAsset(
projectDocument.foliagePrototypes,
asset
);
const nextScenes: ProjectDocument["scenes"] = {};
let didChangeScenes = false;
for (const [sceneId, scene] of Object.entries(projectDocument.scenes)) {
const nextScene = cleanupSceneForDeletedAsset(
scene,
asset,
foliageCleanup.removedFoliagePrototypeIds
);
nextScenes[sceneId] = nextScene;
didChangeScenes ||= nextScene !== scene;
}
return {
...projectDocument,
assets: nextAssets,
foliagePrototypes: foliageCleanup.foliagePrototypes,
scenes: didChangeScenes ? nextScenes : projectDocument.scenes
};
}