Skip to Content
API 레퍼런스에디터 (editor)

에디터 API

OBS는 읽기 전용입니다. 네이티브 Tauri 메인·오버레이 창은 revision coordinator를 거쳐 쓸 수 있고, OBS는 canonical 문서를 조회·구독만 할 수 있습니다.

dmn.editor는 에디터 레이아웃을 이루는 일곱 컬렉션을 revision이 있는 문서 하나로 조회하고 갱신합니다. 사용자 동작 하나가 여러 컬렉션을 함께 바꾸거나, 소비자가 순서가 보장된 변경 흐름 하나를 받아야 할 때 사용하세요.

문서 타입

type EditorField = | 'keys' | 'keyPositions' | 'statPositions' | 'graphPositions' | 'knobPositions' | 'spritePositions' | 'layerGroups'; interface EditorDocumentV1 { schemaVersion: 1; keys: KeyMappings; keyPositions: KeyPositions; statPositions: StatItemPositions; graphPositions: GraphItemPositions; knobPositions: KnobItemPositions; spritePositions: SpritePositions; layerGroups: LayerGroups; } type EditorPatchV1 = { schemaVersion: 1; } & Partial<Pick<EditorDocumentV1, EditorField>>;

EditorPatchV1에 포함한 각 필드는 해당 최상위 컬렉션의 canonical 전체 값입니다. 항목 하나만 표현하는 diff가 아닙니다.

멀티 키 업데이트부터 get()이 반환하는 EditorDocumentV1의 keys와 onCommitted()가 전달하는 EditorPatchV1의 keys는 순수 문자열이 아니라 KeySlot union(string | MultiKeySlot)을 사용합니다. 슬롯 형태와 canonical 식별자 규칙은 Keys API를 참고하세요.

위치 컬렉션(keyPositions, statPositions, graphPositions, knobPositions, spritePositions)의 모든 요소는 앱이 발급·소유하는 안정 id(UUID)를 가집니다. 불투명 값으로 다루세요. 읽은 값을 그대로 되돌려 보내고, 직접 만들지 마세요. id가 없거나 미확인인 요소를 쓰면 나머지 필드가 같은 기존 요소의 신원을 승계합니다. 맞는 항목이 없으면 백엔드가 새 값을 발급합니다. id를 빼는 것이 새 신원을 보장하지는 않고, 값을 바꾸면서 id까지 빼면 새 신원이 될 수 있습니다. 읽은 요소를 복사해 배열에 덧붙이는 경우처럼 같은 id가 여러 번 나오면, 원래 자리의 요소가 신원을 지키고 나머지 사본이 새 값을 받습니다. 스프라이트 요소의 각 자세가 가진 poseId도 같은 규칙을 따릅니다. 신원 규칙은 Keys API를 참고하세요.

스프라이트 위치 타입

반응형 스프라이트는 담당 키를 누르고 있는 동안 자세가 바뀌는 이미지 레이어입니다. 키 요소와 달리 키 매핑에 인덱스로 결합하지 않고, 자세마다 담당 키 요소 id를 직접 참조합니다. 읽은 문서에는 id와 poseId가 항상 실려 있습니다. 쓸 때는 둘 다 그대로 돌려보내세요. 아래 표기된 개수·값 상한은 커밋 시점에 검증하고, 읽은 문서는 저장된 데이터를 그대로 되돌려줍니다.

