@penkit/engine
v1.1.0
Published
PenKit runtime: the headless editor that owns active tool, input routing, item store, selection, and live preview. The thing a host mounts.
Readme
@penkit/engine
Headless PenKit runtime이다. active tool, pointer routing, item state, selection, history, document import/export, stroke grouping, live preview adapter call을 관리한다.
npm install @penkit/engine @penkit/core대부분의 브라우저 앱은 @penkit/sdk로 시작하면 된다. 이미 input pipeline이 있거나, non-DOM runtime/test harness가 있거나, host app이 정규화된 RawPoint sample을 직접 feed하려면 @penkit/engine을 직접 사용한다.
LLM Agent Quick Start
@penkit/core의PenHostAdapter를 구현하거나 import한다.new PenEngine({ adapter })를 만든다.pointerDown,pointerMove,pointerUp으로RawPointsample을 넣거나engine.pointerHandlers를@penkit/dom-input에 넘긴다.- host UI update가 필요하면 engine event를 subscribe한다.
- DOM mounting을 한 번에 끝내고 싶다면
@penkit/sdk를 사용한다.
import { PenEngine } from "@penkit/engine";
import type { PenHostAdapter, PenItemProps, RawPoint } from "@penkit/core";
const adapter: PenHostAdapter = {
name: "host",
createPenItem(payload) {
return payload;
},
render(item: PenItemProps) {
hostRenderer.render(item);
},
remove(itemId: string) {
hostRenderer.remove(itemId);
},
};
const engine = new PenEngine({ adapter });
engine.setTool("pen");주요 모듈
PenEngine
PenEngine은 host가 mount하는 runtime이다. 여전히 headless이며 DOM, SVG, React, host editor import가 없다.
주요 설정:
const engine = new PenEngine({
adapter,
tool: "pen",
style: {
color: "#2563eb",
baseWidth: 5,
opacity: 1,
cap: "round",
join: "round",
},
strokeStabilization: { smoothing: 0.25, simplifyTolerance: 0.8 },
barrelButtonTool: "eraser",
eraserButtonTool: "eraser",
transformPoint(point) {
return {
...point,
x: (point.x - viewportLeft) / zoom + pan.x,
y: (point.y - viewportTop) / zoom + pan.y,
};
},
});주요 method/property:
setTool(kind),tool,availableToolssetStyle(patch),style,setStrokeStabilization(options)pointerDown(point),pointerMove(points),pointerUp(point)pointerHandlersitems,selection,selectionBounds()selectAll(),clearSelection(),deleteSelection(),duplicateSelection(),transformSelection(),updateSelectionStyle(),updateSelectionBrush()undo(),redo(),canUndo,canRedoexportDocument(),importDocument(snapshot)strokeGrouping,hasPendingStrokeGroup,flushStrokeGroup()
Built-In Tools
createDefaultTools()는 기본 runtime tool을 등록한다.
penhighlightereraserlassopointerselect
createNoteToolset()은 note-app 스타일 tool을 추가한다.
fountain,ballpoint,marker,pencilprecision-eraserline,rectangle,ellipse,triangle,arrowmove- recognizer가 주입되면 smart shape tools
engine은 @penkit/ai를 import하지 않는다. host가 recognizer를 주입한다.
Custom Tools
새 기능은 ToolStrategy를 구현하고 ToolContext를 통해서만 상태를 바꾼다.
import type { ToolContext, ToolStrategy } from "@penkit/engine";
import type { RawPoint } from "@penkit/core";
class EyedropperTool implements ToolStrategy {
readonly kind = "eyedropper";
onStart(point: RawPoint, ctx: ToolContext): void {
const hit = ctx.hitTest(point);
if (hit) {
ctx.setStyle({
color: hit.style.color,
baseWidth: hit.style.baseWidth,
opacity: hit.style.opacity,
});
}
}
onMove(): void {}
onEnd(): void {}
}
engine.registerTool(new EyedropperTool());
engine.setTool("eyedropper");tool은 commitStroke, removeItem, setSelection, moveSelection, haptic, setPreview, beginHistory 같은 ToolContext method만 사용한다.
class SnapTool implements ToolStrategy {
readonly kind = "snap";
onStart(_point: RawPoint, ctx: ToolContext): void {
ctx.haptic({ kind: "impact", style: "medium", source: "snap" });
}
onMove(): void {}
onEnd(): void {}
}Shape Assist and Scribble Erase
smart shape snapping은 opt-in이다. @penkit/ai의 recognizeShape 같은 ShapeRecognizer를 넘긴다.
import { recognizeShape } from "@penkit/ai";
import { SmartShapeTool } from "@penkit/engine";
engine.registerTool(new SmartShapeTool(recognizeShape));
engine.setTool("smart-shape");ScribbleEraseInkTool은 scribble-to-erase의 guarded PoC다. false positive를 처리할 host UX 없이 일반 ink를 대체하지 않는다.
SmartShapeTool과 hold-to-shape가 켜진 HoldShapeInkTool은 인식된 freehand stroke를 clean shape로 snap할 때 ctx.haptic({ kind: "impact", style: "medium", source: "smart-shape" })를 호출한다. haptic bridge가 없는 host에서는 안전하게 no-op이다.
Stroke Grouping
기본은 pen-up 하나가 item 하나를 만든다. note app에서 빠른 여러 stroke를 하나의 PenItemProps로 묶고 싶다면 grouping을 켠다.
const engine = new PenEngine({
adapter,
strokeGrouping: { commitDebounceMs: 600 },
});
engine.flushStrokeGroup();commitStroke(..., { group: true })를 호출하는 tool만 grouping session에 들어간다.
Selection, Editing, History
engine은 selected item id를 저장하고 selectionchange를 emit한다. handle과 overlay는 host UI 책임이다.
engine.on("selectionchange", (ids) => drawSelectionOverlay(ids));
engine.selectAll();
engine.updateSelectionStyle({ color: "#ef4444", baseWidth: 8 });
engine.updateSelectionBrush(calibratedBrushPreset);
engine.transformSelection({ scale: 1.1, rotateDeg: 15 });
engine.undo();
engine.redo();updateSelectionStyle({ baseWidth })는 저장된 raw point로 pressure outline을 다시 만든다. 선택된 item에 built-in 또는 보정된 BrushPreset의 pressure curve까지 적용해야 할 때는 updateSelectionBrush(brush)를 사용한다.
새 stroke에는 engine.setBrush(calibratedBrushPreset)로 넘긴 active BrushPreset outline이 적용된다. built-in ink tool이 같은 brush kind를 commit하더라도 active preset의 pressureCurve 같은 보정값을 우선한다.
Document Save and Load
exportDocument()는 @penkit/core의 versioned snapshot을 반환한다. importDocument(snapshot)는 검증 후 engine document를 교체한다.
const snapshot = engine.exportDocument({
metadata: { source: "host-doc-123" },
});
localStorage.setItem("ink", JSON.stringify(snapshot));
engine.importDocument(JSON.parse(localStorage.getItem("ink") ?? "{}"));Events
engine.on(event, listener)로 subscribe한다. 반환 함수는 unsubscribe다.
const offCommit = engine.on("commit", (item) => {
analytics.track("pen_commit", { id: item.id, strokes: item.strokes.length });
});
offCommit();하지 말 것
- browser
PointerEvent객체를 직접 넘기지 않는다.RawPoint로 정규화한다. - custom
ToolStrategy안에 DOM/renderer 코드를 넣지 않는다. - built-in hit test를 full vector/rotated-shape hit test라고 가정하지 않는다. 기본은 item bounds 기반이다.
- engine이 OCR, handwriting recognition, production LLM을 수행한다고 말하지 않는다.
