fix(fig): limit target-aspect scaling
- Traverse fixed wrappers only to reach descendants with SCALE constraints - Preserve fixed siblings and use component geometry as the scale basis - Cover target-aspect icons without changing the Preline geometry baseline
This commit is contained in:
parent
fcfe259f26
commit
77263c1d48
|
|
@ -50,8 +50,8 @@
|
|||
### Fixed
|
||||
|
||||
- Scale proportion-constrained `.fig` instance geometry through fixed wrapper layers so imported logos and icons retain their intended size.
|
||||
- Match Figma auto-layout spacing, padding, min/max constraints, scalar variable bindings, imported text bounds, and nested instance geometry more closely.
|
||||
- Match Figma Plugin API vector path and network editing, including bounds, winding rules, region fills, validation, and handle mirroring. (#444)
|
||||
- Match Figma auto-layout spacing, padding, min/max constraints, scalar variable bindings, CanvasKit-shaped generated text, imported text bounds, and nested instance geometry more closely.
|
||||
- Match Figma Plugin API vector path and network editing, including bounds, transforms, winding rules, region fills, validation, and handle mirroring. (#444)
|
||||
- Let AI and MCP tools create arbitrary vectors from SVG path data, validating input without leaving blank layers behind. (#440)
|
||||
- Improve AI design accuracy by exposing every supported shape, including visible stroke colors and weights in visual descriptions, and accepting supported inline SVG attributes without false warnings. (#445, #447, #448)
|
||||
- Restore Anthropic AI connections in the web app instead of failing with a browser endpoint error. (#438)
|
||||
|
|
|
|||
|
|
@ -10,9 +10,14 @@ import { overrideCandidates } from './utils'
|
|||
|
||||
const MAX_CLONE_CHAIN_DEPTH = 10
|
||||
|
||||
interface ScaleDescendantAxes {
|
||||
horizontal: boolean
|
||||
vertical: boolean
|
||||
}
|
||||
|
||||
interface InstanceScale {
|
||||
basis: SceneNode
|
||||
scaleEntireSubtree: boolean
|
||||
scaleThroughFixedWrappers: boolean
|
||||
sx: number
|
||||
sy: number
|
||||
useCurrentChildAsSource: boolean
|
||||
|
|
@ -52,7 +57,7 @@ export function applyConstraintScaling(ctx: OverrideContext): void {
|
|||
ctx.geometryOverrideNodes,
|
||||
scale.useCurrentChildAsSource,
|
||||
strokeScale,
|
||||
scale.scaleEntireSubtree
|
||||
scale.scaleThroughFixedWrappers
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -68,13 +73,13 @@ function resolveInstanceScale(
|
|||
const resolvedBasis = resolveScaleBasis(graph, instance, component)
|
||||
if (!targetAspectRatio && !resolvedBasis) return null
|
||||
const basis = resolvedBasis ?? component
|
||||
const scaleEntireSubtree = targetAspectRatio !== null
|
||||
const scaleThroughFixedWrappers = targetAspectRatio !== null
|
||||
return {
|
||||
basis,
|
||||
scaleEntireSubtree,
|
||||
sx: instance.width / (targetAspectRatio?.width ?? basis.width),
|
||||
sy: instance.height / (targetAspectRatio?.height ?? basis.height),
|
||||
useCurrentChildAsSource: scaleEntireSubtree || basis !== component
|
||||
scaleThroughFixedWrappers,
|
||||
sx: instance.width / basis.width,
|
||||
sy: instance.height / basis.height,
|
||||
useCurrentChildAsSource: basis !== component
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -268,6 +273,40 @@ function scaledGeometryUpdates(
|
|||
return updates
|
||||
}
|
||||
|
||||
function scaleDescendantAxes(
|
||||
graph: SceneGraph,
|
||||
node: SceneNode,
|
||||
cache: Map<string, ScaleDescendantAxes>
|
||||
): ScaleDescendantAxes {
|
||||
const cached = cache.get(node.id)
|
||||
if (cached) return cached
|
||||
const result: ScaleDescendantAxes = { horizontal: false, vertical: false }
|
||||
for (const child of graph.getChildren(node.id)) {
|
||||
const nested = scaleDescendantAxes(graph, child, cache)
|
||||
result.horizontal ||= child.horizontalConstraint === 'SCALE' || nested.horizontal
|
||||
result.vertical ||= child.verticalConstraint === 'SCALE' || nested.vertical
|
||||
if (result.horizontal && result.vertical) break
|
||||
}
|
||||
cache.set(node.id, result)
|
||||
return result
|
||||
}
|
||||
|
||||
function childScaleAxes(
|
||||
graph: SceneGraph,
|
||||
child: SceneNode,
|
||||
scaleThroughFixedWrappers: boolean,
|
||||
cache: Map<string, ScaleDescendantAxes>
|
||||
): ScaleDescendantAxes {
|
||||
const descendantAxes = scaleDescendantAxes(graph, child, cache)
|
||||
return {
|
||||
horizontal:
|
||||
child.horizontalConstraint === 'SCALE' ||
|
||||
(scaleThroughFixedWrappers && descendantAxes.horizontal),
|
||||
vertical:
|
||||
child.verticalConstraint === 'SCALE' || (scaleThroughFixedWrappers && descendantAxes.vertical)
|
||||
}
|
||||
}
|
||||
|
||||
function scaleChildren(
|
||||
graph: SceneGraph,
|
||||
instance: SceneNode,
|
||||
|
|
@ -278,7 +317,8 @@ function scaleChildren(
|
|||
geometryOverrideNodes: Set<string>,
|
||||
useCurrentChildAsSource = false,
|
||||
strokeScale?: number,
|
||||
scaleEntireSubtree = false
|
||||
scaleThroughFixedWrappers = false,
|
||||
descendantScaleCache = new Map<string, ScaleDescendantAxes>()
|
||||
): void {
|
||||
const len = Math.min(instance.childIds.length, comp.childIds.length)
|
||||
for (let i = 0; i < len; i++) {
|
||||
|
|
@ -286,8 +326,9 @@ function scaleChildren(
|
|||
const compChild = graph.getNode(comp.childIds[i])
|
||||
if (!child || !compChild) continue
|
||||
|
||||
const hScale = scaleEntireSubtree || child.horizontalConstraint === 'SCALE'
|
||||
const vScale = scaleEntireSubtree || child.verticalConstraint === 'SCALE'
|
||||
const scaleAxes = childScaleAxes(graph, child, scaleThroughFixedWrappers, descendantScaleCache)
|
||||
const hScale = scaleAxes.horizontal
|
||||
const vScale = scaleAxes.vertical
|
||||
if (!hScale && !vScale) continue
|
||||
|
||||
const updates: Partial<SceneNode> = {}
|
||||
|
|
@ -321,7 +362,8 @@ function scaleChildren(
|
|||
geometryOverrideNodes,
|
||||
useCurrentChildAsSource,
|
||||
strokeScale,
|
||||
scaleEntireSubtree
|
||||
scaleThroughFixedWrappers,
|
||||
descendantScaleCache
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,7 +161,15 @@ describe('@open-pencil/fig instance interpretation', () => {
|
|||
const pageId = graph.getPages()[0].id
|
||||
const component = graph.createNode('COMPONENT', pageId, { width: 310, height: 62 })
|
||||
const wrapper = graph.createNode('FRAME', component.id, { width: 310, height: 61.214 })
|
||||
graph.createNode('VECTOR', wrapper.id, {
|
||||
const inset = graph.createNode('RECTANGLE', wrapper.id, {
|
||||
x: 250,
|
||||
y: 10,
|
||||
width: 20,
|
||||
height: 20,
|
||||
horizontalConstraint: 'MAX',
|
||||
verticalConstraint: 'MIN'
|
||||
})
|
||||
const vector = graph.createNode('VECTOR', wrapper.id, {
|
||||
width: 56.392,
|
||||
height: 61.214,
|
||||
horizontalConstraint: 'SCALE',
|
||||
|
|
@ -177,11 +185,48 @@ describe('@open-pencil/fig instance interpretation', () => {
|
|||
populateAndApplyOverrides(graph, new Map(), new Map())
|
||||
|
||||
const scaledWrapper = graph.getChildren(instance.id)[0]
|
||||
const scaledShape = graph.getChildren(scaledWrapper.id)[0]
|
||||
const scaledShape = graph.getChildren(scaledWrapper.id)[1]
|
||||
const preservedInset = graph.getChildren(scaledWrapper.id)[0]
|
||||
expect(scaledWrapper.width).toBeCloseTo(100)
|
||||
expect(scaledWrapper.height).toBeCloseTo((61.214 * 20) / 62)
|
||||
expect(scaledShape.width).toBeCloseTo((56.392 * 100) / 310)
|
||||
expect(scaledShape.height).toBeCloseTo((61.214 * 20) / 62)
|
||||
expect(preservedInset).toMatchObject({
|
||||
x: inset.x,
|
||||
y: inset.y,
|
||||
width: inset.width,
|
||||
height: inset.height
|
||||
})
|
||||
expect(scaledShape.width).toBeCloseTo((vector.width * 100) / 310)
|
||||
expect(scaledShape.height).toBeCloseTo((vector.height * 20) / 62)
|
||||
})
|
||||
|
||||
test('uses target-aspect metadata only to reach scale-constrained descendants', () => {
|
||||
const graph = new SceneGraph()
|
||||
const pageId = graph.getPages()[0].id
|
||||
const component = graph.createNode('COMPONENT', pageId, { width: 24, height: 24 })
|
||||
const vector = graph.createNode('VECTOR', component.id, {
|
||||
x: 9,
|
||||
y: 3,
|
||||
width: 6,
|
||||
height: 6,
|
||||
horizontalConstraint: 'SCALE',
|
||||
verticalConstraint: 'SCALE'
|
||||
})
|
||||
const instance = graph.createNode('INSTANCE', pageId, {
|
||||
width: 14,
|
||||
height: 14,
|
||||
componentId: component.id
|
||||
})
|
||||
instance.source.fig.rawNodeFields.targetAspectRatio = { value: { x: 32, y: 32 } }
|
||||
|
||||
populateAndApplyOverrides(graph, new Map(), new Map())
|
||||
|
||||
const scaledVector = graph.getChildren(instance.id)[0]
|
||||
expect(scaledVector).toMatchObject({
|
||||
x: (vector.x * 14) / 24,
|
||||
y: (vector.y * 14) / 24,
|
||||
width: (vector.width * 14) / 24,
|
||||
height: (vector.height * 14) / 24
|
||||
})
|
||||
})
|
||||
|
||||
test('limits lazy population to required global propagation scans', () => {
|
||||
|
|
|
|||
Loading…
Reference in a new issue