@penkit/core
v1.1.0
Published
Headless core of PenKit: stroke/session model, bounds, geometry, host adapter contract.
Readme
@penkit/core
PenKit의 headless domain model과 geometry utility 패키지다.
DOM input, rendering, host editor dependency 없이 안정적인 contract만 필요할 때 사용한다. 대부분의 브라우저 앱은 @penkit/sdk에서 시작하면 된다. adapter, custom engine pipeline, persistence layer, replay UI, geometry feature를 만들 때는 @penkit/core를 직접 사용한다.
npm install @penkit/coreLLM Agent Quick Start
새 host project에 이 패키지를 설치했다면 다음 순서를 지킨다.
@penkit/core를 pure하게 유지한다. DOM, React, canvas, SVG, host editor API를 core 성격의 코드에 넣지 않는다.- committed ink는
PenItemProps로 모델링한다. item은strokes,style, aggregatebounds, generictransform을 가진다. - host boundary는
PenHostAdapter로 둔다. adapter는 중립PenCommitPayload/PenItemProps를 받아 host renderer나 command system으로 변환한다. - browser input이 필요하면
@penkit/dom-input또는@penkit/sdk를 설치한다. - ready runtime이 필요하면
@penkit/engine또는@penkit/sdk를 설치한다.
주요 모듈
Domain Types
host data shape이나 adapter signature를 정의할 때 import한다.
import type {
Bounds,
InkStyle,
PenCommitPayload,
PenHostAdapter,
PenItemProps,
PenStroke,
RawPoint,
Transform,
} from "@penkit/core";중요 contract:
RawPoint: host canvas 좌표계로 정규화된 pointer sample.PenStroke: raw sample, centerline path, pressure outline path, closed flag, bounds를 가진 stroke.PenItemProps: 하나 이상의 stroke를 담는 persisted host item.PenCommitPayload: adapter가 host item id/transform을 부여하기 전의 중립 commit payload.PenHostAdapter: renderer/editor integration seam.
Pointer Buttons
stylus side button이나 eraser-end metadata를 tool override로 매핑할 때 사용한다.
import { PenButton, hasButton } from "@penkit/core";
if (hasButton(point, PenButton.BARREL)) {
// 이 stroke 동안 eraser/lasso/pointer 같은 도구로 전환한다.
}Geometry and Stroke Paths
renderer, hit test, precomputed path pipeline에서 사용한다.
import {
buildCenterlinePath,
buildPressureOutline,
computeBounds,
mergeBounds,
stabilizeStrokePoints,
} from "@penkit/core";
const pathPoints = stabilizeStrokePoints(rawPoints, {
smoothing: 0.25,
simplifyTolerance: 0.8,
});
const stroke = {
id: "stroke-1",
rawPoints,
centerPathD: buildCenterlinePath(pathPoints),
outlinePathD: buildPressureOutline(pathPoints, { baseWidth: 6 }),
closed: false,
bounds: computeBounds(pathPoints),
};Lasso Geometry
lassoSelect는 polygon으로 candidate item을 선택한다. candidate bounds는 빠른 필터로 유지하고, candidate가 stroke paths를 함께 넘기면 최종 hit-test는 넓은 빈 bounds가 아니라 실제 path와 선택 보정값 hitTolerance를 기준으로 판정한다. 일반 lasso는 mode: "intersect", item geometry가 polygon 안에 완전히 들어와야 할 때는 mode: "contain"을 사용한다.
import { lassoSelect } from "@penkit/core";
const selected = lassoSelect(lassoPolygon, items, {
mode: "intersect",
});StrokeSession
StrokeSession은 연속 stroke를 하나의 중립 commit payload로 묶는다. core는 시간을 읽지 않고 호출자가 시간을 넘긴다.
import { StrokeSession } from "@penkit/core";
const session = new StrokeSession({ commitDebounceMs: 600 }, style);
session.addStroke(strokeA, 1000);
session.addStroke(strokeB, 1400);
if (session.shouldCommit(2100)) {
const payload = session.commit();
adapter.createPenItem(payload);
}Document Snapshots
JSON save/load에는 snapshot을 사용한다. createPenDocumentSnapshot은 item을 deep clone하고, assertPenDocumentSnapshot은 import 전에 optional brush, outline, stabilization metadata까지 runtime shape을 검증한다.
Migration policy는 docs/snapshot-migration-policy.md에 둔다.
import {
assertPenDocumentSnapshot,
createPenDocumentSnapshot,
} from "@penkit/core";
const snapshot = createPenDocumentSnapshot(items, {
metadata: { documentId: "demo" },
});
const loaded: unknown = JSON.parse(serialized);
assertPenDocumentSnapshot(loaded);
for (const item of loaded.items) adapter.render(item);Ink Replay
InkReplayController는 headless다. render-ready frame을 반환하고, host가 committed items와 active preview를 그린다.
import { InkReplayController } from "@penkit/core";
const replay = new InkReplayController(items);
const frame = replay.seek(250);
renderCommittedItems(frame.items);
if (frame.preview) {
adapter.renderPreview?.([frame.preview.stroke], frame.preview.style);
}Adapter Contract
renderer나 host editor adapter는 PenHostAdapter를 구현한다.
import type { PenCommitPayload, PenHostAdapter, PenItemProps } from "@penkit/core";
type HostNode = unknown;
class HostAdapter implements PenHostAdapter<HostNode> {
readonly name = "host";
createPenItem(payload: PenCommitPayload): HostNode {
return host.commands.createPenNode(payload);
}
render(item: PenItemProps): void {
host.renderer.renderPenItem(item);
}
remove(itemId: string): void {
host.commands.removeNode(itemId);
}
}built-in 또는 custom BrushRenderHints를 실제로 그리는 adapter는
brushCapabilities를 선언한다. host는 preset을 선택하기 전에 renderer 지원 여부를 확인할 수 있다.
import { BUILTIN_BRUSH_PRESETS, supportsBrushRenderHints } from "@penkit/core";
supportsBrushRenderHints(adapter.brushCapabilities, BUILTIN_BRUSH_PRESETS.pencil.renderHints);다음 책임은 host에 남긴다.
- browser/client 좌표를 document 좌표로 변환.
- z-index, selection handle, editor command, undo policy, persistence storage.
- SVG path, canvas drawing, WebGL buffer, native editor node 같은 renderer-specific 결정.
하지 말 것
- DOM event handling을
@penkit/core에 넣지 않는다.@penkit/dom-input을 사용한다. - core가 adapter를 import하게 만들지 않는다. adapter가 core에 의존해야 한다.
- browser
PointerEvent객체를 저장하지 않는다.RawPoint[]를 저장한다. - engine이 받은 뒤의
RawPoint.x/y를 client coordinate라고 가정하지 않는다. host가 먼저 canvas/document 좌표로 변환해야 한다.
