Merge branch 'fix-vector-rendering' into compatibility-fixes
This commit is contained in:
commit
8a6746fa9d
|
|
@ -218,7 +218,8 @@ export function importClipboardNodes(
|
|||
populateAndApplyOverrides(
|
||||
graph,
|
||||
guidMap as unknown as Map<string, InstanceNodeChange>,
|
||||
created
|
||||
created,
|
||||
blobs
|
||||
)
|
||||
|
||||
for (const figmaId of internalTopLevel) {
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ export {
|
|||
type VectorVertex,
|
||||
type VectorSegment,
|
||||
type VectorRegion,
|
||||
type GeometryPath,
|
||||
type HandleMirroring,
|
||||
type WindingRule,
|
||||
type VariableType,
|
||||
|
|
@ -68,6 +69,7 @@ export {
|
|||
export { parseColor, colorToHex, colorToHexRaw, colorToRgba255 } from './color'
|
||||
export {
|
||||
vectorNetworkToPath,
|
||||
geometryBlobToPath,
|
||||
decodeVectorNetworkBlob,
|
||||
encodeVectorNetworkBlob,
|
||||
computeVectorBounds
|
||||
|
|
|
|||
|
|
@ -307,6 +307,21 @@ export function sceneNodeToKiwi(
|
|||
}
|
||||
}
|
||||
|
||||
if (node.fillGeometry.length > 0) {
|
||||
nc.fillGeometry = node.fillGeometry.map((g) => {
|
||||
const blobIdx = blobs.length
|
||||
blobs.push(g.commandsBlob)
|
||||
return { windingRule: g.windingRule, commandsBlob: blobIdx }
|
||||
})
|
||||
}
|
||||
if (node.strokeGeometry.length > 0) {
|
||||
nc.strokeGeometry = node.strokeGeometry.map((g) => {
|
||||
const blobIdx = blobs.length
|
||||
blobs.push(g.commandsBlob)
|
||||
return { windingRule: g.windingRule, commandsBlob: blobIdx }
|
||||
})
|
||||
}
|
||||
|
||||
const result: KiwiNodeChange[] = [nc]
|
||||
const children = graph.getChildren(node.id)
|
||||
for (let i = 0; i < children.length; i++) {
|
||||
|
|
|
|||
|
|
@ -254,6 +254,8 @@ export interface NodeChange {
|
|||
frameMaskDisabled?: boolean
|
||||
// Vector
|
||||
vectorData?: unknown
|
||||
fillGeometry?: Array<{ windingRule?: string; commandsBlob?: number }>
|
||||
strokeGeometry?: Array<{ windingRule?: string; commandsBlob?: number }>
|
||||
// Text
|
||||
fontSize?: number
|
||||
fontWeight?: number
|
||||
|
|
|
|||
|
|
@ -182,7 +182,8 @@ export function importNodeChanges(
|
|||
populateAndApplyOverrides(
|
||||
graph,
|
||||
changeMap as unknown as Map<string, InstanceNodeChange>,
|
||||
guidToNodeId
|
||||
guidToNodeId,
|
||||
blobs
|
||||
)
|
||||
|
||||
// Ensure at least one page exists
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import type { SceneGraph, SceneNode } from '../scene-graph'
|
||||
import { guidToString, convertOverrideToProps } from './kiwi-convert'
|
||||
import { guidToString, convertOverrideToProps, resolveGeometryPaths } from './kiwi-convert'
|
||||
import type { GUID } from './codec'
|
||||
|
||||
interface SymbolOverride {
|
||||
|
|
@ -27,6 +27,9 @@ interface ComponentPropAssignment {
|
|||
interface DerivedSymbolOverride {
|
||||
guidPath?: { guids?: GUID[] }
|
||||
size?: { x: number; y: number }
|
||||
transform?: { m00: number; m01: number; m02: number; m10: number; m11: number; m12: number }
|
||||
fillGeometry?: Array<{ windingRule?: string; commandsBlob?: number }>
|
||||
strokeGeometry?: Array<{ windingRule?: string; commandsBlob?: number }>
|
||||
}
|
||||
|
||||
export interface InstanceNodeChange {
|
||||
|
|
@ -54,7 +57,8 @@ export interface InstanceNodeChange {
|
|||
export function populateAndApplyOverrides(
|
||||
graph: SceneGraph,
|
||||
changeMap: Map<string, InstanceNodeChange>,
|
||||
guidToNodeId: Map<string, string>
|
||||
guidToNodeId: Map<string, string>,
|
||||
blobs: Uint8Array[] = []
|
||||
): void {
|
||||
// Iterative population: cloning creates new instances that themselves need children
|
||||
let populated = 1
|
||||
|
|
@ -252,6 +256,33 @@ export function populateAndApplyOverrides(
|
|||
// Apply derivedSymbolData — pre-computed sizes for the current set of
|
||||
// component property values. Uses the same guidPath resolution as
|
||||
// symbolOverrides.
|
||||
function scaleGeometryBlobs(
|
||||
geom: import('../scene-graph').GeometryPath[],
|
||||
sx: number,
|
||||
sy: number
|
||||
): import('../scene-graph').GeometryPath[] {
|
||||
if (sx === 1 && sy === 1) return geom
|
||||
return geom.map((g) => {
|
||||
const src = g.commandsBlob
|
||||
const scaled = new Uint8Array(src.length)
|
||||
scaled.set(src)
|
||||
const dv = new DataView(scaled.buffer, scaled.byteOffset, scaled.byteLength)
|
||||
let o = 0
|
||||
while (o < scaled.length) {
|
||||
const cmd = scaled[o++]
|
||||
if (cmd === 0) continue
|
||||
const coords = cmd === 1 || cmd === 2 ? 1 : cmd === 4 ? 3 : -1
|
||||
if (coords < 0) break
|
||||
for (let i = 0; i < coords; i++) {
|
||||
dv.setFloat32(o, dv.getFloat32(o, true) * sx, true)
|
||||
dv.setFloat32(o + 4, dv.getFloat32(o + 4, true) * sy, true)
|
||||
o += 8
|
||||
}
|
||||
}
|
||||
return { windingRule: g.windingRule, commandsBlob: scaled }
|
||||
})
|
||||
}
|
||||
|
||||
function applyDerivedSymbolData() {
|
||||
for (const [ncId, nc] of changeMap) {
|
||||
if (nc.type !== 'INSTANCE') continue
|
||||
|
|
@ -264,12 +295,38 @@ export function populateAndApplyOverrides(
|
|||
for (const d of derived) {
|
||||
const guids = d.guidPath?.guids
|
||||
if (!guids?.length) continue
|
||||
if (!d.size) continue
|
||||
|
||||
const targetId = resolveOverrideTarget(nodeId, guids)
|
||||
if (!targetId) continue
|
||||
|
||||
graph.updateNode(targetId, { width: d.size.x, height: d.size.y })
|
||||
const target = graph.getNode(targetId)
|
||||
if (!target) continue
|
||||
|
||||
const updates: Partial<SceneNode> = {}
|
||||
if (d.size) {
|
||||
updates.width = d.size.x
|
||||
updates.height = d.size.y
|
||||
}
|
||||
if (d.transform) {
|
||||
updates.x = d.transform.m02
|
||||
updates.y = d.transform.m12
|
||||
}
|
||||
const fg = resolveGeometryPaths(d.fillGeometry, blobs)
|
||||
const sg = resolveGeometryPaths(d.strokeGeometry, blobs)
|
||||
if (fg.length > 0) {
|
||||
updates.fillGeometry = fg
|
||||
} else if (d.size && target.fillGeometry.length > 0 && target.width > 0 && target.height > 0) {
|
||||
updates.fillGeometry = scaleGeometryBlobs(target.fillGeometry, d.size.x / target.width, d.size.y / target.height)
|
||||
}
|
||||
if (sg.length > 0) {
|
||||
updates.strokeGeometry = sg
|
||||
} else if (d.size && target.strokeGeometry.length > 0 && target.width > 0 && target.height > 0) {
|
||||
updates.strokeGeometry = scaleGeometryBlobs(target.strokeGeometry, d.size.x / target.width, d.size.y / target.height)
|
||||
}
|
||||
|
||||
if (Object.keys(updates).length > 0) {
|
||||
graph.updateNode(targetId, updates)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,8 +26,10 @@ import type {
|
|||
TextDecoration,
|
||||
ArcData,
|
||||
VectorNetwork,
|
||||
GeometryPath,
|
||||
StyleRun,
|
||||
CharacterStyleOverride
|
||||
CharacterStyleOverride,
|
||||
WindingRule
|
||||
} from '../scene-graph'
|
||||
import type { NodeChange, Paint, Effect as KiwiEffect, GUID } from './codec'
|
||||
|
||||
|
|
@ -390,6 +392,30 @@ function resolveVectorNetwork(
|
|||
}
|
||||
}
|
||||
|
||||
interface KiwiPath {
|
||||
windingRule?: string
|
||||
commandsBlob?: number
|
||||
}
|
||||
|
||||
export function resolveGeometryPaths(
|
||||
paths: KiwiPath[] | undefined,
|
||||
blobs: Uint8Array[]
|
||||
): GeometryPath[] {
|
||||
if (!paths || paths.length === 0) return []
|
||||
const result: GeometryPath[] = []
|
||||
for (const p of paths) {
|
||||
if (p.commandsBlob === undefined || p.commandsBlob < 0 || p.commandsBlob >= blobs.length)
|
||||
continue
|
||||
const blob = blobs[p.commandsBlob]
|
||||
if (!blob || blob.length === 0) continue
|
||||
result.push({
|
||||
windingRule: (p.windingRule === 'EVENODD' ? 'EVENODD' : 'NONZERO') as WindingRule,
|
||||
commandsBlob: blob
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
function extractBoundVariables(nc: NodeChange): Record<string, string> {
|
||||
const bindings: Record<string, string> = {}
|
||||
nc.fillPaints?.forEach((paint, i) => {
|
||||
|
|
@ -493,6 +519,8 @@ export function nodeChangeToProps(
|
|||
layoutGrow: (ext(nc).stackChildPrimaryGrow as number) ?? 0,
|
||||
layoutAlignSelf: (ext(nc).stackChildAlignSelf as string) === 'STRETCH' ? 'STRETCH' : 'AUTO',
|
||||
vectorNetwork: resolveVectorNetwork(nc, blobs),
|
||||
fillGeometry: resolveGeometryPaths(nc.fillGeometry, blobs),
|
||||
strokeGeometry: resolveGeometryPaths(nc.strokeGeometry, blobs),
|
||||
arcData: mapArcData(ext(nc).arcData as Record<string, number> | undefined),
|
||||
strokeCap: (nc.strokeCap ?? 'NONE') as StrokeCap,
|
||||
strokeJoin: (nc.strokeJoin ?? 'MITER') as StrokeJoin,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ import {
|
|||
DEFAULT_FONT_FAMILY
|
||||
} from './constants'
|
||||
import { isFontLoaded } from './fonts'
|
||||
import { vectorNetworkToPath } from './vector'
|
||||
import { vectorNetworkToPath, geometryBlobToPath } from './vector'
|
||||
import { RenderProfiler } from './profiler'
|
||||
|
||||
import type { SceneNode, SceneGraph, Fill, Stroke } from './scene-graph'
|
||||
|
|
@ -143,7 +143,9 @@ export class SkiaRenderer {
|
|||
private fontProvider: TypefaceFontProvider | null = null
|
||||
private fontsLoaded = false
|
||||
private imageCache = new Map<string, CKImage>()
|
||||
private vectorPathCache = new Map<string, Path>()
|
||||
private vectorPathCache = new Map<string, Path[]>()
|
||||
private fillGeometryCache = new Map<string, Path[]>()
|
||||
private strokeGeometryCache = new Map<string, Path[]>()
|
||||
private scenePicture: SkPicture | null = null
|
||||
private scenePictureVersion = -1
|
||||
private scenePicturePageId: string | null = null
|
||||
|
|
@ -1153,21 +1155,50 @@ export class SkiaRenderer {
|
|||
canvas.restore()
|
||||
}
|
||||
|
||||
private getVectorPath(node: SceneNode): Path | null {
|
||||
private getVectorPaths(node: SceneNode): Path[] | null {
|
||||
if (!node.vectorNetwork) return null
|
||||
const cached = this.vectorPathCache.get(node.id)
|
||||
if (cached) return cached
|
||||
const path = vectorNetworkToPath(this.ck, node.vectorNetwork)
|
||||
this.vectorPathCache.set(node.id, path)
|
||||
return path
|
||||
const paths = vectorNetworkToPath(this.ck, node.vectorNetwork)
|
||||
this.vectorPathCache.set(node.id, paths)
|
||||
return paths
|
||||
}
|
||||
|
||||
private getFillGeometry(node: SceneNode): Path[] | null {
|
||||
if (node.fillGeometry.length === 0) return null
|
||||
const cached = this.fillGeometryCache.get(node.id)
|
||||
if (cached) return cached
|
||||
const paths = node.fillGeometry.map((g) =>
|
||||
geometryBlobToPath(this.ck, g.commandsBlob, g.windingRule)
|
||||
)
|
||||
this.fillGeometryCache.set(node.id, paths)
|
||||
return paths
|
||||
}
|
||||
|
||||
private getStrokeGeometry(node: SceneNode): Path[] | null {
|
||||
if (node.strokeGeometry.length === 0) return null
|
||||
const cached = this.strokeGeometryCache.get(node.id)
|
||||
if (cached) return cached
|
||||
const paths = node.strokeGeometry.map((g) =>
|
||||
geometryBlobToPath(this.ck, g.commandsBlob, g.windingRule)
|
||||
)
|
||||
this.strokeGeometryCache.set(node.id, paths)
|
||||
return paths
|
||||
}
|
||||
|
||||
invalidateVectorPath(nodeId: string): void {
|
||||
const old = this.vectorPathCache.get(nodeId)
|
||||
if (old) {
|
||||
old.delete()
|
||||
for (const p of old) p.delete()
|
||||
this.vectorPathCache.delete(nodeId)
|
||||
}
|
||||
for (const cache of [this.fillGeometryCache, this.strokeGeometryCache]) {
|
||||
const oldGeom = cache.get(nodeId)
|
||||
if (oldGeom) {
|
||||
for (const p of oldGeom) p.delete()
|
||||
cache.delete(nodeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private strokeNodeShape(canvas: Canvas, node: SceneNode, paint: Paint): void {
|
||||
|
|
@ -1178,8 +1209,10 @@ export class SkiaRenderer {
|
|||
canvas.drawOval(rect, paint)
|
||||
return
|
||||
case 'VECTOR': {
|
||||
const vp = this.getVectorPath(node)
|
||||
if (vp) canvas.drawPath(vp, paint)
|
||||
const vps = this.getVectorPaths(node)
|
||||
if (vps) {
|
||||
for (const vp of vps) canvas.drawPath(vp, paint)
|
||||
}
|
||||
return
|
||||
}
|
||||
case 'LINE':
|
||||
|
|
@ -1548,10 +1581,51 @@ export class SkiaRenderer {
|
|||
}
|
||||
|
||||
// Strokes
|
||||
const sg = node.type === 'VECTOR' ? this.getStrokeGeometry(node) : null
|
||||
const vectorPaths = !sg && node.type === 'VECTOR' ? this.getVectorPaths(node) : null
|
||||
for (let si = 0; si < node.strokes.length; si++) {
|
||||
const stroke = node.strokes[si]!
|
||||
if (!stroke.visible) continue
|
||||
const sc = this.resolveStrokeColor(stroke, si, node, graph)
|
||||
|
||||
if (sg) {
|
||||
this.fillPaint.setColor(this.ck.Color4f(sc.r, sc.g, sc.b, sc.a))
|
||||
this.fillPaint.setAlphaf(stroke.opacity)
|
||||
this.fillPaint.setShader(null)
|
||||
for (const p of sg) canvas.drawPath(p, this.fillPaint)
|
||||
continue
|
||||
}
|
||||
|
||||
if (vectorPaths) {
|
||||
const capMap: Record<string, EmbindEnumEntity> = {
|
||||
NONE: this.ck.StrokeCap.Butt,
|
||||
ROUND: this.ck.StrokeCap.Round,
|
||||
SQUARE: this.ck.StrokeCap.Square
|
||||
}
|
||||
const joinMap: Record<string, EmbindEnumEntity> = {
|
||||
MITER: this.ck.StrokeJoin.Miter,
|
||||
ROUND: this.ck.StrokeJoin.Round,
|
||||
BEVEL: this.ck.StrokeJoin.Bevel
|
||||
}
|
||||
const strokeOpts = {
|
||||
width: stroke.weight,
|
||||
miter_limit: 4,
|
||||
cap: capMap[stroke.cap ?? 'NONE'] ?? this.ck.StrokeCap.Butt,
|
||||
join: joinMap[stroke.join ?? 'MITER'] ?? this.ck.StrokeJoin.Miter
|
||||
}
|
||||
this.fillPaint.setColor(this.ck.Color4f(sc.r, sc.g, sc.b, sc.a))
|
||||
this.fillPaint.setAlphaf(stroke.opacity)
|
||||
this.fillPaint.setShader(null)
|
||||
for (const vp of vectorPaths) {
|
||||
const outline = vp.copy().stroke(strokeOpts)
|
||||
if (outline) {
|
||||
canvas.drawPath(outline, this.fillPaint)
|
||||
outline.delete()
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
this.strokePaint.setColor(this.ck.Color4f(sc.r, sc.g, sc.b, sc.a))
|
||||
this.strokePaint.setStrokeWidth(stroke.weight)
|
||||
this.strokePaint.setAlphaf(stroke.opacity)
|
||||
|
|
@ -1597,8 +1671,15 @@ export class SkiaRenderer {
|
|||
): void {
|
||||
switch (node.type) {
|
||||
case 'VECTOR': {
|
||||
const vp = this.getVectorPath(node)
|
||||
if (vp) canvas.drawPath(vp, this.fillPaint)
|
||||
const fg = this.getFillGeometry(node)
|
||||
if (fg) {
|
||||
for (const p of fg) canvas.drawPath(p, this.fillPaint)
|
||||
} else {
|
||||
const vps = this.getVectorPaths(node)
|
||||
if (vps) {
|
||||
for (const vp of vps) canvas.drawPath(vp, this.fillPaint)
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'ELLIPSE':
|
||||
|
|
@ -1638,8 +1719,10 @@ export class SkiaRenderer {
|
|||
): void {
|
||||
switch (node.type) {
|
||||
case 'VECTOR': {
|
||||
const vp = this.getVectorPath(node)
|
||||
if (vp) canvas.drawPath(vp, this.strokePaint)
|
||||
const vps = this.getVectorPaths(node)
|
||||
if (vps) {
|
||||
for (const vp of vps) canvas.drawPath(vp, this.strokePaint)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'ELLIPSE':
|
||||
|
|
@ -1762,8 +1845,10 @@ export class SkiaRenderer {
|
|||
path.addOval(rect)
|
||||
break
|
||||
case 'VECTOR': {
|
||||
const vp = this.getVectorPath(node)
|
||||
if (vp) path.addPath(vp)
|
||||
const vps = this.getVectorPaths(node)
|
||||
if (vps) {
|
||||
for (const vp of vps) path.addPath(vp)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'POLYGON':
|
||||
|
|
@ -2977,8 +3062,12 @@ export class SkiaRenderer {
|
|||
|
||||
for (const img of this.imageCache.values()) img.delete()
|
||||
this.imageCache.clear()
|
||||
for (const p of this.vectorPathCache.values()) p.delete()
|
||||
this.vectorPathCache.clear()
|
||||
for (const cache of [this.vectorPathCache, this.fillGeometryCache, this.strokeGeometryCache]) {
|
||||
for (const paths of cache.values()) {
|
||||
for (const p of paths) p.delete()
|
||||
}
|
||||
cache.clear()
|
||||
}
|
||||
this.fillPaint.delete()
|
||||
this.strokePaint.delete()
|
||||
this.selectionPaint.delete()
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ export interface VectorNetwork {
|
|||
regions: VectorRegion[]
|
||||
}
|
||||
|
||||
export interface GeometryPath {
|
||||
windingRule: WindingRule
|
||||
commandsBlob: Uint8Array
|
||||
}
|
||||
|
||||
export type NodeType =
|
||||
| 'CANVAS'
|
||||
| 'FRAME'
|
||||
|
|
@ -227,6 +232,8 @@ export interface SceneNode {
|
|||
layoutAlignSelf: 'AUTO' | 'STRETCH'
|
||||
|
||||
vectorNetwork: VectorNetwork | null
|
||||
fillGeometry: GeometryPath[]
|
||||
strokeGeometry: GeometryPath[]
|
||||
|
||||
arcData: ArcData | null
|
||||
|
||||
|
|
@ -357,6 +364,8 @@ function createDefaultNode(type: NodeType, overrides: Partial<SceneNode> = {}):
|
|||
layoutGrow: 0,
|
||||
layoutAlignSelf: 'AUTO',
|
||||
vectorNetwork: null,
|
||||
fillGeometry: [],
|
||||
strokeGeometry: [],
|
||||
arcData: null,
|
||||
textAlignVertical: 'TOP',
|
||||
textAutoResize: 'NONE',
|
||||
|
|
|
|||
|
|
@ -172,43 +172,48 @@ export function encodeVectorNetworkBlob(network: VectorNetwork): Uint8Array {
|
|||
return new Uint8Array(buf)
|
||||
}
|
||||
|
||||
export function vectorNetworkToPath(ck: CanvasKit, network: VectorNetwork): Path {
|
||||
const path = new ck.Path()
|
||||
export function vectorNetworkToPath(ck: CanvasKit, network: VectorNetwork): Path[] {
|
||||
const { vertices, segments, regions } = network
|
||||
|
||||
if (regions.length > 0) {
|
||||
const paths: Path[] = []
|
||||
for (const region of regions) {
|
||||
const regionPath = new ck.Path()
|
||||
for (const loop of region.loops) {
|
||||
addLoopToPath(path, loop, segments, vertices)
|
||||
addLoopToPath(regionPath, loop, segments, vertices)
|
||||
}
|
||||
path.setFillType(region.windingRule === 'EVENODD' ? ck.FillType.EvenOdd : ck.FillType.Winding)
|
||||
regionPath.setFillType(
|
||||
region.windingRule === 'EVENODD' ? ck.FillType.EvenOdd : ck.FillType.Winding
|
||||
)
|
||||
paths.push(regionPath)
|
||||
}
|
||||
} else {
|
||||
// No regions — draw all segments as open paths
|
||||
const visited = new Set<number>()
|
||||
const chains = buildChains(segments, vertices.length)
|
||||
return paths
|
||||
}
|
||||
|
||||
for (const chain of chains) {
|
||||
if (chain.length === 0) continue
|
||||
const firstSeg = segments[chain[0]]
|
||||
path.moveTo(vertices[firstSeg.start].x, vertices[firstSeg.start].y)
|
||||
// No regions — draw all segments as open paths
|
||||
const path = new ck.Path()
|
||||
const visited = new Set<number>()
|
||||
const chains = buildChains(segments, vertices.length)
|
||||
|
||||
for (const segIdx of chain) {
|
||||
visited.add(segIdx)
|
||||
addSegmentToPath(path, segments[segIdx], vertices)
|
||||
}
|
||||
}
|
||||
for (const chain of chains) {
|
||||
if (chain.length === 0) continue
|
||||
const firstSeg = segments[chain[0]]
|
||||
path.moveTo(vertices[firstSeg.start].x, vertices[firstSeg.start].y)
|
||||
|
||||
// Any remaining disconnected segments
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
if (visited.has(i)) continue
|
||||
const seg = segments[i]
|
||||
path.moveTo(vertices[seg.start].x, vertices[seg.start].y)
|
||||
addSegmentToPath(path, seg, vertices)
|
||||
for (const segIdx of chain) {
|
||||
visited.add(segIdx)
|
||||
addSegmentToPath(path, segments[segIdx], vertices)
|
||||
}
|
||||
}
|
||||
|
||||
return path
|
||||
for (let i = 0; i < segments.length; i++) {
|
||||
if (visited.has(i)) continue
|
||||
const seg = segments[i]
|
||||
path.moveTo(vertices[seg.start].x, vertices[seg.start].y)
|
||||
addSegmentToPath(path, seg, vertices)
|
||||
}
|
||||
|
||||
return [path]
|
||||
}
|
||||
|
||||
function addLoopToPath(
|
||||
|
|
@ -331,3 +336,57 @@ export function computeVectorBounds(network: VectorNetwork): {
|
|||
|
||||
return { x: minX, y: minY, width: maxX - minX, height: maxY - minY }
|
||||
}
|
||||
|
||||
const CMD_CLOSE = 0
|
||||
const CMD_MOVE_TO = 1
|
||||
const CMD_LINE_TO = 2
|
||||
const CMD_CUBIC_TO = 4
|
||||
|
||||
export function geometryBlobToPath(
|
||||
ck: CanvasKit,
|
||||
blob: Uint8Array,
|
||||
windingRule: WindingRule
|
||||
): Path {
|
||||
const path = new ck.Path()
|
||||
const dv = new DataView(blob.buffer, blob.byteOffset, blob.byteLength)
|
||||
let o = 0
|
||||
|
||||
while (o < blob.length) {
|
||||
const cmd = blob[o++]
|
||||
switch (cmd) {
|
||||
case CMD_CLOSE:
|
||||
path.close()
|
||||
break
|
||||
case CMD_MOVE_TO: {
|
||||
const x = dv.getFloat32(o, true)
|
||||
const y = dv.getFloat32(o + 4, true)
|
||||
o += 8
|
||||
path.moveTo(x, y)
|
||||
break
|
||||
}
|
||||
case CMD_LINE_TO: {
|
||||
const x = dv.getFloat32(o, true)
|
||||
const y = dv.getFloat32(o + 4, true)
|
||||
o += 8
|
||||
path.lineTo(x, y)
|
||||
break
|
||||
}
|
||||
case CMD_CUBIC_TO: {
|
||||
const x1 = dv.getFloat32(o, true)
|
||||
const y1 = dv.getFloat32(o + 4, true)
|
||||
const x2 = dv.getFloat32(o + 8, true)
|
||||
const y2 = dv.getFloat32(o + 12, true)
|
||||
const x = dv.getFloat32(o + 16, true)
|
||||
const y = dv.getFloat32(o + 20, true)
|
||||
o += 24
|
||||
path.cubicTo(x1, y1, x2, y2, x, y)
|
||||
break
|
||||
}
|
||||
default:
|
||||
return path
|
||||
}
|
||||
}
|
||||
|
||||
path.setFillType(windingRule === 'EVENODD' ? ck.FillType.EvenOdd : ck.FillType.Winding)
|
||||
return path
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue