fix(clipboard): orient text glyph outlines
This commit is contained in:
parent
91f516ecaa
commit
358ac9ebf7
49
packages/core/src/kiwi/node-change/path-commands.ts
Normal file
49
packages/core/src/kiwi/node-change/path-commands.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
import type { OutlineCommand } from '#core/text/opentype'
|
||||
|
||||
const CMD_CLOSE = 0
|
||||
const CMD_MOVE_TO = 1
|
||||
const CMD_LINE_TO = 2
|
||||
const CMD_CUBIC_TO = 4
|
||||
|
||||
export function encodePathCommandsBlob(commands: OutlineCommand[], scale = 1): Uint8Array {
|
||||
const bytes: number[] = []
|
||||
const pushFloat = (value: number | undefined) => {
|
||||
const buf = new ArrayBuffer(4)
|
||||
new DataView(buf).setFloat32(0, (value ?? 0) / scale, true)
|
||||
bytes.push(...new Uint8Array(buf))
|
||||
}
|
||||
|
||||
for (const command of commands) {
|
||||
switch (command.type) {
|
||||
case 'M':
|
||||
bytes.push(CMD_MOVE_TO)
|
||||
pushFloat(command.x)
|
||||
pushFloat(command.y === undefined ? undefined : -command.y)
|
||||
break
|
||||
case 'L':
|
||||
bytes.push(CMD_LINE_TO)
|
||||
pushFloat(command.x)
|
||||
pushFloat(command.y === undefined ? undefined : -command.y)
|
||||
break
|
||||
case 'C':
|
||||
bytes.push(CMD_CUBIC_TO)
|
||||
pushFloat(command.x1)
|
||||
pushFloat(command.y1 === undefined ? undefined : -command.y1)
|
||||
pushFloat(command.x2)
|
||||
pushFloat(command.y2 === undefined ? undefined : -command.y2)
|
||||
pushFloat(command.x)
|
||||
pushFloat(command.y === undefined ? undefined : -command.y)
|
||||
break
|
||||
case 'Q':
|
||||
bytes.push(CMD_LINE_TO)
|
||||
pushFloat(command.x)
|
||||
pushFloat(command.y === undefined ? undefined : -command.y)
|
||||
break
|
||||
case 'Z':
|
||||
bytes.push(CMD_CLOSE)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return new Uint8Array(bytes)
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import { buildDerivedTextData as buildSharedDerivedTextData } from '#core/text/derived-text-data'
|
||||
import { normalizeFontFamily, weightToFigmaStyle, weightToStyle } from '#core/text/fonts'
|
||||
import { encodePathCommandsBlob } from '#core/kiwi/node-change/path-commands'
|
||||
import { getGlyphOutlineMetricsSync } from '#core/text/opentype'
|
||||
import { encodeVectorNetworkBlob, buildStyleOverrideTable } from '#core/vector'
|
||||
export {
|
||||
|
|
@ -109,7 +110,7 @@ function buildDerivedTextData(
|
|||
const glyphAdvance = node.text.length > 0 ? node.width / Math.max(node.text.length, 1) : 0
|
||||
|
||||
const glyphs = glyphMetrics.map((glyph, index) => ({
|
||||
commandsBlob: blobs.push(glyph.commandsBlob) - 1,
|
||||
commandsBlob: blobs.push(encodePathCommandsBlob(glyph.commands, node.fontSize)) - 1,
|
||||
position: { x: glyph.x || index * glyphAdvance, y: lineHeight },
|
||||
fontSize: node.fontSize,
|
||||
firstCharacter: index,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import type { NodeChange } from '#core/kiwi/binary/codec'
|
||||
import type { SceneNode } from '#core/scene-graph'
|
||||
|
||||
import { encodePathCommandsBlob } from '#core/kiwi/node-change/path-commands'
|
||||
|
||||
import { buildDerivedTextData } from './derived-text-data'
|
||||
import { normalizeFontFamily, weightToFigmaStyle, weightToStyle } from './fonts'
|
||||
import { getGlyphOutlineMetricsSync } from './opentype'
|
||||
|
|
@ -32,16 +34,40 @@ export async function buildDerivedTextDataV4(
|
|||
const glyphMetrics = getGlyphOutlineMetricsSync(node.fontFamily, style, node.text, node.fontSize) ?? []
|
||||
|
||||
const fallbackAdvance = node.text.length > 0 ? node.width / Math.max(node.text.length, 1) : 0
|
||||
const fallbackBaselines: NonNullable<NodeChange['derivedTextData']>['baselines'] = []
|
||||
const fallbackOffsets = Array.from({ length: node.text.length + 1 }, () => 0)
|
||||
let fallbackX = 0
|
||||
let fallbackY = lineHeightFallback
|
||||
let lineStart = 0
|
||||
const lineAscent = Math.max(lineHeightFallback - node.fontSize * 0.2, 0)
|
||||
|
||||
const glyphs = glyphMetrics.map((glyph, index) => {
|
||||
const shapedGlyph = shaped?.glyphs[index]
|
||||
const fallbackX = glyph.x || index * fallbackAdvance
|
||||
const fallbackGlyphAdvance = glyph.advance || fallbackAdvance
|
||||
const commandsBlob = blobs ? blobs.push(glyph.commandsBlob) - 1 : undefined
|
||||
if (!shapedGlyph && fallbackX > 0 && fallbackX + fallbackGlyphAdvance > node.width) {
|
||||
fallbackBaselines.push({
|
||||
firstCharacter: lineStart,
|
||||
endCharacter: Math.max(index - 1, lineStart),
|
||||
position: { x: 0, y: fallbackY },
|
||||
width: fallbackX,
|
||||
lineHeight: lineHeightFallback,
|
||||
lineAscent
|
||||
})
|
||||
lineStart = index
|
||||
fallbackX = 0
|
||||
fallbackY += lineHeightFallback
|
||||
}
|
||||
const glyphX = fallbackX
|
||||
fallbackOffsets[index] = glyphX
|
||||
fallbackX += fallbackGlyphAdvance
|
||||
const commandsBlob = blobs
|
||||
? blobs.push(encodePathCommandsBlob(glyph.commands, node.fontSize)) - 1
|
||||
: undefined
|
||||
return {
|
||||
commandsBlob,
|
||||
position: {
|
||||
x: shapedGlyph?.x ?? fallbackX,
|
||||
y: shapedGlyph?.y ?? shaped?.baseline ?? lineHeightFallback
|
||||
x: shapedGlyph?.x ?? glyphX,
|
||||
y: shapedGlyph?.y ?? shaped?.baseline ?? fallbackY
|
||||
},
|
||||
fontSize: node.fontSize,
|
||||
firstCharacter: shapedGlyph?.firstCharacter ?? index,
|
||||
|
|
@ -49,6 +75,17 @@ export async function buildDerivedTextDataV4(
|
|||
rotation: 0
|
||||
}
|
||||
})
|
||||
fallbackOffsets[node.text.length] = fallbackX
|
||||
if (node.text.length > 0) {
|
||||
fallbackBaselines.push({
|
||||
firstCharacter: lineStart,
|
||||
endCharacter: node.text.length - 1,
|
||||
position: { x: 0, y: fallbackY },
|
||||
width: fallbackX,
|
||||
lineHeight: lineHeightFallback,
|
||||
lineAscent
|
||||
})
|
||||
}
|
||||
|
||||
return buildDerivedTextData({
|
||||
node,
|
||||
|
|
@ -65,9 +102,8 @@ export async function buildDerivedTextDataV4(
|
|||
baseline: shaped?.baseline ?? lineHeightFallback,
|
||||
width: shaped?.lineWidth ?? node.width,
|
||||
lineHeight: shaped?.lineHeight ?? lineHeightFallback,
|
||||
lineAscent: shaped?.lineAscent ?? Math.max(lineHeightFallback - node.fontSize * 0.2, 0),
|
||||
logicalIndexToCharacterOffsetMap:
|
||||
shaped?.logicalIndexToCharacterOffsetMap ??
|
||||
Array.from({ length: node.text.length + 1 }, (_, index) => index * fallbackAdvance)
|
||||
lineAscent: shaped?.lineAscent ?? lineAscent,
|
||||
baselines: shaped ? undefined : fallbackBaselines,
|
||||
logicalIndexToCharacterOffsetMap: shaped?.logicalIndexToCharacterOffsetMap ?? fallbackOffsets
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ interface DerivedTextDataOptions {
|
|||
width: number
|
||||
lineHeight: number
|
||||
lineAscent: number
|
||||
baselines?: NonNullable<NodeChange['derivedTextData']>['baselines']
|
||||
logicalIndexToCharacterOffsetMap: number[]
|
||||
}
|
||||
|
||||
|
|
@ -17,7 +18,7 @@ export function buildDerivedTextData(
|
|||
): NodeChange['derivedTextData'] {
|
||||
return {
|
||||
layoutSize: { x: options.node.width, y: options.node.height },
|
||||
baselines: [
|
||||
baselines: options.baselines ?? [
|
||||
{
|
||||
firstCharacter: 0,
|
||||
endCharacter: Math.max(options.node.text.length - 1, 0),
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import * as OpenTypeSync from 'opentype.js'
|
|||
|
||||
import { fontManager } from './fonts'
|
||||
|
||||
interface OutlineCommand {
|
||||
export interface OutlineCommand {
|
||||
type: string
|
||||
x?: number
|
||||
y?: number
|
||||
|
|
@ -81,59 +81,11 @@ export function measureTextWithOpenType(
|
|||
}
|
||||
|
||||
export interface GlyphOutlineMetrics {
|
||||
commandsBlob: Uint8Array
|
||||
commands: OutlineCommand[]
|
||||
x: number
|
||||
advance: number
|
||||
}
|
||||
|
||||
const CMD_CLOSE = 0
|
||||
const CMD_MOVE_TO = 1
|
||||
const CMD_LINE_TO = 2
|
||||
const CMD_CUBIC_TO = 4
|
||||
|
||||
function commandsToBlob(commands: OutlineCommand[], fontSize: number): Uint8Array {
|
||||
const bytes: number[] = []
|
||||
const pushFloat = (value: number | undefined) => {
|
||||
const buf = new ArrayBuffer(4)
|
||||
new DataView(buf).setFloat32(0, (value ?? 0) / fontSize, true)
|
||||
bytes.push(...new Uint8Array(buf))
|
||||
}
|
||||
|
||||
for (const command of commands) {
|
||||
switch (command.type) {
|
||||
case 'M':
|
||||
bytes.push(CMD_MOVE_TO)
|
||||
pushFloat(command.x)
|
||||
pushFloat(command.y)
|
||||
break
|
||||
case 'L':
|
||||
bytes.push(CMD_LINE_TO)
|
||||
pushFloat(command.x)
|
||||
pushFloat(command.y)
|
||||
break
|
||||
case 'C':
|
||||
bytes.push(CMD_CUBIC_TO)
|
||||
pushFloat(command.x1)
|
||||
pushFloat(command.y1)
|
||||
pushFloat(command.x2)
|
||||
pushFloat(command.y2)
|
||||
pushFloat(command.x)
|
||||
pushFloat(command.y)
|
||||
break
|
||||
case 'Q':
|
||||
bytes.push(CMD_LINE_TO)
|
||||
pushFloat(command.x)
|
||||
pushFloat(command.y)
|
||||
break
|
||||
case 'Z':
|
||||
bytes.push(CMD_CLOSE)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return new Uint8Array(bytes)
|
||||
}
|
||||
|
||||
export function getGlyphOutlineMetricsSync(
|
||||
family: string,
|
||||
style: string,
|
||||
|
|
@ -147,9 +99,9 @@ export function getGlyphOutlineMetricsSync(
|
|||
let x = 0
|
||||
const scale = fontSize / font.unitsPerEm
|
||||
return glyphs.map((glyph) => {
|
||||
const commandsBlob = commandsToBlob(glyph.getPath(0, 0, fontSize).commands, fontSize)
|
||||
const commands = glyph.getPath(0, 0, fontSize).commands
|
||||
const advance = (glyph.advanceWidth ?? 0) * scale
|
||||
const metrics = { commandsBlob, x, advance }
|
||||
const metrics = { commands, x, advance }
|
||||
x += advance
|
||||
return metrics
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in a new issue