// 탭/모드 id(예: "4key")로 키잉 type SpritePositions = Record<string, ReactiveSpritePosition[]>; interface ReactiveSpritePosition { id?: string; // 안정 요소 신원 (UUID) dx: number; // 요소 상자 원점과 크기. 상자가 곧 기본 이미지 상자이고 오버레이 창 크기 계산에 들어갑니다 dy: number; width: number; height: number; rotation?: number; // 요소 상자 중심을 축으로 하는 배치 회전, -180–180도, 기본 0 hidden: boolean; zIndex: number | null; layerName?: string | null; // 미설정이면 백엔드가 키를 생략합니다 groupId?: string | null; // 미설정이면 백엔드가 키를 생략합니다 className: string | null; useInlineStyles: boolean | null; baseImage: string | null; // 일치하는 자세가 없을 때 보여줄 이미지. 항상 요소 상자를 채웁니다 referenceNaturalSize: { source: string | null; width: number; height: number; } | null; // 픽셀 배율의 기준이 되는 원본 크기(정수, 1–32768). baseImage가 있으면 source가 그 경로와 같아야 하고 없으면 null이어야 합니다. null이면 모든 이미지를 요소 상자에 그대로 그립니다 pivot: { x: number; y: number }; // 정규화 공통 축, 각각 0–1, 기본 {0.5, 0.5}. 기본 이미지의 기준점이자 모든 자세 변환의 축 idleTransform: SpriteTransform; // 일치하는 자세가 없을 때의 기본 변환 poses: SpritePose[]; // 커밋은 최대 64개까지 허용 activation: 'whileHeld' | 'onPress'; // 기본 whileHeld. onPress는 누른 순간 자세를 한 번 재생하고 idle로 복귀 pressDurationMs: number; // onPress 재생 시간, 1–5000, 기본 300 transitionMs: number; // 자세 전환 시간, 0–1000, 기본 0. onPress는 무시하고 pressDurationMs 동안 재생 transitionEasing: string; // CSS easing, 기본 'cubic-bezier(0.4, 0, 0.2, 1)' } interface SpritePose { poseId?: string; // 안정 자세 신원 (UUID) name?: string | null; // 미설정이면 백엔드가 키를 생략합니다 triggers: string[]; // 키 요소 id 목록, 커밋은 최대 512개까지 허용하며 정렬·중복 제거 transform: SpriteTransform; pivot?: { x: number; y: number } | null; // 상태 이미지의 독립 기준점. null 또는 생략이면 스프라이트의 공통 기준점을 따라갑니다 imageOverride: string | null; // 이 자세가 활성인 동안 baseImage를 대체 imageOverrideMetrics: { source: string; width: number; height: number; } | null; // imageOverride의 원본 픽셀 크기(정수, 1–32768). 선택 사항이고, 있으면 source가 경로와 정확히 같아야 합니다. imageOverride가 없으면 null로 정규화됩니다 } interface SpriteTransform { x: number; // -2000–2000 px y: number; // -2000–2000 px rotation: number; // -180–180 도 scale: number; // 0.1–10 }

요소의 rotation은 논리 상자의 중심을 축으로 모든 자세를 포함한 스프라이트 전체를 돌립니다. 공통 기준점을 축으로 도는 로컬 회전인 idleTransform.rotation, 각 자세의 transform.rotation과는 독립적입니다. 선택 전체를 회전하면 스프라이트의 dx, dy, 요소 rotation을 함께 바꾸며 자세 변환, 트리거, 이미지 참조와 자세 전환 방향은 유지됩니다. 새 스프라이트에서 rotation을 생략하면 0입니다. 이미 있는 스프라이트의 schemaVersion 1·2 커밋에서는 생략 시 저장된 회전값을 유지합니다. null, 비유한값, -180~180 밖의 값은 거절됩니다.

자세는 스프라이트가 참조하는 키 중 현재 눌린 키 집합이 트리거 집합과 정확히 일치할 때 활성화되고, 스프라이트가 참조하지 않는 키는 무시합니다. 정확히 일치하는 자세가 없으면 눌린 키 각각의 단일 트리거 자세를 같은 비중으로 섞습니다. x·y·scale은 산술 평균, rotation은 원형 평균이고 이미지는 poseId 사전순 첫 override를 씁니다. 트리거가 둘 이상인 자세는 섞기에 참여하지 않으므로 눌린 키가 그런 자세에만 있으면 idle로 돌아갑니다. 한 스프라이트의 두 자세가 같은 트리거 집합을 가질 수 없고, 빈 트리거 목록은 커밋에서 거부됩니다.

