Clear references to a template object in every scene on delete (#269)
Deleting an object in a container tab only cleared references inside the template. Placements in scenes and other containers kept their instance edit and showed "(missing)" in the inspector. - shownObjectIds records which objects every holder shows before the delete. dropReferencesToGone compares that with the state after it and clears each reference that no longer resolves. - A reference is resolved from its own place with referenceCandidates, the same way the runtime does. One rule covers instance edits, nested placements and full ids like p~crate. - A cleared instance edit is dropped when it now says the same as the template's own value. Without such a value a marked None stays, as if picked by hand, and Reset hands the row back to the template. - All cleared references come back with a single undo. - References that were already dead in saved projects stay untouched. Findings, not fixed here: - A reference from container D into a nested placement q is stored as q~<id>, but the copy is named r~q/<id> in every placement of D. The runtime finds nothing, so that reference is dead from the start. - Delete asset removes placed objects by cascade but does not clear references to them. Tests: - Unit object-references: 17 passed (13 + 4) - Gate: 198 files, 2873 tests, exit 0 - e2e 1-17,57: 564 checks passed, 57 has 13 (+1 for 57h) - Five unit counter-probes and one browser counter-probe fail as expected - e2e total 1607 is computed, not measured in a full run Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0f6e628616
commit
214218d1b2
6 files changed
+249
-18
No files matched your search
@@ -3,10 +3,18 @@
|
||||
* empty for none. Inside a container the id is the one the template gives it, so every placement of
|
||||
* the container points at its own copy – which is why a reference is looked up from where it sits.
|
||||
*/
|
||||
import { findAsset, type Project } from './Project';
|
||||
import { findAsset, type ObjectHolder, objectHolders, type Project } from './Project';
|
||||
import { treeOrder, type ObjectTree } from './hierarchy';
|
||||
import { resolveObjects } from './instance';
|
||||
import { mapNested, type PropertyBag, type PropertyValue, tagged, tagOf, untag } from './Properties';
|
||||
import { instanceChildId, instanceTargetOf, isEmptyEdit, resolveObjects } from './instance';
|
||||
import {
|
||||
mapNested,
|
||||
type PropertyBag,
|
||||
type PropertyValue,
|
||||
sameValue,
|
||||
tagged,
|
||||
tagOf,
|
||||
untag,
|
||||
} from './Properties';
|
||||
import { objectName, type SceneObject } from './SceneObject';
|
||||
import { Recorder } from '../util/recorder';
|
||||
|
||||
@@ -72,22 +80,65 @@ export function objectChoices(project: Project, holder: ObjectTree, objectId: st
|
||||
|
||||
/**
|
||||
* Renames the references the objects' own values and their container edits hold, as `swap` says,
|
||||
* through `recorder` so an undo takes it back.
|
||||
* through `recorder` so an undo takes it back. `fromId` is the object the reference sits on.
|
||||
*/
|
||||
export function swapReferences(
|
||||
objects: readonly SceneObject[],
|
||||
swap: (ref: string) => string | undefined,
|
||||
swap: (ref: string, fromId: string) => string | undefined,
|
||||
recorder: Recorder,
|
||||
): void {
|
||||
const inBag = (bag: PropertyBag) => {
|
||||
const inBag = (bag: PropertyBag, fromId: string) => {
|
||||
for (const [key, value] of Object.entries(bag)) {
|
||||
const next = withReferences(value, swap);
|
||||
const next = withReferences(value, (ref) => swap(ref, fromId));
|
||||
if (next !== value) recorder.set(bag, key, next);
|
||||
}
|
||||
};
|
||||
for (const object of objects) {
|
||||
inBag(object.overrides);
|
||||
for (const edit of Object.values(object.instanceEdits)) inBag(edit.overrides);
|
||||
inBag(object.overrides, object.id);
|
||||
for (const [path, edit] of Object.entries(object.instanceEdits))
|
||||
inBag(edit.overrides, instanceChildId(object.id, path));
|
||||
}
|
||||
}
|
||||
|
||||
/** The ids every holder shows, container content included – what a reference there can name. */
|
||||
export function shownObjectIds(project: Project): Map<ObjectHolder, Set<string>> {
|
||||
return new Map(
|
||||
objectHolders(project).map((h) => [h, new Set(resolveObjects(project, h).map((o) => o.id))]),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Empties every reference that named an object shown `before` and gone now – in the holder it was
|
||||
* removed from and in every placement of a container it was removed from (#269). A container edit
|
||||
* emptied down to what its template says goes, so the placement follows the template again.
|
||||
*/
|
||||
export function dropReferencesToGone(
|
||||
project: Project,
|
||||
before: Map<ObjectHolder, Set<string>>,
|
||||
recorder: Recorder,
|
||||
): void {
|
||||
const edits: { holder: ObjectHolder; object: SceneObject; path: string; was: PropertyBag }[] = [];
|
||||
for (const [holder, now] of shownObjectIds(project)) {
|
||||
const was = before.get(holder);
|
||||
if (!was || [...was].every((id) => now.has(id))) continue;
|
||||
for (const object of holder.objects)
|
||||
for (const [path, edit] of Object.entries(object.instanceEdits))
|
||||
edits.push({ holder, object, path, was: { ...edit.overrides } });
|
||||
const swap = (ref: string, fromId: string) => {
|
||||
const named = referenceCandidates(fromId, ref).find((id) => was.has(id));
|
||||
return named && !now.has(named) ? '' : undefined;
|
||||
};
|
||||
swapReferences(holder.objects, swap, recorder);
|
||||
}
|
||||
// Only once every holder is done: the base of a nested placement is the edits of the one around it.
|
||||
for (const { holder, object, path, was } of edits) {
|
||||
const edit = object.instanceEdits[path]!;
|
||||
const emptied = Object.keys(was).filter((key) => edit.overrides[key] !== was[key]);
|
||||
const base = emptied.length && instanceTargetOf(project, holder, instanceChildId(object.id, path))?.base;
|
||||
if (!base) continue;
|
||||
for (const key of emptied)
|
||||
if (sameValue(edit.overrides[key], base.overrides[key])) recorder.delete(edit.overrides, key);
|
||||
if (isEmptyEdit(edit)) recorder.delete(object.instanceEdits, path);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user