Two findings from #269. A reference from a container into a placement nested in it was dead from the start, and deleting an asset left references to the objects it took along showing "(missing)". - A reference is stored the way its holder shows the object. In a container tab that is q~x for content of a nested placement q. - referenceCandidates translates it when the container is placed: in placement r, q~x becomes r~q/x. pathOfId and idOfPath in instance.ts do the mapping, objectChoices offers the same spelling the other way. The rule holds at any depth. - Storing q/x as proposed in #285 was dropped. The container tab would then have had to translate its own ids. - No migration. All projects and snapshots hold three references, none of them with ~ or /. - Create container lets a reference to content of a member placement follow it. It was left dead before. - Paste lets such a reference point into the copy. It kept pointing at the original before. Both go through the new renamedReference. - Delete asset records what every holder shows before the cascade and clears every reference to an object that is gone afterwards, with dropReferencesToGone from #269. The row shows None, undo restores the references (#286). Tests: - Gate: 198 files, 2882 tests (2873 + 9), exit 0 - Unit object-references: 26 passed (17 + 9) - e2e 1-17,57: 566 checks passed, 57 has +57i and +57j - Six unit counter-probes fail between 1 and 3 tests each - The browser counter-probe fails 57i and 57j as described in the issues - e2e total 1609 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
214218d1b2
commit
5e21d473a4
9 files changed
+295
-17
No files matched your search
@@ -105,6 +105,16 @@ export function splitInstanceId(id: string): { instanceId: string; path: string
|
||||
return { instanceId: id.slice(0, at), path: id.slice(at + 1) };
|
||||
}
|
||||
|
||||
/** The template path an id of a holder's own view stands for once the holder is placed: `q~x` → `q/x`. */
|
||||
export function pathOfId(id: string): string {
|
||||
return id.replace(ID_SEP, PATH_SEP);
|
||||
}
|
||||
|
||||
/** The id a holder's own view gives the object at a template path: `q/x` → `q~x`. */
|
||||
export function idOfPath(path: string): string {
|
||||
return path.replace(PATH_SEP, ID_SEP);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the object is only resolved from a template rather than stored in the holder. The
|
||||
* question is where it lives, not what its id looks like: unpacking keeps the derived id – so the
|
||||
|
||||
@@ -1,11 +1,21 @@
|
||||
/**
|
||||
* An object's place names another object of the same scene by its id (#258): `{"$object": "<id>"}`,
|
||||
* 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.
|
||||
* empty for none. Inside a container the id is the one the container's own view 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. Content of a placement nested in the container is `q~x` there and `r~q/x` in its
|
||||
* placement r (#285).
|
||||
*/
|
||||
import { findAsset, type ObjectHolder, objectHolders, type Project } from './Project';
|
||||
import { treeOrder, type ObjectTree } from './hierarchy';
|
||||
import { instanceChildId, instanceTargetOf, isEmptyEdit, resolveObjects } from './instance';
|
||||
import {
|
||||
idOfPath,
|
||||
instanceChildId,
|
||||
instanceTargetOf,
|
||||
isEmptyEdit,
|
||||
pathOfId,
|
||||
resolveObjects,
|
||||
splitInstanceId,
|
||||
} from './instance';
|
||||
import {
|
||||
mapNested,
|
||||
type PropertyBag,
|
||||
@@ -45,7 +55,19 @@ function scopeOf(objectId: string): string {
|
||||
export function referenceCandidates(fromId: string, ref: string): string[] {
|
||||
if (!ref) return [];
|
||||
const scope = scopeOf(fromId);
|
||||
return scope ? [scope + ref, ref] : [ref];
|
||||
return scope ? [scope + pathOfId(ref), ref] : [ref];
|
||||
}
|
||||
|
||||
/**
|
||||
* `ref` once the objects in `ids` are renamed: the object itself, or content of a placement that is
|
||||
* renamed – the copy of the placement shows the same content. Undefined when it stays as it is.
|
||||
*/
|
||||
export function renamedReference(ref: string, ids: ReadonlyMap<string, string>): string | undefined {
|
||||
const own = ids.get(ref);
|
||||
if (own) return own;
|
||||
const split = splitInstanceId(ref);
|
||||
const placement = split && ids.get(split.instanceId);
|
||||
return placement && instanceChildId(placement, split.path);
|
||||
}
|
||||
|
||||
export interface ObjectChoice {
|
||||
@@ -70,7 +92,8 @@ export function objectChoices(project: Project, holder: ObjectTree, objectId: st
|
||||
for (const { object, depth } of treeOrder({ ...holder, objects })) {
|
||||
if (!object.id.startsWith(scope)) continue;
|
||||
const label = objectName(object, findAsset(project, object.assetId));
|
||||
choices.push({ ref: object.id.slice(scope.length), objectId: object.id, label, depth });
|
||||
const ref = scope ? idOfPath(object.id.slice(scope.length)) : object.id;
|
||||
choices.push({ ref, objectId: object.id, label, depth });
|
||||
}
|
||||
if (!scope) return choices;
|
||||
// The container's own content sits one level below it in the tree.
|
||||
|
||||
Reference in new issue
Block a user