요소 상자가 곧 기본 이미지 상자입니다. 기본 이미지는 항상 상자를 채우고, 공통 축 P는 스프라이트의 pivot × (width, height)입니다. 상태의 pivot이 null이거나 생략되면 상태 이미지도 같은 정규화 기준점을 사용합니다. 상태에 pivot 값이 있으면 그 상태 이미지의 독립 기준점을 P에 맞춥니다. 원본 크기가 다른 상태 이미지는 원본 픽셀 × 기준 배율로 그립니다. 기준 배율은 (width, height)를 referenceNaturalSize로 축별로 나눈 값입니다. 기준 크기나 상태 이미지의 크기가 없으면 그 이미지를 요소 상자 크기로 그립니다. 에디터에서 공통 기준점을 옮기면 기본 이미지는 제자리에 남고, 공통 기준점에 연결된 상태는 새 축을 따라갑니다. 독립 기준점을 가진 상태는 x/y를 같은 커밋에서 보정해 화면 위치를 유지합니다. 상태 기준점을 옮기거나 연결을 전환할 때도 상태 이미지가 갑자기 움직이지 않도록 x/y를 보정합니다. 범위를 벗어난 기준점은 커밋이 VALIDATION_FAILED로 거절되며 details.validationCode는 INVALID_SPRITE_PIVOT 또는 INVALID_SPRITE_POSE_PIVOT입니다. 원본 크기가 경로와 어긋나거나 범위를 벗어나면 STALE_SPRITE_IMAGE_METRICS 또는 INVALID_SPRITE_IMAGE_METRICS입니다. schemaVersion 1 패치에서 이미 있는 요소·자세의 pivot, referenceNaturalSize, imageOverrideMetrics, activation, pressDurationMs를 생략하면 저장된 값이 유지됩니다. 상태의 pivot을 null로 보내면 공통 기준점 연결로 바뀝니다. 다만 baseImage나 imageOverride를 바꾸면서 크기를 생략하면 크기는 비워집니다.

현재 문서 조회

dmn.editor.get(): Promise<EditorGetResult>

현재 revision과 그 revision에 정확히 대응하는 전체 문서를 한 스냅샷으로 반환합니다.

interface EditorGetResult { revision: number; document: EditorDocumentV1; } const { revision, document } = await dmn.editor.get(); console.log(revision, document.keyPositions);

반환된 revision을 보관하세요. 이후 커밋의 baseRevision으로 사용하여 오래된 스냅샷이 최신 편집을 조용히 덮어쓰지 못하게 합니다.

변경 사항 원자적 커밋

dmn.editor.commit(request): Promise<EditorPluginCommitResult>

baseRevision의 문서에 changes를 합치고, 완성된 문서를 검증한 다음, 포함된 모든 컬렉션을 한 번의 원자적 교체로 함께 저장합니다. 커밋이 거절되면 일부만 적용되지 않습니다. DM Note는 지원 플랫폼마다 가능한 가장 강한 파일·디렉터리 내구성 barrier를 요청합니다. 다만 하드웨어나 펌웨어가 이 barrier를 지키지 않는 고장까지 애플리케이션이 절대 보장할 수는 없습니다.

앱의 Undo/Redo도 편집 컬렉션 7개, 커스텀 탭 정보, 선택 모드, 카운터, 프리셋 설정, 탭별 노트 설정을 백엔드 store 트랜잭션 한 번으로 함께 복원합니다. 이 내부 커맨드는 공개 플러그인 API로 노출하지 않습니다.

