fix(fig): keep Figma's order keys on instances and untouched siblings (#819)
* fix(fig): keep Figma's order keys on instances and untouched siblings
Materialized instance occurrences were built without the record's
parentIndex position, so every imported instance lost its key and was
renumbered on save. They now carry it like other imported nodes.
Sibling keys anchored greedily on the first imported key, so a layer moved
up past its siblings kept its key and pushed every sibling after it onto a
new one. Keep the longest increasing run of imported keys instead, so only
the moved and inserted layers are re-keyed.
* fix(fig): don't anchor sibling keys on one nothing can precede
A moved layer in front of the lowest key ('!') left no room below it, so
the anchor was dropped and every sibling up to the next anchor re-keyed.
Such a key no longer starts the kept run when a sibling precedes it.
This commit is contained in:
parent
6a3960a5d4
commit
12b6662ba3
|
|
@ -19,6 +19,8 @@ function occurrenceMetadata(
|
|||
converted: ReturnType<typeof nodeChangeToProps>
|
||||
) {
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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<string | null | undefined>): Set<number> {
|
||||
const tails: number[] = []
|
||||
const previous = new Map<number, number>()
|
||||
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<number>()
|
||||
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 | null | undefined>): string[] {
|
||||
const keys: string[] = []
|
||||
|
|
@ -107,9 +131,10 @@ export function siblingOrderKeys(sourceKeys: ReadonlyArray<string | null | undef
|
|||
return filled
|
||||
}
|
||||
|
||||
const kept = increasingKeyIndices(sourceKeys)
|
||||
for (let index = 0; index < sourceKeys.length; index++) {
|
||||
const key = sourceKeys[index]
|
||||
const filled = key && (prev === null || key > 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
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue