fix(fig): preserve imported hug container bounds
This commit is contained in:
parent
0232ab17f6
commit
4195f13fad
|
|
@ -60,7 +60,8 @@ function applyNodeTransforms(
|
|||
const rotation =
|
||||
overlays.rotationPreview?.nodeId === nodeId ? overlays.rotationPreview.angle : node.rotation
|
||||
if (rotation !== 0) {
|
||||
canvas.rotate(rotation, node.width / 2, node.height / 2)
|
||||
if (node.type === 'LINE') canvas.rotate(rotation, 0, 0)
|
||||
else canvas.rotate(rotation, node.width / 2, node.height / 2)
|
||||
}
|
||||
|
||||
if (node.flipX || node.flipY) {
|
||||
|
|
|
|||
|
|
@ -328,9 +328,27 @@ function convertLayoutPadding(
|
|||
}
|
||||
}
|
||||
|
||||
function convertLayoutProps(
|
||||
nc: NodeChange
|
||||
): Pick<
|
||||
function visibleContainerDerivedLayout(
|
||||
nc: NodeChange,
|
||||
layoutMode: SceneNode['layoutMode'],
|
||||
primaryAxisSizing: SceneNode['primaryAxisSizing'],
|
||||
counterAxisSizing: SceneNode['counterAxisSizing']
|
||||
): SceneNode['figmaDerivedLayout'] | undefined {
|
||||
const hasHugAxis = primaryAxisSizing === 'HUG' || counterAxisSizing === 'HUG'
|
||||
const hasVisiblePaint =
|
||||
(nc.fillPaints?.some((paint) => paint.visible !== false) ?? false) ||
|
||||
(nc.strokePaints?.some((paint) => paint.visible !== false) ?? false)
|
||||
if (layoutMode === 'NONE' || !hasHugAxis || !hasVisiblePaint) return undefined
|
||||
|
||||
return {
|
||||
x: nc.transform?.m02 ?? 0,
|
||||
y: nc.transform?.m12 ?? 0,
|
||||
width: nc.size?.x ?? 100,
|
||||
height: nc.size?.y ?? 100
|
||||
}
|
||||
}
|
||||
|
||||
function convertLayoutProps(nc: NodeChange): Pick<
|
||||
SceneNode,
|
||||
| 'layoutMode'
|
||||
| 'itemSpacing'
|
||||
|
|
@ -351,13 +369,24 @@ function convertLayoutProps(
|
|||
| 'itemReverseZIndex'
|
||||
| 'strokesIncludedInLayout'
|
||||
| 'layoutDirection'
|
||||
> {
|
||||
> &
|
||||
Partial<Pick<SceneNode, 'figmaDerivedLayout'>> {
|
||||
const layoutMode = mapStackMode(nc.stackMode)
|
||||
const primaryAxisSizing = mapStackSizing(nc.stackPrimarySizing)
|
||||
const counterAxisSizing = mapStackSizing(nc.stackCounterSizing)
|
||||
const figmaDerivedLayout = visibleContainerDerivedLayout(
|
||||
nc,
|
||||
layoutMode,
|
||||
primaryAxisSizing,
|
||||
counterAxisSizing
|
||||
)
|
||||
|
||||
return {
|
||||
layoutMode: mapStackMode(nc.stackMode),
|
||||
layoutMode,
|
||||
itemSpacing: nc.stackSpacing ?? 0,
|
||||
...convertLayoutPadding(nc),
|
||||
primaryAxisSizing: mapStackSizing(nc.stackPrimarySizing),
|
||||
counterAxisSizing: mapStackSizing(nc.stackCounterSizing),
|
||||
primaryAxisSizing,
|
||||
counterAxisSizing,
|
||||
primaryAxisAlign: mapStackJustify(nc.stackPrimaryAlignItems ?? nc.stackJustify),
|
||||
counterAxisAlign: mapStackCounterAlign(nc.stackCounterAlignItems ?? nc.stackCounterAlign),
|
||||
layoutWrap: nc.stackWrap === 'WRAP' ? 'WRAP' : 'NO_WRAP',
|
||||
|
|
@ -372,7 +401,8 @@ function convertLayoutProps(
|
|||
layoutDirection:
|
||||
(getOpenPencilPluginValue(nc, LAYOUT_DIRECTION_PLUGIN_KEY) as
|
||||
| SceneNode['layoutDirection']
|
||||
| null) || 'AUTO'
|
||||
| null) || 'AUTO',
|
||||
...(figmaDerivedLayout ? { figmaDerivedLayout } : {})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
106
tests/engine/layout/auto-layout/imported-derived-layout.test.ts
Normal file
106
tests/engine/layout/auto-layout/imported-derived-layout.test.ts
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import { describe, expect, test } from 'bun:test'
|
||||
|
||||
import { computeAllLayouts, SceneGraph } from '@open-pencil/core'
|
||||
|
||||
describe('imported auto-layout bounds', () => {
|
||||
test('preserves visible hug container bounds when hidden children would collapse layout', () => {
|
||||
const graph = new SceneGraph()
|
||||
const page = graph.getPages()[0]
|
||||
const frame = graph.createNode('FRAME', page.id, {
|
||||
width: 280,
|
||||
height: 44,
|
||||
layoutMode: 'VERTICAL',
|
||||
primaryAxisSizing: 'HUG',
|
||||
counterAxisSizing: 'HUG',
|
||||
paddingTop: 2,
|
||||
paddingRight: 2,
|
||||
paddingBottom: 2,
|
||||
paddingLeft: 2,
|
||||
fills: [
|
||||
{
|
||||
type: 'SOLID',
|
||||
color: { r: 1, g: 1, b: 1, a: 1 },
|
||||
opacity: 1,
|
||||
visible: true
|
||||
}
|
||||
],
|
||||
strokes: [
|
||||
{
|
||||
color: { r: 0.58, g: 0.64, b: 0.72, a: 1 },
|
||||
weight: 2,
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
align: 'OUTSIDE'
|
||||
}
|
||||
],
|
||||
figmaDerivedLayout: { x: 0, y: 0, width: 280, height: 44 }
|
||||
})
|
||||
const wrapper = graph.createNode('FRAME', frame.id, {
|
||||
x: 2,
|
||||
y: 2,
|
||||
width: 276,
|
||||
height: 40,
|
||||
layoutMode: 'VERTICAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'FIXED'
|
||||
})
|
||||
const field = graph.createNode('FRAME', wrapper.id, {
|
||||
width: 276,
|
||||
height: 40,
|
||||
layoutMode: 'HORIZONTAL',
|
||||
primaryAxisSizing: 'FIXED',
|
||||
counterAxisSizing: 'HUG',
|
||||
layoutAlignSelf: 'STRETCH',
|
||||
paddingTop: 8,
|
||||
paddingRight: 56,
|
||||
paddingBottom: 8,
|
||||
paddingLeft: 12,
|
||||
fills: [
|
||||
{
|
||||
type: 'SOLID',
|
||||
color: { r: 1, g: 1, b: 1, a: 1 },
|
||||
opacity: 1,
|
||||
visible: true
|
||||
}
|
||||
],
|
||||
strokes: [
|
||||
{
|
||||
color: { r: 0.8, g: 0.84, b: 0.88, a: 1 },
|
||||
weight: 1,
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
align: 'OUTSIDE'
|
||||
}
|
||||
],
|
||||
figmaDerivedLayout: { x: 0, y: 0, width: 276, height: 40 }
|
||||
})
|
||||
graph.createNode('TEXT', field.id, {
|
||||
x: 12,
|
||||
y: 8,
|
||||
width: 132,
|
||||
height: 24,
|
||||
text: 'typing something',
|
||||
visible: false
|
||||
})
|
||||
graph.createNode('LINE', field.id, {
|
||||
x: 12,
|
||||
y: 8,
|
||||
width: 24,
|
||||
height: 0,
|
||||
rotation: 90,
|
||||
strokes: [
|
||||
{
|
||||
color: { r: 0, g: 0, b: 0, a: 1 },
|
||||
weight: 1,
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
align: 'CENTER'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
computeAllLayouts(graph)
|
||||
|
||||
expect(graph.getNode(field.id)).toMatchObject({ x: 0, y: 0, width: 276, height: 40 })
|
||||
})
|
||||
})
|
||||
73
tests/engine/render/canvas/line.test.ts
Normal file
73
tests/engine/render/canvas/line.test.ts
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
import { beforeAll, describe, expect, test } from 'bun:test'
|
||||
|
||||
import { renderNodesToImage, SceneGraph, SkiaRenderer } from '@open-pencil/core'
|
||||
|
||||
import { initCanvasKit } from '#cli/headless'
|
||||
|
||||
import { expectDefined } from '#tests/helpers/assert'
|
||||
|
||||
let ck: Awaited<ReturnType<typeof initCanvasKit>>
|
||||
|
||||
beforeAll(async () => {
|
||||
ck = await initCanvasKit()
|
||||
})
|
||||
|
||||
describe('line rendering', () => {
|
||||
test('rotates lines around their transform origin', async () => {
|
||||
const graph = new SceneGraph()
|
||||
const page = graph.getPages()[0]
|
||||
const frame = graph.createNode('FRAME', page.id, {
|
||||
width: 40,
|
||||
height: 40
|
||||
})
|
||||
graph.createNode('LINE', frame.id, {
|
||||
x: 10,
|
||||
y: 8,
|
||||
width: 24,
|
||||
height: 0,
|
||||
rotation: 90,
|
||||
strokes: [
|
||||
{
|
||||
color: { r: 0, g: 0, b: 0, a: 1 },
|
||||
weight: 1,
|
||||
opacity: 1,
|
||||
visible: true,
|
||||
align: 'CENTER'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const surface = expectDefined(ck.MakeSurface(1, 1), 'surface')
|
||||
const renderer = new SkiaRenderer(ck, surface)
|
||||
|
||||
try {
|
||||
const png = expectDefined(
|
||||
renderNodesToImage(ck, renderer, graph, page.id, [frame.id], {
|
||||
scale: 1,
|
||||
format: 'PNG'
|
||||
}),
|
||||
'line png'
|
||||
)
|
||||
const image = expectDefined(ck.MakeImageFromEncoded(png), 'line image')
|
||||
const pixels = expectDefined(
|
||||
image.readPixels(0, 0, {
|
||||
width: image.width(),
|
||||
height: image.height(),
|
||||
colorType: ck.ColorType.RGBA_8888,
|
||||
alphaType: ck.AlphaType.Unpremul,
|
||||
colorSpace: ck.ColorSpace.SRGB
|
||||
}),
|
||||
'line pixels'
|
||||
)
|
||||
const width = image.width()
|
||||
const alphaAt = (x: number, y: number) => pixels[(y * width + x) * 4 + 3]
|
||||
|
||||
expect(alphaAt(10, 36)).toBeGreaterThan(0)
|
||||
expect(alphaAt(22, 36)).toBe(0)
|
||||
|
||||
image.delete()
|
||||
} finally {
|
||||
surface.delete()
|
||||
}
|
||||
})
|
||||
})
|
||||
Loading…
Reference in a new issue