Extend lint coverage from src/ to all packages (178 files, 140 rules). Core (37 files): - Remove 98 unnecessary conditions (dead ?./?? on non-nullish values) - Remove 38 unnecessary type assertions (! and as casts) - Convert 12 patterns to optional chain (a && a.b → a?.b) - Fix 10 consistent-type-imports (merge duplicates, inline type keyword) - Add parentheses to 16 nested ternaries - Convert 4 indexed for-loops to for-of - Merge 4 duplicate import statements MCP (3 files): - Replace typeof undefined check with 'in' operator - Capture graph in local const to avoid ! assertions ACP (1 file): - Fix 5 TypeScript errors (type predicates, object shapes, dead code) - Remove unnecessary template expression Vendored kiwi-schema gets broader rule overrides to avoid touching it. Core complexity stays as warning (24 functions over limit).
140 lines
3.7 KiB
TypeScript
140 lines
3.7 KiB
TypeScript
import type { SkiaRenderer } from './renderer'
|
|
import type { SceneGraph } from './scene-graph'
|
|
import type { CanvasKit, Canvas } from 'canvaskit-wasm'
|
|
|
|
export type ExportFormat = 'PNG' | 'JPG' | 'WEBP' | 'SVG'
|
|
|
|
interface RenderOptions {
|
|
scale: number
|
|
format: ExportFormat
|
|
quality?: number
|
|
}
|
|
|
|
export function computeContentBounds(
|
|
graph: SceneGraph,
|
|
nodeIds: string[]
|
|
): { minX: number; minY: number; maxX: number; maxY: number } | null {
|
|
let minX = Infinity,
|
|
minY = Infinity,
|
|
maxX = -Infinity,
|
|
maxY = -Infinity
|
|
|
|
for (const id of nodeIds) {
|
|
const node = graph.getNode(id)
|
|
if (!node || !node.visible) continue
|
|
const abs = graph.getAbsolutePosition(id)
|
|
minX = Math.min(minX, abs.x)
|
|
minY = Math.min(minY, abs.y)
|
|
maxX = Math.max(maxX, abs.x + node.width)
|
|
maxY = Math.max(maxY, abs.y + node.height)
|
|
}
|
|
|
|
if (!isFinite(minX)) return null
|
|
return { minX, minY, maxX, maxY }
|
|
}
|
|
|
|
function ckImageFormat(ck: CanvasKit, format: ExportFormat) {
|
|
switch (format) {
|
|
case 'JPG':
|
|
return ck.ImageFormat.JPEG
|
|
case 'WEBP':
|
|
return ck.ImageFormat.WEBP
|
|
default:
|
|
return ck.ImageFormat.PNG
|
|
}
|
|
}
|
|
|
|
function renderToSurface(
|
|
ck: CanvasKit,
|
|
renderer: SkiaRenderer,
|
|
graph: SceneGraph,
|
|
pageId: string,
|
|
width: number,
|
|
height: number,
|
|
format: ExportFormat,
|
|
quality: number,
|
|
setup: (canvas: Canvas) => void
|
|
): Uint8Array | null {
|
|
const surface = ck.MakeSurface(width, height)
|
|
if (!surface) return null
|
|
|
|
try {
|
|
const canvas = surface.getCanvas()
|
|
setup(canvas)
|
|
renderer.renderSceneToCanvas(canvas, graph, pageId)
|
|
surface.flush()
|
|
const image = surface.makeImageSnapshot()
|
|
const encoded = image.encodeToBytes(ckImageFormat(ck, format), quality)
|
|
image.delete()
|
|
return encoded ? new Uint8Array(encoded) : null
|
|
} finally {
|
|
surface.delete()
|
|
}
|
|
}
|
|
|
|
export function renderNodesToImage(
|
|
ck: CanvasKit,
|
|
renderer: SkiaRenderer,
|
|
graph: SceneGraph,
|
|
pageId: string,
|
|
nodeIds: string[],
|
|
options: RenderOptions
|
|
): Uint8Array | null {
|
|
const bounds = computeContentBounds(graph, nodeIds)
|
|
if (!bounds) return null
|
|
|
|
const contentW = bounds.maxX - bounds.minX
|
|
const contentH = bounds.maxY - bounds.minY
|
|
if (contentW <= 0 || contentH <= 0) return null
|
|
|
|
const pixelW = Math.ceil(contentW * options.scale)
|
|
const pixelH = Math.ceil(contentH * options.scale)
|
|
if (pixelW <= 0 || pixelH <= 0) return null
|
|
|
|
const quality = options.quality ?? (options.format === 'PNG' ? 100 : 90)
|
|
return renderToSurface(
|
|
ck,
|
|
renderer,
|
|
graph,
|
|
pageId,
|
|
pixelW,
|
|
pixelH,
|
|
options.format,
|
|
quality,
|
|
(canvas) => {
|
|
canvas.clear(ck.TRANSPARENT)
|
|
canvas.scale(options.scale, options.scale)
|
|
canvas.translate(-bounds.minX, -bounds.minY)
|
|
}
|
|
)
|
|
}
|
|
|
|
export function renderThumbnail(
|
|
ck: CanvasKit,
|
|
renderer: SkiaRenderer,
|
|
graph: SceneGraph,
|
|
pageId: string,
|
|
width: number,
|
|
height: number
|
|
): Uint8Array | null {
|
|
const page = graph.getNode(pageId)
|
|
if (!page || page.childIds.length === 0) return null
|
|
|
|
const bounds = computeContentBounds(graph, page.childIds)
|
|
if (!bounds) return null
|
|
|
|
const contentW = bounds.maxX - bounds.minX
|
|
const contentH = bounds.maxY - bounds.minY
|
|
if (contentW <= 0 || contentH <= 0) return null
|
|
|
|
const scale = Math.min(width / contentW, height / contentH, 2)
|
|
|
|
return renderToSurface(ck, renderer, graph, pageId, width, height, 'PNG', 100, (canvas) => {
|
|
canvas.clear(ck.Color4f(renderer.pageColor.r, renderer.pageColor.g, renderer.pageColor.b, 1))
|
|
const offsetX = (width - contentW * scale) / 2 - bounds.minX * scale
|
|
const offsetY = (height - contentH * scale) / 2 - bounds.minY * scale
|
|
canvas.translate(offsetX, offsetY)
|
|
canvas.scale(scale, scale)
|
|
})
|
|
}
|