@penkit/adapter-miricanvas
v1.1.0
Published
Miricanvas host adapter for PenKit (payload contract + stub). Proves portability without engine coupling.
Readme
@penkit/adapter-miricanvas
PenKit의 Miricanvas adapter contract 패키지다. 중립 PenKit commit payload를 Miricanvas 형태의 create payload로 변환하고, host가 넘긴 callback 또는 lifecycle bridge로 runtime rendering, removal, preview를 위임할 수 있다.
npm install @penkit/adapter-miricanvas @penkit/core이 패키지 하나만으로 live Miricanvas integration이 완성되는 것은 아니다. adapter boundary, 즉 payload conversion과 optional lifecycle bridge를 제공한다.
LLM Agent Quick Start
@penkit/core는 host-neutral로 유지한다. Miricanvas command shape는 이 adapter 또는 host integration code에 둔다.- 중립
PenCommitPayload를 Miricanvas create command로 바꿀 때toPenItemCreatePayload(payload)를 사용한다. PenEngine또는@penkit/sdk가 실제 host renderer/command path를 호출해야 하면createMiricanvasPenAdapter(...)로 command sync, renderer binding, brush pass dispatch를 하나의 adapter로 묶는다.- 낮은 수준 callback만 필요하면
MiricanvasPenAdapterOptions를 직접 넘긴다. - callback, lifecycle bridge,
brushRenderer가 없으면 기본render()와remove()가 throw한다는 점을 기대한다. - native renderer가
BrushRenderHints를 지원한다면 payload의brushRenderPlan을 실행하거나brushCapabilities로 지원 범위를 override한다. - coordinate mapping은
@penkit/sdk/engine layer의transformPoint에 둔다.
주요 모듈
toPenItemCreatePayload
중립 commit payload를 Miricanvas create-command shape로 변환한다.
import { toPenItemCreatePayload } from "@penkit/adapter-miricanvas";
const payload = toPenItemCreatePayload(commitPayload);
editor.commands.create(payload);출력 shape:
type MiricanvasPenItemCreatePayload = {
type: "PEN_ITEM";
props: {
strokes: PenStroke[];
style: InkStyle;
bounds: Bounds;
brushRenderPlan: MiricanvasBrushRenderPlan;
};
transform: Transform;
};brushRenderPlan은 built-in brush preset을 native renderer pass로 풀어낸다. 예를 들어 fountain은 base outline fill + wet-ink center shade, marker는 segment fill accumulation, pencil은 graphite hatch/dot pass로 내려간다.
toBrushRenderPlan
runtime item 또는 commit payload의 strokes와 style을 native brush pass 목록으로 변환한다.
import { toBrushRenderPlan } from "@penkit/adapter-miricanvas";
const plan = toBrushRenderPlan(item);
nativePenRenderer.render(plan.passes);executeBrushRenderPlan
brushRenderPlan을 host native renderer callback으로 실행한다. host가 pass 전체를 직접 처리하려면 renderPass만 넘기고, PenKit 기본 dispatch를 쓰려면 fillPath, strokePath, texturePath를 구현한다.
import { executeBrushRenderPlan } from "@penkit/adapter-miricanvas";
executeBrushRenderPlan(nativePenRenderer, payload.props.brushRenderPlan, {
payload,
transform: payload.transform,
});toPenItemCreatePayloadFromItem
runtime이 이미 transform이 있는 PenItemProps를 갖고 있을 때 사용한다.
import { toPenItemCreatePayloadFromItem } from "@penkit/adapter-miricanvas";
const payload = toPenItemCreatePayloadFromItem(item);createMiricanvasPenAdapter
제품 연동에서는 command/state sync와 native renderer lifecycle이 같은 순서로 실행되어야 한다. createMiricanvasPenAdapter는 host bridge를 받아 syncPenItem -> renderPenItem -> renderBrushPlan/brushRenderer 순서로 묶은 MiricanvasPenAdapter를 만든다.
import { createMiricanvasPenAdapter } from "@penkit/adapter-miricanvas";
const adapter = createMiricanvasPenAdapter(
{
syncPenItem(payload, item) {
editor.commands.upsertPenItem(item.id, payload);
},
renderPenItem(item, payload) {
editor.renderer.bindPenItem(item.id, payload.transform);
},
removePenItem(itemId) {
editor.commands.removeItem(itemId);
editor.renderer.releasePenItem(itemId);
},
renderPreview(strokes, style, options) {
editor.overlay.renderPenPreview(strokes, style, options);
},
clearPreview() {
editor.overlay.clearPenPreview();
},
},
{
brushRenderer: {
renderPass(pass, context) {
editor.renderer.renderPenBrushPass(context.item?.id, pass);
},
},
},
);host가 brush plan dispatch를 직접 소유한다면 brushRenderer 대신 bridge의 renderBrushPlan(plan, context)를 구현한다. syncPenItem은 이름 그대로 create/update를 host command layer가 판정하는 idempotent sync hook이다.
MiricanvasPenAdapter
MiricanvasPenAdapter는 PenHostAdapter<MiricanvasPenItemCreatePayload>를 구현한다. 기본 createPenItem은 payload를 반환한다. 기본 render와 remove는 명확한 stub error를 throw한다. local overlay demo나 native editor command path에 연결할 때 callback 또는 brushRenderer를 넘긴다.
import { MiricanvasPenAdapter } from "@penkit/adapter-miricanvas";
const adapter = new MiricanvasPenAdapter({
render(item, payload) {
editor.commands.addPenItem(payload);
editor.renderer.renderPenItem(item, payload.props.brushRenderPlan);
},
remove(itemId) {
editor.commands.removeItem(itemId);
},
renderPreview(strokes, style, options) {
overlayRenderer.renderPreview(strokes, style, options);
},
clearPreview() {
overlayRenderer.clearPreview();
},
});pass 단위 renderer만 있는 host라면 full render callback 대신 brushRenderer를 넘길 수 있다.
const adapter = new MiricanvasPenAdapter({
brushRenderer: {
beginPlan(plan, context) {
editor.renderer.beginPenItem(context.payload, plan.capabilities);
},
fillPath(pass) {
editor.renderer.fillPath(pass.pathD, pass);
},
strokePath(pass) {
editor.renderer.strokePath(pass.pathD, pass);
},
texturePath(pass) {
editor.renderer.texturePath(pass.pathD, pass);
},
endPlan() {
editor.renderer.endPenItem();
},
},
});Full SDK Overlay Example
import { createMiricanvasPenAdapter } from "@penkit/adapter-miricanvas";
import { createPenLayer } from "@penkit/sdk";
const adapter = createMiricanvasPenAdapter({
syncPenItem(payload, item) {
editor.commands.upsertPenItem(item.id, payload);
},
renderPenItem(item, payload) {
editor.renderer.bindPenItem(item.id, payload.transform);
},
removePenItem(itemId) {
editor.commands.removeItem(itemId);
editor.renderer.releasePenItem(itemId);
},
renderPreview(strokes, style, options) {
editor.overlay.renderPenPreview(strokes, style, options);
},
clearPreview() {
editor.overlay.clearPenPreview();
},
});
const pen = createPenLayer(editor.viewportElement, {
adapter,
penPriority: true,
transformPoint(point) {
const rect = editor.viewportElement.getBoundingClientRect();
const localX = point.x - rect.left + editor.viewport.scrollLeft;
const localY = point.y - rect.top + editor.viewport.scrollTop;
return {
...point,
x: localX / editor.viewport.zoom + editor.viewport.pan.x,
y: localY / editor.viewport.zoom + editor.viewport.pan.y,
};
},
});
pen.engine.setTool("pen");Host Responsibilities
host editor가 계속 소유하는 책임:
- 실제 command bus 호출과 undo integration.
- committed pen item renderer lifecycle.
brushRenderPlanpass를 native renderer의 fill/stroke/texture primitive로 매핑.- selection overlay, handle, native hit-test policy.
- zoom, pan, scroll, page offset, CSS transform 같은 viewport transform.
- Miricanvas 저장 schema가
PenDocumentSnapshot과 다를 때 persistence schema.
하지 말 것
- lifecycle bridge가 실제 editor command/renderer API에 연결되기 전에는 complete native Miricanvas integration이라고 말하지 않는다.
@penkit/core가 Miricanvas command type을 import하게 만들지 않는다.transformPoint를 무시하지 않는다. raw browser coordinate는 editor document coordinate로 변환되어야 한다.
