diff --git a/packages/fig/src/instance-overrides/materialize-instance.ts b/packages/fig/src/instance-overrides/materialize-instance.ts index e64453fd6..09449af51 100644 --- a/packages/fig/src/instance-overrides/materialize-instance.ts +++ b/packages/fig/src/instance-overrides/materialize-instance.ts @@ -19,6 +19,8 @@ function occurrenceMetadata( converted: ReturnType ) { const metadata = createDefaultSourceMetadata() + // Figma's own sibling key, so re-export keeps the order Figma saved. + metadata.orderKey = current.properties.parentIndex?.position ?? null metadata.fig.layout = converted.source?.fig.layout ?? null metadata.fig.uniformScaleFactor = symbolDataOf(current.properties)?.uniformScaleFactor ?? null return metadata diff --git a/packages/fig/src/node-change/basics.ts b/packages/fig/src/node-change/basics.ts index 0d23537b1..dd12c961d 100644 --- a/packages/fig/src/node-change/basics.ts +++ b/packages/fig/src/node-change/basics.ts @@ -81,10 +81,34 @@ export function orderKeyBetween(lo: string | null, hi: string | null): string | return hi.slice(0, i + 1) + (orderKeyBetween(null, rest) ?? '') } +/** Indices of the longest strictly increasing run of keys, so a moved layer re-keys alone. */ +function increasingKeyIndices(sourceKeys: ReadonlyArray): Set { + const tails: number[] = [] + const previous = new Map() + for (let index = 0; index < sourceKeys.length; index++) { + const key = sourceKeys[index] + if (!key) continue + let low = 0 + let high = tails.length + while (low < high) { + const mid = (low + high) >> 1 + if ((sourceKeys[tails[mid]] ?? '') < key) low = mid + 1 + else high = mid + } + // A key with nothing below it cannot start the run once a sibling has to precede it. + if (low === 0 && index > 0 && orderKeyBetween(null, key) === null) continue + if (low > 0) previous.set(index, tails[low - 1]) + tails[low] = index + } + const kept = new Set() + for (let index = tails.at(-1); index !== undefined; index = previous.get(index)) kept.add(index) + return kept +} + /** - * Order keys for siblings in their current order. Imported keys are kept while they still - * increase; siblings without a usable key get the index key when it fits between its - * neighbours, and otherwise a key between them, so no two siblings share a key. + * Order keys for siblings in their current order. The longest increasing run of imported keys + * is kept; other siblings get the index key when it fits between their neighbours, and + * otherwise a key between them, so no two siblings share a key. */ export function siblingOrderKeys(sourceKeys: ReadonlyArray): string[] { const keys: string[] = [] @@ -107,9 +131,10 @@ export function siblingOrderKeys(sourceKeys: ReadonlyArray prev) ? fill(key) : null + const filled = key && kept.has(index) ? fill(key) : null if (key && filled) { pending.forEach((pendingIndex, i) => (keys[pendingIndex] = filled[i])) keys[index] = key diff --git a/packages/fig/tests/instance-overrides/materialize.test.ts b/packages/fig/tests/instance-overrides/materialize.test.ts index 615d1cc66..eaf70b90a 100644 --- a/packages/fig/tests/instance-overrides/materialize.test.ts +++ b/packages/fig/tests/instance-overrides/materialize.test.ts @@ -46,6 +46,15 @@ test('materializes separate occurrence nodes with direct component identities an for (const node of result.nodes.values()) expect(node.source.id).toBeNull() }) +test('materialized occurrences keep the order keys Figma saved', () => { + const { occurrence, graph, page, components } = setup() + const result = materializeInstance(graph, page.id, occurrence, components) + for (const [source, node] of result.nodes) { + expect(node.source.orderKey).toBe(source.properties.parentIndex?.position ?? null) + } + expect(result.root.source.orderKey).not.toBeNull() +}) + test('materialization does not share editable paint payloads with interpreted occurrences', () => { const { occurrence, graph, page, components } = setup() occurrence.properties.fillPaints = [ diff --git a/packages/fig/tests/node-change/order-keys.test.ts b/packages/fig/tests/node-change/order-keys.test.ts index fbfe56c08..e37f95f35 100644 --- a/packages/fig/tests/node-change/order-keys.test.ts +++ b/packages/fig/tests/node-change/order-keys.test.ts @@ -81,9 +81,23 @@ describe('siblingOrderKeys', () => { expectStrictlyIncreasing(keys) }) - test('re-keys imported siblings that were moved out of order', () => { - const keys = siblingOrderKeys(['#', '!', '"']) - expect(keys[0]).toBe('#') + test('re-keys only the sibling that was moved out of order', () => { + const keys = siblingOrderKeys(['$', '!O', '"', '#']) + expect(keys.slice(1)).toEqual(['!O', '"', '#']) + expectStrictlyIncreasing(keys) + }) + + test('does not anchor on the lowest key when a sibling has to precede it', () => { + const keys = siblingOrderKeys(['"', '!', '#']) + expect(keys[0]).toBe('"') + expect(keys[2]).toBe('#') + expectStrictlyIncreasing(keys) + }) + + test('keeps the siblings a moved layer jumped over', () => { + const keys = siblingOrderKeys([null, '~', '!O', '"', '#']) + expect(keys.slice(2)).toEqual(['!O', '"', '#']) + expect(new Set(keys).size).toBe(keys.length) expectStrictlyIncreasing(keys) }) })