Fix references into nested placements and on asset delete (#285, #286)

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:
adminandClaude Opus 5.5 committed 2026-10-01 07:28:49 +02:00
1 parent 214218d1b2
commit 5e21d473a4
9 files changed
+295 -17

No files matched your search

+12 -5
View File
@@ -1,8 +1,14 @@
import { newId } from '../ids';
import type { ContainerAsset } from '../model/Asset';
import { findObject, removeSubtrees, subtreeIds, subtreeRoots, treeOrder } from '../model/hierarchy';
import { instanceChildId, type InstanceEdit, isLiveInstance, materializeInstance } from '../model/instance';
import { anchorReferences, swapReferences } from '../model/objectRefs';
import {
instanceChildId,
type InstanceEdit,
isLiveInstance,
materializeInstance,
pathOfId,
} from '../model/instance';
import { anchorReferences, renamedReference, swapReferences } from '../model/objectRefs';
import { apply, invert, multiply, translation } from '../model/matrix';
import type { Position } from '../model/Properties';
import { findAsset, findObjectHolder, type Project } from '../model/Project';
@@ -114,12 +120,13 @@ export class CreateContainerCommand implements Command {
if (!containerParentId && groupId) container.groupId = groupId;
this.removed = removeSubtrees(holder, [...roots]);
// A reference to a member follows it (#258): inside the template to its template id, from the
// rest of the holder to what the placed container shows of it.
const templateIdOf = (ref: string) => templateIds.get(ref);
// rest of the holder to what the placed container shows of it – content of a member placement
// included (#285).
const templateIdOf = (ref: string) => renamedReference(ref, templateIds);
swapReferences(asset.objects, templateIdOf, this.references);
const shown = (ref: string) => {
const moved = templateIdOf(ref);
return moved && instanceChildId(this.objectId, moved);
return moved && instanceChildId(this.objectId, pathOfId(moved));
};
swapReferences(holder.objects, shown, this.references);
project.assets.push(asset);