interface EditorCommitRequest { baseRevision: number; mutationId: string; gestureId?: string; gestureIds?: string[]; // keys를 포함한 커밋의 멀티 키 슬롯 지원 선언 multiKey?: boolean; changes: EditorPatchV1; } interface EditorPluginCommitResult { revision: number; changedFields: EditorField[]; } const snapshot = await dmn.editor.get(); const result = await dmn.editor.commit({ baseRevision: snapshot.revision, mutationId: crypto.randomUUID(), changes: { schemaVersion: 1, statPositions: nextStatPositions, layerGroups: nextLayerGroups, }, }); console.log('커밋된 revision:', result.revision);

요청에는 위에 표시된 여섯 개의 키 baseRevision, mutationId, changes, gestureId, gestureIds, multiKey만 사용할 수 있습니다. 그 외의 키가 있으면 백엔드에 도달하기 전에 TypeError로 거절되며, JSON 직렬화할 수 없는 객체도 같습니다. 이 거절에는 errorCode가 없으므로 errorCode만으로 분기하는 처리기는 이 오류를 구분하지 못합니다.

mutationId는 64바이트 이하의 UUID 문자열이어야 합니다. 현재 앱 프로세스에서 최근 요청을 같은 ID로 재시도하면 중복 적용되지 않습니다. 이 메모리 내 중복 방지는 즉시 IPC 재시도용이며 앱을 재시작하면 유지되지 않습니다. 아직 보관 중인 ID를 다른 요청에 재사용하면 거절됩니다.

gestureId는 히스토리 병합의 대표 게스처입니다. gestureIds는 요청 하나로 합쳐진 모든 프리뷰 세션을 전달하여 committed 이벤트가 전체 집합을 echo하게 합니다. 두 필드는 모두 선택 사항이며 UUID만 허용합니다. gestureIds 배열은 항목이 최대 32개이고, 두 필드를 합친 고유 ID 집합도 최대 32개로 제한됩니다.

제출한 값이 이미 현재 값과 같으면 현재 revision과 빈 changedFields를 반환하고 canonical committed 이벤트는 발행하지 않습니다. 호환 wrapper는 예전 필드별 refresh 이벤트를 한 번 투영할 수 있지만, 같은 mutationId 재시도에서는 다시 투영하지 않습니다.

키 구조는 반드시 함께 변경

keys[mode][i]와 keyPositions[mode][i]는 같은 인덱스로 하나의 항목을 나타냅니다. 모드를 추가·삭제하거나 배열 길이를 바꿀 때는 두 컬렉션을 같은 커밋에 함께 넣으세요. 두 컬렉션의 모드 집합과 모드별 길이는 같아야 합니다.

await dmn.editor.commit({ baseRevision, mutationId: crypto.randomUUID(), multiKey: true, changes: { schemaVersion: 1, keys: nextKeys, keyPositions: nextKeyPositions, }, });

키 이름이나 위치 속성처럼 구조와 길이가 그대로인 변경은 해당 컬렉션 하나만 갱신해도 됩니다. 구조를 바꾸면서 keys 또는 keyPositions 하나만 제출하면 PAIRED_UPDATE_REQUIRED로 거절됩니다.

현재 매핑에 멀티 키 슬롯이 하나라도 있으면 keys를 포함한 커밋은 요청에 multiKey: true를 함께 선언해야 합니다. 선언 없는 쓰기는 MULTI_KEY_UNSUPPORTED(재시도 불가)로 거절됩니다. 멀티 키 슬롯을 모르는 플러그인이 매핑을 왕복시키며 파괴하는 것을 막는 fail-closed 게이트입니다.

호환용 dmn.keys API를 사용할 때 이런 구조 변경은 dmn.keys.updateWithPositions(keys, keyPositions)로 제출하세요. update()와 updatePositions() 두 호출로 나누면 안 됩니다.

커밋 오류

commit()은 아래 구조의 오류로 reject됩니다. 사람이 읽는 message가 아니라 errorCode로 분기하세요.

type EditorCommitErrorCode = | 'REVISION_CONFLICT' | 'PLUGIN_REVISION_CONFLICT' | 'VALIDATION_FAILED' | 'TOO_MANY_GESTURE_IDS' | 'INVALID_GESTURE_ID' | 'PAIRED_UPDATE_REQUIRED' | 'MULTI_KEY_UNSUPPORTED' | 'MUTATION_ID_REUSED' | 'HISTORY_IN_PROGRESS' | 'HISTORY_EPOCH_CONFLICT' | 'IO_ERROR'; interface EditorCommitError { errorCode: EditorCommitErrorCode; message: string; details?: { currentRevision?: number; validationCode?: string; field?: string; currentHistoryEpoch?: number; }; retryable: boolean; }
코드의미retryable
REVISION_CONFLICTbaseRevision이 오래됨. 다시 조회·조정한 뒤 커밋true
PLUGIN_REVISION_CONFLICT플러그인 범위 revision이 오래됨. 다시 조회한 뒤 커밋true
VALIDATION_FAILED완성된 문서가 에디터 검증 규칙을 위반함false
TOO_MANY_GESTURE_IDSgestureIds가 32개를 넘거나 합친 고유 ID가 32개를 초과함false
INVALID_GESTURE_IDgesture ID가 UUID가 아니거나 64바이트를 초과함false
PAIRED_UPDATE_REQUIRED키 구조 변경에 짝 컬렉션이 빠짐false
MULTI_KEY_UNSUPPORTED멀티 키 슬롯이 있는데 multiKey 선언 없이 keys를 씀false
MUTATION_ID_REUSED같은 mutation ID를 다른 요청에 재사용함false
IO_ERROR문서를 디스크에 저장하지 못함true
HISTORY_IN_PROGRESSundo/redo 진행 중. 끝난 뒤 재시도true
HISTORY_EPOCH_CONFLICT관측한 history epoch이 오래됨. 다시 조회한 뒤 커밋true

오류에 해당할 때 details.currentRevision, details.validationCode, details.field, details.currentHistoryEpoch가 함께 제공됩니다.

커밋된 변경 구독

dmn.editor.onCommitted(callback): ReadyUnsubscribe

canonical editor:committed 흐름을 구독합니다. 이벤트 하나가 성공한 원자적 에디터 커밋 하나를 나타냅니다.

interface EditorCommittedV1 { schemaVersion: 1; revision: number; mutationId: string; gestureId?: string | null; gestureIds?: string[]; origin?: string; changedFields: EditorField[]; // patch.keys는 KeySlot union(string | MultiKeySlot) patch: EditorPatchV1; } const unsubscribe = dmn.editor.onCommitted((event) => { console.log(event.revision, event.changedFields); applyEditorPatch(event.patch); }); await unsubscribe.ready; dmn.plugin.registerCleanup(() => { unsubscribe(); });

반환된 구독 해제 함수에는 ready: Promise<void> 속성이 있습니다. 스냅샷을 조회하기 전에 이벤트가 하나도 빠지면 안 되는 경우 await 하세요.

이벤트는 대응하는 commit() Promise가 resolve되기 전에 도착할 수 있습니다. mutationId와 revision으로 같은 변경을 두 번 적용하지 마세요. 수신한 revision이 하나 이상 건너뛰면 dmn.editor.get()으로 전체 스냅샷을 다시 맞추세요. 모르는 origin 값과 앞으로 추가될 모르는 필드는 무시하세요.

기존 변경 이벤트

아래 컬렉션별 이벤트는 기존 플러그인 호환을 위해 같은 페이로드로 계속 제공되지만, 신규 에디터 상태 동기화 용도로는 deprecated입니다. 여러 컬렉션을 바꾼 편집 하나를 순서가 있는 변경 하나로 받으려면 dmn.editor.onCommitted()를 사용하세요.

호환 이벤트대체 API
dmn.keys.onChanged()dmn.editor.onCommitted()
dmn.keys.onPositionsChanged()dmn.editor.onCommitted()
dmn.statItems.onPositionsChanged()dmn.editor.onCommitted()
dmn.graphItems.onPositionsChanged()dmn.editor.onCommitted()
dmn.knobItems.onPositionsChanged()dmn.editor.onCommitted()
dmn.layerGroups.onChanged()dmn.editor.onCommitted()

기존 플러그인은 당장 마이그레이션하지 않아도 됩니다. 신규 동기화 코드는 canonical 이벤트와 호환 이벤트를 둘 다 적용하지 말고 canonical 이벤트만 구독하세요.