linked copies

Ideallo supports linked copies – objects that share the same underlying content while maintaining independent position, style, and other metadata.

Overview

When an object is duplicated as a linked copy, the new object shares the same text content, polygon geometry, or freehand path data as the original. Editing the shared content in one object immediately updates all linked copies. However, each copy has its own position (xy), rotation (r), style fields (sc, fc, etc.), and dimensions (wh).

This is useful for repeating elements like labels, shapes, or icons that should stay synchronized across the board.

Content ID Fields

Three optional fields control which content map entry an object references:

Field Object Types Content Map Description
tid Text (T), Sticky (S) txt Text content ID – keys into the Y.Text map
gid Polygon (P) geo Geometry ID – keys into the Y.Array<number> map
pid Freehand (F) paths Path ID – keys into the SVG path string map

Rule: When the content ID field is omitted, the object’s own ObjectId is used as the key in the corresponding content map. When present, it points to a different object’s content entry.

How It Works

graph LR
    subgraph "Objects Map (o)"
        A["Object A<br/>(original)<br/>tid: --"]
        B["Object B<br/>(linked copy)<br/>tid: 'A'"]
    end

    subgraph "Text Map (txt)"
        T["'A' → Y.Text<br/>'Hello, world!'"]
    end

    A -- "uses own ID as key" --> T
    B -- "tid points to A" --> T

Object A uses its own ID (A) as the key in the txt map. Object B has tid: 'A', so it references the same Y.Text entry. Editing the text through either object updates the shared content.

True Copy vs Linked Copy

Ideallo provides two duplication operations with different content semantics:

True Copy (duplicateObject)

Creates a fully independent copy with its own content entry:

// Original object "aB3x_Qm7kL9p"
{ "t": "T", "xy": [100, 100], "wh": [200, 50] }
// txt map: "aB3x_Qm7kL9p" → "Hello"

// True copy "Xk2nR8vH_wYq" — independent content
{ "t": "T", "xy": [120, 120], "wh": [200, 50] }
// txt map: "Xk2nR8vH_wYq"  "Hello"  (separate Y.Text copy)

The true copy gets a new Y.Text entry with the same initial content but is fully independent – editing one does not affect the other. No tid field is stored.

Linked Copy (duplicateAsLinkedCopy)

Creates a copy that shares the original’s content entry:

// Original object "aB3x_Qm7kL9p"
{ "t": "T", "xy": [100, 100], "wh": [200, 50] }
// txt map: "aB3x_Qm7kL9p" → "Hello"

// Linked copy "Hw5_qT2mLkJx" — shared content
{ "t": "T", "xy": [120, 120], "wh": [200, 50], "tid": "aB3x_Qm7kL9p" }
// No new txt map entry  uses "aB3x_Qm7kL9p" from original

The linked copy has an explicit tid field pointing to the original’s content. Both objects render the same text, and editing through either one updates both.

Chained Linking

When duplicating a linked copy as another linked copy, the new copy points to the original source, not to the intermediate copy. This prevents chains of indirection:

// Object B is a linked copy of A: B.tid = 'A'
// Creating a linked copy of B:
duplicated.tid = existing.tid ?? objectId
// Result: C.tid = 'A' (not 'B')

All linked copies in a group always point directly to the same source content entry, keeping lookups to a single indirection.

Deletion and Orphan Handling

When an object is deleted, its associated content map entries are not deleted. This is for two reasons:

  1. CRDT tombstones: Deleting a Y.Map entry creates a tombstone that takes space anyway – no storage is reclaimed by also deleting content entries.
  2. Linked copies may still reference the content: Other objects might have tid/gid/pid pointing to the deleted object’s content.

Orphaned content entries (those with no remaining objects referencing them) are cleaned up during export or compaction operations.

Content Access Helpers

The CRDT module provides helper functions that automatically resolve content ID indirection:

Function Returns Description
getObjectYText(doc, objectId) Y.Text | undefined Resolves tid or falls back to objectId for Text/Sticky objects
getObjectYArray(doc, objectId) Y.Array<number> | undefined Resolves gid or falls back to objectId for Polygon objects
getObjectPathData(doc, objectId) string | undefined Resolves pid or falls back to objectId for Freehand objects

These helpers look up the stored object, check for a content ID field, and return the corresponding content map entry. Application code should always use these helpers rather than accessing content maps directly.