fix(canvas): preserve path text with loaded fonts
This commit is contained in:
parent
2e66792c59
commit
c361ec3c87
|
|
@ -947,16 +947,15 @@ function shouldClipTextToLayoutBox(node: SceneNode): boolean {
|
|||
)
|
||||
}
|
||||
|
||||
function drawSubstitutedPathText(
|
||||
function drawResolvedPathText(
|
||||
r: SkiaRenderer,
|
||||
canvas: Canvas,
|
||||
node: SceneNode,
|
||||
fontReadiness: ReturnType<SkiaRenderer['nodeFontReadiness']>
|
||||
): boolean {
|
||||
return (
|
||||
fontReadiness === 'substituted' &&
|
||||
node.textPathData !== null &&
|
||||
drawDerivedText(r, canvas, node)
|
||||
// Resolving the exact face must not replace curved glyph placement with a straight paragraph.
|
||||
fontReadiness !== 'exhausted' && node.textPathData !== null && drawDerivedText(r, canvas, node)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -974,7 +973,7 @@ export function renderText(r: SkiaRenderer, canvas: Canvas, node: SceneNode, fil
|
|||
canvas.restore()
|
||||
return
|
||||
}
|
||||
if (drawSubstitutedPathText(r, canvas, node, fontReadiness)) {
|
||||
if (drawResolvedPathText(r, canvas, node, fontReadiness)) {
|
||||
canvas.restore()
|
||||
return
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
Before Width: | Height: | Size: 350 KiB After Width: | Height: | Size: 250 KiB |
|
|
@ -1,14 +1,21 @@
|
|||
import { expect, test, useEditorSetupWithClear } from '#tests/e2e/fixtures'
|
||||
import { expect, test } from '#tests/e2e/fixtures'
|
||||
import { CanvasHelper } from '#tests/helpers/canvas'
|
||||
import { trackFontModuleResources } from '#tests/helpers/fonts/runtime'
|
||||
|
||||
const editor = useEditorSetupWithClear('/?test&no-chrome&no-rulers')
|
||||
|
||||
async function expectCanvas(name: string): Promise<void> {
|
||||
await editor.canvas.waitForRender()
|
||||
editor.canvas.assertNoErrors()
|
||||
expect(await editor.canvas.screenshotCanvasRegion()).toMatchSnapshot(`${name}.png`)
|
||||
async function expectCanvas(canvas: CanvasHelper, name: string): Promise<void> {
|
||||
await canvas.waitForRender()
|
||||
canvas.assertNoErrors()
|
||||
expect(await canvas.screenshotCanvasRegion()).toMatchSnapshot(`${name}.png`)
|
||||
}
|
||||
|
||||
test('first edit replaces baked missing-font glyphs with visible live substitution', async () => {
|
||||
test('first edit replaces baked missing-font glyphs with visible live substitution', async ({
|
||||
page
|
||||
}) => {
|
||||
await trackFontModuleResources(page)
|
||||
await page.goto('/?test&no-chrome&no-rulers')
|
||||
const editor = { page, canvas: new CanvasHelper(page) }
|
||||
await editor.canvas.waitForInit()
|
||||
await editor.canvas.clearCanvas()
|
||||
const textId = await editor.page.evaluate(() => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
if (!store?.renderer) throw new Error('OpenPencil renderer not initialized')
|
||||
|
|
@ -52,13 +59,13 @@ test('first edit replaces baked missing-font glyphs with visible live substituti
|
|||
const node = store?.graph.getNode(id)
|
||||
return node ? store?.renderer?.nodeFontReadiness(node) === 'substituted' : false
|
||||
}, textId)
|
||||
await expectCanvas('missing-font-baked-before-edit')
|
||||
await expectCanvas(editor.canvas, 'missing-font-baked-before-edit')
|
||||
|
||||
await editor.page.keyboard.press('Enter')
|
||||
const textarea = editor.page.locator('textarea[aria-hidden="true"]')
|
||||
await textarea.fill('!')
|
||||
await textarea.dispatchEvent('input')
|
||||
await expectCanvas('missing-font-live-substitution-after-first-edit')
|
||||
await expectCanvas(editor.canvas, 'missing-font-live-substitution-after-first-edit')
|
||||
|
||||
const edited = await editor.page.evaluate((id) => {
|
||||
const store = window.openPencil?.getStore?.()
|
||||
|
|
@ -81,5 +88,5 @@ test('first edit replaces baked missing-font glyphs with visible live substituti
|
|||
|
||||
await editor.page.keyboard.press('Escape')
|
||||
await editor.canvas.undo()
|
||||
await expectCanvas('missing-font-substitution-after-undo')
|
||||
await expectCanvas(editor.canvas, 'missing-font-substitution-after-undo')
|
||||
})
|
||||
|
|
|
|||
|
|
@ -382,6 +382,66 @@ describe('derived text rendering', () => {
|
|||
})
|
||||
|
||||
describe('rotated derived glyphs (text-on-path)', () => {
|
||||
test('paints the curved glyph placement on the first render with an already loaded exact font', async () => {
|
||||
const graph = new SceneGraph()
|
||||
const page = expectDefined(graph.getPages()[0], 'page')
|
||||
const text = graph.createNode('TEXT', page.id, {
|
||||
width: 200,
|
||||
height: 200,
|
||||
text: 'x',
|
||||
fontFamily: 'Inter',
|
||||
textPathData: {
|
||||
network: { vertices: [], segments: [], regions: [] },
|
||||
normalizedSize: { x: 200, y: 200 },
|
||||
tValue: 0,
|
||||
forward: true
|
||||
},
|
||||
fills: [{ type: 'SOLID', color: { r: 0, g: 0, b: 0, a: 1 }, opacity: 1, visible: true }],
|
||||
derivedTextGlyphs: [
|
||||
{
|
||||
commandsBlob: squareCommandsBlob(),
|
||||
x: 50,
|
||||
y: 50,
|
||||
fontSize: 20,
|
||||
rotation: -Math.PI / 2
|
||||
}
|
||||
]
|
||||
})
|
||||
const renderer = new SkiaRenderer(ck, expectDefined(ck.MakeSurface(1, 1), 'surface'))
|
||||
try {
|
||||
await renderer.loadFonts()
|
||||
expect(renderer.nodeFontReadiness(text)).toBe('ready')
|
||||
const png = expectDefined(
|
||||
renderNodesToImage(ck, renderer, graph, page.id, [text.id], {
|
||||
scale: 1,
|
||||
format: 'PNG'
|
||||
}),
|
||||
'png'
|
||||
)
|
||||
const image = expectDefined(ck.MakeImageFromEncoded(png), 'image')
|
||||
try {
|
||||
const pixels = expectDefined(
|
||||
image.readPixels(0, 0, {
|
||||
alphaType: ck.AlphaType.Unpremul,
|
||||
colorType: ck.ColorType.RGBA_8888,
|
||||
colorSpace: ck.ColorSpace.SRGB,
|
||||
width: image.width(),
|
||||
height: image.height()
|
||||
}),
|
||||
'pixels'
|
||||
)
|
||||
const alphaAt = (x: number, y: number) => pixels[(y * image.width() + x) * 4 + 3]
|
||||
// Font-space Y inversion and the stored rotation place the square at [50, 70]².
|
||||
expect(alphaAt(60, 60)).toBe(255)
|
||||
expect(alphaAt(10, 10)).toBe(0)
|
||||
} finally {
|
||||
image.delete()
|
||||
}
|
||||
} finally {
|
||||
renderer.destroy()
|
||||
}
|
||||
})
|
||||
|
||||
test('hasRotatedDerivedGlyphs detects non-zero rotation', () => {
|
||||
expect(
|
||||
hasRotatedDerivedGlyphs({
|
||||
|
|
|
|||
|
|
@ -232,36 +232,39 @@ describe('renderText', () => {
|
|||
expect(canvas.drawParagraph).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
test('keeps derived path-text glyphs when its face is finalized as substituted', () => {
|
||||
const base = createMockRenderer()
|
||||
const r = createMockRenderer({
|
||||
nodeFontReadiness: mock(() => 'substituted'),
|
||||
ck: { ...base.ck, FillType: { EvenOdd: 0, Winding: 1 } }
|
||||
})
|
||||
const canvas = createMockCanvas()
|
||||
const node = textNode({
|
||||
fontFamily: 'Missing Path Font',
|
||||
textPathData: {
|
||||
network: { vertices: [], segments: [], regions: [] },
|
||||
normalizedSize: { x: 100, y: 20 },
|
||||
tValue: 0,
|
||||
forward: true
|
||||
},
|
||||
derivedTextGlyphs: [
|
||||
{
|
||||
commandsBlob: new Uint8Array(),
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
fontSize: 12
|
||||
}
|
||||
]
|
||||
})
|
||||
test.each(['ready', 'substituted'] as const)(
|
||||
'keeps derived path-text glyphs when its face is %s',
|
||||
(readiness) => {
|
||||
const base = createMockRenderer()
|
||||
const r = createMockRenderer({
|
||||
nodeFontReadiness: mock(() => readiness),
|
||||
ck: { ...base.ck, FillType: { EvenOdd: 0, Winding: 1 } }
|
||||
})
|
||||
const canvas = createMockCanvas()
|
||||
const node = textNode({
|
||||
fontFamily: 'Missing Path Font',
|
||||
textPathData: {
|
||||
network: { vertices: [], segments: [], regions: [] },
|
||||
normalizedSize: { x: 100, y: 20 },
|
||||
tValue: 0,
|
||||
forward: true
|
||||
},
|
||||
derivedTextGlyphs: [
|
||||
{
|
||||
commandsBlob: new Uint8Array(),
|
||||
x: 0,
|
||||
y: 0,
|
||||
rotation: 0,
|
||||
fontSize: 12
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
renderText(r, canvas as never, node)
|
||||
renderText(r, canvas as never, node)
|
||||
|
||||
expect(r.buildParagraph).not.toHaveBeenCalled()
|
||||
})
|
||||
expect(r.buildParagraph).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
test('uses baked text pictures after font resolution is exhausted', () => {
|
||||
const r = createMockRenderer({ nodeFontReadiness: mock(() => 'exhausted') })
|
||||
const canvas = createMockCanvas()
|
||||
|
|
|
|||
Loading…
Reference in a new issue