fix: preserve authored instance sizing through synchronization
This commit is contained in:
parent
3df430f036
commit
4077dcbd44
|
|
@ -64,6 +64,7 @@
|
|||
- Honor `.pen` frame layout defaults and sizing and padding shorthands so imported auto-layout frames keep their computed dimensions and child positions. (#564)
|
||||
- Avoid macOS Keychain prompts during credential status checks and pause repeated credential access after failures until explicitly retried from Settings.
|
||||
|
||||
- Honor explicit Design JSX instance dimensions and preserve authored overrides through component synchronization.
|
||||
- Route browser Command/Ctrl plus and minus shortcuts to canvas zoom instead of page zoom.
|
||||
- Resolve `$name` references in imported `.pen` fills, stroke fills, font families, dimensions, and spacing without requiring a `--` prefix. (#563)
|
||||
- Resolve bound fields in each layer’s mode, keep variable edits scoped and undoable, and make broken bindings visible and recoverable.
|
||||
|
|
|
|||
|
|
@ -430,13 +430,34 @@ async function renderInstanceNode(
|
|||
const label = typeof ref === 'string' || typeof ref === 'number' ? String(ref) : ''
|
||||
throw new Error(`<Instance> component not found: ${label}`)
|
||||
}
|
||||
const overrides = {
|
||||
const overrides: Partial<SceneNode> = {
|
||||
...propsToOverrides(props, false, parentLayout),
|
||||
...componentMetadata(props, 'INSTANCE', componentPropertyScope(graph, parentId))
|
||||
}
|
||||
// Instances inherit their container layout, but explicitly authored dimensions
|
||||
// must also replace the inherited sizing mode on that axis.
|
||||
const layout = overrides.layoutMode ?? component.layoutMode
|
||||
if (layout !== 'NONE') {
|
||||
const axes =
|
||||
layout === 'HORIZONTAL'
|
||||
? (['primaryAxisSizing', 'counterAxisSizing'] as const)
|
||||
: (['counterAxisSizing', 'primaryAxisSizing'] as const)
|
||||
for (const [dimension, field] of [
|
||||
['w', axes[0]],
|
||||
['h', axes[1]]
|
||||
] as const) {
|
||||
const value = props[dimension]
|
||||
if (typeof value === 'number') overrides[field] = 'FIXED'
|
||||
else if (value === 'hug' || value === 'fill') overrides[field] = 'HUG'
|
||||
}
|
||||
}
|
||||
const instance =
|
||||
graph.createInstance(component.id, parentId, overrides) ?? graph.createNode('FRAME', parentId)
|
||||
try {
|
||||
for (const [field, value] of Object.entries(overrides)) {
|
||||
setInstanceOverride(instance.instanceOverrides, instance.id, instance.id, field, value)
|
||||
}
|
||||
graph.updateNode(instance.id, { instanceOverrides: instance.instanceOverrides })
|
||||
applyBindings(graph, instance.id, bindings)
|
||||
applyInstanceOverrides(graph, instance, tree.props.overrides)
|
||||
assignComponentProperties(graph, instance, props.properties)
|
||||
|
|
|
|||
26
tests/e2e/canvas/instance-sizing.spec.ts
Normal file
26
tests/e2e/canvas/instance-sizing.spec.ts
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
import { expect, test, useEditorSetupWithClear } from '#tests/e2e/fixtures'
|
||||
import type * as SizingFixture from '#tests/helpers/canvas/instance-sizing'
|
||||
|
||||
test.use({ viewport: { width: 900, height: 700 } })
|
||||
|
||||
const editor = useEditorSetupWithClear('/?test&no-chrome&no-rulers')
|
||||
|
||||
test('Fill instances wrap inside narrower parents after component synchronization', async () => {
|
||||
await editor.page.evaluate(async () => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store) throw new Error('OpenPencil store not initialized')
|
||||
const fixtureURL = '/tests/helpers/canvas/instance-sizing.ts'
|
||||
const { createInstanceSizingScene }: typeof SizingFixture = await import(fixtureURL)
|
||||
const { boardId, componentId } = await createInstanceSizingScene(
|
||||
store.graph,
|
||||
store.state.currentPageId
|
||||
)
|
||||
await store.loadFontsForNodes([boardId, componentId])
|
||||
store.clearSelection()
|
||||
store.zoomToFit()
|
||||
store.requestRender()
|
||||
})
|
||||
await editor.canvas.waitForRender()
|
||||
editor.canvas.assertNoErrors()
|
||||
expect(await editor.canvas.screenshotCanvasRegion()).toMatchSnapshot('instance-fill-sizing.png')
|
||||
})
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 84 KiB |
49
tests/engine/render/jsx/instance-sizing.test.ts
Normal file
49
tests/engine/render/jsx/instance-sizing.test.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import { expect, test } from 'bun:test'
|
||||
|
||||
import { Component, Frame, Instance, Rectangle, renderTree } from '@open-pencil/core/design-jsx'
|
||||
import { computeAllLayouts } from '@open-pencil/core/layout'
|
||||
|
||||
import { getNodeOrThrow } from '#tests/helpers/assert'
|
||||
import { makeSceneGraph } from '#tests/helpers/scene'
|
||||
|
||||
for (const flex of ['row', 'col'] as const) {
|
||||
test(`instances fill their parent across inherited fixed ${flex} dimensions`, async () => {
|
||||
const graph = makeSceneGraph()
|
||||
const component = await renderTree(
|
||||
graph,
|
||||
Component({ flex, w: 280, h: 100, children: Rectangle({ w: 20, h: 20 }) })
|
||||
)
|
||||
const parent = await renderTree(
|
||||
graph,
|
||||
Frame({
|
||||
flex: 'col',
|
||||
w: 220,
|
||||
h: 180,
|
||||
children: Instance({ of: component.id, w: 'fill', h: 'fill' })
|
||||
})
|
||||
)
|
||||
const instance = graph.getChildren(parent.id)[0]
|
||||
expect(instance.width).toBe(220)
|
||||
expect(instance.height).toBe(180)
|
||||
graph.syncInstances(component.id)
|
||||
computeAllLayouts(graph)
|
||||
expect(instance.width).toBe(220)
|
||||
expect(instance.height).toBe(180)
|
||||
})
|
||||
|
||||
test(`explicit instance dimensions override inherited Hug ${flex} sizing`, async () => {
|
||||
const graph = makeSceneGraph()
|
||||
const component = await renderTree(
|
||||
graph,
|
||||
Component({ flex, w: 'hug', h: 'hug', children: Rectangle({ w: 20, h: 20 }) })
|
||||
)
|
||||
const result = await renderTree(graph, Instance({ of: component.id, w: 120, h: 80 }))
|
||||
const instance = getNodeOrThrow(graph, result.id)
|
||||
expect(instance.width).toBe(120)
|
||||
expect(instance.height).toBe(80)
|
||||
graph.syncInstances(component.id)
|
||||
computeAllLayouts(graph)
|
||||
expect(instance.width).toBe(120)
|
||||
expect(instance.height).toBe(80)
|
||||
})
|
||||
}
|
||||
65
tests/helpers/canvas/instance-sizing.ts
Normal file
65
tests/helpers/canvas/instance-sizing.ts
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
import { Component, Frame, Instance, Text, renderTree } from '@open-pencil/core/design-jsx'
|
||||
import type { SceneGraph } from '@open-pencil/scene-graph'
|
||||
|
||||
export async function createInstanceSizingScene(graph: SceneGraph, parentId: string) {
|
||||
const library = graph.addPage('Sizing component')
|
||||
const component = await renderTree(
|
||||
graph,
|
||||
Component({
|
||||
name: 'Note',
|
||||
flex: 'col',
|
||||
w: 280,
|
||||
h: 'hug',
|
||||
p: 16,
|
||||
bg: '#FFFFFF',
|
||||
rounded: 10,
|
||||
properties: [{ id: 'message', name: 'Message', type: 'TEXT', defaultValue: 'A short note.' }],
|
||||
children: Text({
|
||||
name: 'Message',
|
||||
w: 'fill',
|
||||
font: 'Inter',
|
||||
size: 14,
|
||||
lineHeight: 20,
|
||||
color: '#252A31',
|
||||
propertyRefs: [{ propertyId: 'message', field: 'TEXT' }],
|
||||
children: 'A short note.'
|
||||
})
|
||||
}),
|
||||
{ parentId: library.id }
|
||||
)
|
||||
const board = await renderTree(
|
||||
graph,
|
||||
Frame({
|
||||
name: 'Instance sizing',
|
||||
flex: 'row',
|
||||
w: 'hug',
|
||||
h: 'hug',
|
||||
gap: 32,
|
||||
p: 32,
|
||||
bg: '#E9E7E2',
|
||||
children: [280, 220].map((width) =>
|
||||
Frame({
|
||||
name: `Container ${width}`,
|
||||
flex: 'col',
|
||||
w: width,
|
||||
h: 'hug',
|
||||
gap: 12,
|
||||
children: [
|
||||
Text({ children: `${width}px container`, font: 'Inter', size: 12, color: '#626975' }),
|
||||
Instance({
|
||||
of: component.id,
|
||||
w: 'fill',
|
||||
properties: {
|
||||
message:
|
||||
'The blue feels right. Give the date a little more room at the bottom, and keep the quieter version for the print edition.'
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
)
|
||||
}),
|
||||
{ parentId }
|
||||
)
|
||||
graph.syncInstances(component.id)
|
||||
return { boardId: board.id, componentId: component.id }
|
||||
}
|
||||
Loading…
Reference in a new issue