Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
128 commits
Select commit Hold shift + click to select a range
8e79bd9
feat: 스프라이트 컬렉션 백엔드 저장·편집·프리셋 경로 추가
eun-yeon Aug 30, 2026
26d195c
feat: 반응형 스프라이트 타입과 에디터 문서 계약 확장
eun-yeon Aug 30, 2026
777ff55
feat: 편집 런타임·그룹·선택 경로에 스프라이트 편입
eun-yeon Aug 30, 2026
d0218a0
feat: 오버레이 창 스프라이트 렌더와 포즈 블렌딩 연결
eun-yeon Aug 30, 2026
5fd2f6f
feat: 에디터 캔버스 스프라이트 배치·생성 흐름 추가
eun-yeon Aug 30, 2026
4b8867d
feat: 스프라이트 속성 패널과 레이어 탭 지원
eun-yeon Aug 30, 2026
b1ef61f
feat: 스프라이트 자세 편집 팝업과 도달 범위 창 확장
eun-yeon Aug 30, 2026
39dec72
fix: 오버레이 스프라이트 클릭이 히트 영역에서 빠져 있던 문제
eun-yeon Aug 30, 2026
0cc9c36
refactor: 미사용 스프라이트 조회 경로와 잔재 정리
eun-yeon Aug 30, 2026
145dbc7
refactor: 스프라이트 중복 구현을 공용 유틸로 통합
eun-yeon Aug 30, 2026
a86c643
fix: 스프라이트 검증 상한과 wire 계약 정렬
eun-yeon Aug 30, 2026
2ffe034
fix: 스프라이트 트리거 순서를 백엔드 정규화와 일치
eun-yeon Aug 30, 2026
3564332
fix: 레이어 탭 우클릭에서 스프라이트 선택 누락
eun-yeon Aug 30, 2026
b69f236
fix: 스프라이트 오버행 시 오버레이 콘텐츠 원점 보정
eun-yeon Aug 30, 2026
0661c9f
fix: 스프라이트 패널 컴파일러 제외와 신규 자세 스크럽 정비
eun-yeon Aug 30, 2026
aedb58e
perf: 스프라이트 해석 캐시와 이미지 워밍업 동시성 제한
eun-yeon Aug 30, 2026
32b573c
fix: 스프라이트 스칼라 로드 복구와 저장 필드 정리
eun-yeon Aug 31, 2026
8854991
refactor: 프리뷰 죽은 스프라이트 필드와 knob 비교 루프 정리
eun-yeon Aug 31, 2026
9a96b0b
fix: 스프라이트 스키마 이원화와 poseId 경계 검증
eun-yeon Aug 31, 2026
60d43f6
refactor: 스프라이트 기본값 상수화와 미사용 export 정리
eun-yeon Aug 31, 2026
463edfd
test: 스프라이트 상한 파리티 경계 고정
eun-yeon Aug 31, 2026
1f2be47
docs: 에디터 문서에 스프라이트 타입 정의 추가
eun-yeon Aug 31, 2026
0324f13
feat: 상태 팝업 이미지 미리보기 카드와 행 전환 연속화
eun-yeon Aug 31, 2026
5254fb3
feat: 스프라이트 활동 영역 점선을 호버 가이드로 전환
eun-yeon Aug 31, 2026
f974a21
feat: 스프라이트 자세 전환 기본 시간 0으로 변경
eun-yeon Aug 31, 2026
3ef5e46
feat: 스프라이트 단발 재생과 손끝 고정 도우미
eun-yeon Aug 31, 2026
a19769a
refactor: 스프라이트 활동 영역 가이드 층 분리와 복제 고스트 추출
eun-yeon Aug 31, 2026
145b142
feat: 스프라이트 리사이즈 콘텐츠 비례 스케일
eun-yeon Aug 31, 2026
6ee83e0
fix: 이미지 선택 흐름 공용화와 컴파일러 최적화 제외 해소
eun-yeon Aug 31, 2026
9fb4a63
refactor: 스프라이트 기하 프리미티브와 클램프 공용화
eun-yeon Aug 31, 2026
86f2bd3
refactor: 미사용 스프라이트 표면과 죽은 렌더 계산 정리
eun-yeon Aug 31, 2026
8a7ba92
refactor: 이미지 상자 편집 한계 명명과 노브 기본 크기 정정
eun-yeon Aug 31, 2026
37adc20
docs: 커스텀 CSS 스프라이트 변수와 선택자 추가
eun-yeon Aug 31, 2026
b47e2a2
refactor: 스프라이트 자세 이름 규칙 순수 함수 분리
eun-yeon Aug 31, 2026
fd8e60c
refactor: 패널 헤더 이름 변경 블록 공용 컴포넌트로 통합
eun-yeon Aug 31, 2026
02966ea
refactor: 캔버스 아이템 상호작용 스캐폴드 공용 훅으로 통합
eun-yeon Aug 31, 2026
9f37e85
refactor: bounds op 적용 골격을 공통 헬퍼로 통합
eun-yeon Aug 31, 2026
4d4f250
test: 스프라이트 이미지의 복구 다음 세션 sweep 보호 고정
eun-yeon Aug 31, 2026
0a24c00
test: 스프라이트 수치 검증과 복구 범위의 표 일치 고정
eun-yeon Aug 31, 2026
804b36c
refactor: 복제 고스트 껍데기와 오버레이 이미지 복원 공용화
eun-yeon Aug 31, 2026
9ed13c7
refactor: 캔버스 잎 props 계약 통일과 종류별 렌더 함수 통합
eun-yeon Aug 31, 2026
bf5aab3
refactor: bounds intent 조립 규칙 단일 구현으로 통합
eun-yeon Aug 31, 2026
663217f
test: 스프라이트 픽스처 빌더 공용화
eun-yeon Aug 31, 2026
34bed51
fix: 이미지 연결 콜백까지 재진입 가드와 예외 처리 범위 복원
eun-yeon Aug 31, 2026
1150105
test: 캔버스 아이템 상호작용 훅 계약 고정과 죽은 mock 정리
eun-yeon Aug 31, 2026
cd79814
test: 혼합 배치 스프라이트 bounds 조회 키와 클램프 경계 고정
eun-yeon Aug 31, 2026
fdd7c10
fix: 무효 문법 easing을 렌더 폴백 곡선으로 강등
eun-yeon Sep 1, 2026
4680563
fix: 키 눌림 edge 구독자별 예외 격리
eun-yeon Sep 1, 2026
495f9d6
fix: 오버레이 스프라이트 클래스 래퍼와 표식 표면 분리
eun-yeon Sep 1, 2026
64542c5
fix: 단발 스프라이트 이미지 실패 시 기본 이미지 복원
eun-yeon Sep 1, 2026
5aff98b
fix: 외부 canonical 변경 시 무효 자세 초안 폐기
eun-yeon Sep 1, 2026
32498da
fix: 엔진이 거부한 easing의 스냅 폴백과 자세 transform 복원
eun-yeon Sep 1, 2026
b1487af
fix: 자세 기즈모 드래그의 소유권 확인 시점 교정
eun-yeon Sep 1, 2026
6a3ca48
docs: 스프라이트 easing 화이트리스트 기준 주석 정정
eun-yeon Sep 1, 2026
620c934
fix: 온캔버스 편집 오버레이 마커 통일과 자세 기즈모 닫힘 예외
eun-yeon Sep 1, 2026
f368f42
fix: 편집 캔버스 스프라이트 이미지 실패 폴백
eun-yeon Sep 1, 2026
0e1bd05
fix: 스프라이트 자세 ID를 값 기준으로 승계
eun-yeon Sep 1, 2026
9256eeb
fix: 자세 위반의 안정 ID 추적을 보완
eun-yeon Sep 1, 2026
b0b35bb
fix: 스프라이트 재생 시간 프리뷰를 허용
eun-yeon Sep 1, 2026
6c4e4b0
fix: 끊긴 자세 드래그를 안전하게 취소
eun-yeon Sep 1, 2026
822cc61
fix: 이미지 워밍업 슬롯을 만료 처리
eun-yeon Sep 1, 2026
57742ea
fix: 넘치는 easing 제어점의 도달 범위 폴백
eun-yeon Sep 1, 2026
391f457
fix: 이전 모드 키 이벤트가 새 탭에 적용되던 문제
eun-yeon Sep 1, 2026
7e54c5a
fix: 엔진이 거부하는 easing의 whileHeld 전환 폴백
eun-yeon Sep 1, 2026
40f7d00
fix: 공백 이미지 참조를 이미지 없음으로 통일
eun-yeon Sep 1, 2026
e7dc6ad
fix: 혼합 선택 크기 조절의 표시값과 미리보기 불일치
eun-yeon Sep 1, 2026
e0a31f9
fix: 재생될 수 없는 자세가 넓히던 오버레이 도달 범위
eun-yeon Sep 1, 2026
e068f03
fix: 다른 탭에 붙여넣은 스프라이트 자세 트리거 결합
eun-yeon Sep 1, 2026
2824b27
fix: 도달 불가능한 자세까지 세던 오버레이 도달 범위
eun-yeon Sep 1, 2026
7872a34
fix: 스프라이트 상태 속성이 키 선택자에 잡히던 문제
eun-yeon Sep 1, 2026
a0569fa
fix: 탭 전환이 스프라이트 외 복제까지 취소하던 문제
eun-yeon Sep 1, 2026
b6cce89
fix: 오버레이 리사이즈마다 반복되던 store 전체 복제
eun-yeon Sep 1, 2026
f811fa5
fix: 자세 착지 판정이 직렬화 키 순서에 흔들리던 문제
eun-yeon Sep 1, 2026
936fa05
docs: 스프라이트 자세 해석 규칙과 오버레이 리사이즈 필드 보강
eun-yeon Sep 1, 2026
6288fa0
refactor: 스프라이트 내부 export와 주석 정리
eun-yeon Sep 1, 2026
8af9802
fix: 자세 기즈모 드래그 중 undo 뒤 이전 변형 재커밋
eun-yeon Sep 2, 2026
a6bd504
fix: 문서 복제와 키 맵 재생성이 onPress 재생을 끊던 문제
eun-yeon Sep 2, 2026
e93d548
fix: 설정 카드와 복제 고스트의 유실 이미지 자리표시자
eun-yeon Sep 2, 2026
168b840
fix: 인라인 우선이 꺼진 스프라이트의 onPress가 사용자 transform을 덮던 문제
eun-yeon Sep 2, 2026
666b56c
fix: OBS 윈도우 캡처가 오버레이 대신 메인 창을 잡던 문제
eun-yeon Sep 2, 2026
59b9726
fix: 자세 팝업 손끝 카드 구조와 분리 창 폭 정렬
eun-yeon Sep 2, 2026
0339558
feat: 스프라이트 축 배치 계약과 원본 크기 필드
eun-yeon Sep 2, 2026
68752d1
feat: 축 배치 기하와 렌더러 연결
eun-yeon Sep 2, 2026
6e17467
feat: 자세 축 편집과 배치 전환 UI, 이미지 상자 맞춤
eun-yeon Sep 2, 2026
3484136
feat: 축 배치 백엔드 검증과 presence 병합
eun-yeon Sep 2, 2026
50cc887
docs: 스프라이트 축 배치 계약과 CSS 변수 범위
eun-yeon Sep 2, 2026
44ac657
feat: 새 스프라이트 축 배치 기본값과 기준점 상시 표시
eun-yeon Sep 2, 2026
9326dec
test: 리사이즈 투영 테스트 자세 리터럴에 축 필드 추가
eun-yeon Sep 2, 2026
0e3343b
fix: 자세 편집 중 기준점 표식이 기즈모 축과 따로 보이던 문제
eun-yeon Sep 2, 2026
b1399cd
feat: 스프라이트 상태별 기준점 백엔드 모델과 검증
eun-yeon Sep 3, 2026
b3d8e25
feat: 스프라이트 기준점 기하와 자세 의도 병합
eun-yeon Sep 3, 2026
68132ad
feat: 스프라이트 기준점 캔버스 핸들과 패널 편집 재설계
eun-yeon Sep 3, 2026
bdd9388
test: 스프라이트 기준점·자세 편집 회귀 테스트
eun-yeon Sep 3, 2026
ad0d857
docs: 스프라이트 기준점 편집 API 문서
eun-yeon Sep 3, 2026
c4c4dcd
fix: 상태 팝업 기준점 구간을 설정 카드 문법으로 정리
eun-yeon Sep 4, 2026
56d2ac7
fix: 입력 직후 자세 핸들 드래그가 옛 값에서 시작하던 문제
eun-yeon Sep 4, 2026
510c0cc
fix: 그룹 리사이즈의 스프라이트 비율 고정과 최소 크기 보장
eun-yeon Sep 4, 2026
4679698
fix: 빈 탭 프리셋이 기존 요소를 남기던 문제
eun-yeon Sep 4, 2026
b122c92
fix: OBS 이미지 서빙 형식을 선택창 목록과 통일
eun-yeon Sep 4, 2026
24c56c6
fix: 스프라이트 복제 고스트에 사용자 CSS 반영
eun-yeon Sep 4, 2026
74fe1a8
fix: 스프라이트 크기 입력의 파생 축 상한
eun-yeon Sep 4, 2026
0699514
feat: 비율 고정 리사이즈 배율 범위와 백엔드 상한 판정 공용화
eun-yeon Sep 4, 2026
43a219f
fix: 단일 리사이즈 비율 고정 재유도와 잡은 축 크기 일치
eun-yeon Sep 4, 2026
0a0d464
fix: 그룹 리사이즈 상한과 잡은 축 기준 최소 크기
eun-yeon Sep 4, 2026
4bdb9d2
fix: 그룹 축소 하한의 투영 반올림과 얇은 축 처리
eun-yeon Sep 4, 2026
27b91e4
fix: 담당 키 지정 전 고른 첫 상태 이미지의 기준 크기 보존
eun-yeon Sep 4, 2026
878c4e9
feat: SIGTERM 수신 시 프론트 정산을 거치는 종료 요청
eun-yeon Sep 5, 2026
44ad8f6
fix: 영속 쓰기 API를 종료 barrier 추적에 연결
eun-yeon Sep 5, 2026
76f1330
fix: 예약 커밋을 barrier 안에서 시작하고 완료까지 정산
eun-yeon Sep 5, 2026
6a84f62
fix: history 잠금 시작 시 진행 중 드래그·스크럽·핸들 취소
eun-yeon Sep 5, 2026
89fca81
fix: 분리 패널 document에도 history 잠금 적용
eun-yeon Sep 5, 2026
705bf66
fix: 미완성 형제 자세와 무관하게 편집한 자세 저장
eun-yeon Sep 5, 2026
392314d
fix: 플러그인 설정 저장 실패 전파와 저장값 복원
eun-yeon Sep 5, 2026
4fe06e2
fix: 노트 설정·이름 변경·배경색 저장 실패 시 초안 유지와 안내
eun-yeon Sep 5, 2026
ac95d10
fix: 설정 화면 저장 실패 복원과 확인 콜백 대기
eun-yeon Sep 5, 2026
7f575b9
feat: 미저장 편집 안내와 다시 저장
eun-yeon Sep 5, 2026
82b0e5d
fix: 파일 없는 내장 CSS 표시와 제거 지원
eun-yeon Sep 5, 2026
20b0e29
fix: macOS 오버레이 반복 토글의 누적 위치 이동 방지
eun-yeon Sep 5, 2026
2e5d2ea
merge: origin/main 모듈 경계 분리와 2.0.2 변경 통합
eun-yeon Sep 5, 2026
803ccae
fix: macOS 히트 패널 독립 창 정렬과 부모 변경 추종
eun-yeon Sep 6, 2026
9d1dedb
feat: 요소 회전 필드 저장 모델과 편집 검증
eun-yeon Sep 6, 2026
62d1f9c
feat: 회전 계약 스키마와 회전 기하 정본
eun-yeon Sep 6, 2026
dbb6bb4
feat: 오버레이 요소 회전 렌더와 노트 트랙 추종
eun-yeon Sep 6, 2026
53b54f2
feat: 캔버스 회전 조작과 회전 요소 리사이즈
eun-yeon Sep 6, 2026
8ba6d86
feat: 다중 선택 공통 회전과 회전 틀 리사이즈
eun-yeon Sep 6, 2026
3676115
docs: 요소 회전 필드와 자동 시작선 안내
eun-yeon Sep 6, 2026
38d228a
merge: 구조 정리와 스프라이트·회전 편집 계약 통합
lee-sihun Sep 6, 2026
a802cb1
merge: 검토 완료된 구조 정리 병합 이력 반영
lee-sihun Sep 6, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
163 changes: 135 additions & 28 deletions docs/content/en/api-reference/editor/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ description: Atomic editor document reads, commits, and change events
document.
</Callout>

`dmn.editor` reads and updates the six collections that make up the editor
`dmn.editor` reads and updates the seven collections that make up the editor
layout as one revisioned document. Use it when one user action changes more
than one collection, or when a consumer needs a single ordered change stream.

Expand All @@ -24,6 +24,7 @@ type EditorField =
| 'statPositions'
| 'graphPositions'
| 'knobPositions'
| 'spritePositions'
| 'layerGroups';

interface EditorDocumentV1 {
Expand All @@ -33,6 +34,7 @@ interface EditorDocumentV1 {
statPositions: StatItemPositions;
graphPositions: GraphItemPositions;
knobPositions: KnobItemPositions;
spritePositions: SpritePositions;
layerGroups: LayerGroups;
}

Expand All @@ -47,22 +49,127 @@ that top-level collection. It is not an item-level diff.
<Callout type="warning">
Since the multi-key update, `keys` entries in `EditorDocumentV1` (returned by
`get()`) and in `EditorPatchV1` (delivered by `onCommitted()`) use the
`KeySlot` union (`string | MultiKeySlot`), not plain strings. See the
[Keys API](/docs/api-reference/keys) for the slot shape and canonical
identifier rules.
`KeySlot` union (`string | MultiKeySlot`), not plain strings. See the [Keys
API](/docs/api-reference/keys) for the slot shape and canonical identifier
rules.
</Callout>

<Callout>
Every element in the position collections (`keyPositions`, `statPositions`,
`graphPositions`, `knobPositions`) carries a stable `id` (UUID) assigned and
owned by the app. Treat it as opaque: echo back the value you read and never
invent one. A written element with a missing or unknown `id` gets a fresh one
from the backend. If the same `id` appears more than once, for example when
you copy an element you read and append it, the element in its original slot
keeps the identity and the remaining copies get fresh ones. See the
[Keys API](/docs/api-reference/keys) for the identity rules.
`graphPositions`, `knobPositions`, `spritePositions`) carries a stable `id`
(UUID) assigned and owned by the app. Treat it as opaque: echo back the value
you read and never invent one. A written element whose `id` is missing or
unknown inherits the `id` of an existing element whose remaining fields match;
with no match, the backend assigns a fresh one. Omitting an `id` does not
guarantee a fresh identity, and editing an element's values while omitting its
`id` may give it one. If the same `id` appears more than once, for example
when you copy an element you read and append it, the element in its original
slot keeps the identity and the remaining copies get fresh ones. The `poseId`
carried by each pose of a sprite element follows the same rules. See the [Keys
API](/docs/api-reference/keys) for the identity rules.
</Callout>

## Sprite Position Types

Reactive sprites are image layers that switch poses while their trigger keys
are held. Unlike key elements they are not index-coupled to key mappings; each
pose references the responsible key element ids directly. Documents you read
always carry `id` and `poseId`; echo both back when you write.
The collection and value limits noted below are enforced when you commit;
documents you read echo stored data as is.

```typescript
// Keyed by tab/mode id (e.g., "4key")
type SpritePositions = Record<string, ReactiveSpritePosition[]>;

interface ReactiveSpritePosition {
id?: string; // stable element identity (UUID)
dx: number; // element box origin and size. The box is the base image box and feeds overlay window sizing
dy: number;
width: number;
height: number;
rotation?: number; // placement rotation around the element-box center, -180–180 deg, default 0
hidden: boolean;
zIndex: number | null;
layerName?: string | null; // omitted by the backend when unset
groupId?: string | null; // omitted by the backend when unset
className: string | null;
useInlineStyles: boolean | null;
baseImage: string | null; // image shown when no pose matches; always fills the element box
referenceNaturalSize: {
source: string | null;
width: number;
height: number;
} | null; // intrinsic pixel size that defines the pixel scale (integers, 1–32768). source must equal baseImage when one is set and be null otherwise; null falls back to drawing every image in the element box
pivot: { x: number; y: number }; // normalized shared axis, 0–1 each, default {0.5, 0.5}. Base-image pivot and transform origin for every pose
idleTransform: SpriteTransform; // base transform when no pose matches
poses: SpritePose[]; // commits accept at most 64
activation: 'whileHeld' | 'onPress'; // default whileHeld. onPress plays the pose once on key-down, then returns to idle
pressDurationMs: number; // onPress playback duration, 1–5000, default 300
transitionMs: number; // pose transition duration, 0–1000, default 0. Ignored by onPress, which plays for pressDurationMs
transitionEasing: string; // CSS easing, default 'cubic-bezier(0.4, 0, 0.2, 1)'
}

interface SpritePose {
poseId?: string; // stable pose identity (UUID)
name?: string | null; // omitted by the backend when unset
triggers: string[]; // key element ids; commits accept at most 512, sorted and deduplicated
transform: SpriteTransform;
pivot?: { x: number; y: number } | null; // independent pivot within the pose image; null or omission follows the sprite's shared pivot
imageOverride: string | null; // replaces baseImage while this pose is active
imageOverrideMetrics: {
source: string;
width: number;
height: number;
} | null; // intrinsic pixel size of imageOverride (integers, 1–32768). Optional; when set, source must equal the path exactly. Normalized to null when imageOverride is absent
}

interface SpriteTransform {
x: number; // -2000–2000 px
y: number; // -2000–2000 px
rotation: number; // -180–180 deg
scale: number; // 0.1–10
}
```

The element-level `rotation` rotates the entire sprite, including all poses,
around the center of its logical box. It is independent of
`idleTransform.rotation` and each pose's `transform.rotation`, which remain
local rotations around the shared pivot. Rotating a selection changes the
sprite's `dx`, `dy`, and element-level `rotation` together; pose transforms,
triggers, image references, and transition directions remain unchanged.
New sprites default to 0 when `rotation` is omitted. For an existing sprite,
schemaVersion 1 and 2 commits preserve its stored rotation when the field is
omitted. `null`, non-finite values, and values outside `-180..180` are rejected.

A pose activates while the set of held keys, among the keys the sprite
references, exactly matches its trigger set; keys the sprite does not reference
are ignored. When no pose matches exactly, the single-trigger poses of the held
keys are blended with equal weight: x, y and scale are averaged, rotation uses a
circular mean, and the image comes from the first override in poseId order.
Poses with two or more triggers never take part in blending, so a sprite whose
held keys only appear in such poses returns to idle. Two poses of one sprite may
not share the same trigger set, and commits reject empty trigger lists.

The element box is the base-image box. The base image always fills it, and the
shared axis P is the sprite pivot × (width, height). A pose whose pivot is null
or omitted uses the same normalized point. A pose with a pivot value aligns that
independent point within its image to P. A pose image with a different intrinsic
size is drawn at intrinsic pixels × the reference scale, where the reference
scale is (width, height) ÷ referenceNaturalSize per axis. Without a reference
size or the pose image's own metrics, the image uses the element-box size.
Moving the shared pivot keeps the base image in place while linked poses follow
the new axis. Poses with an independent pivot keep their screen position by
compensating x/y in the same commit. Moving a pose pivot or switching the link
also compensates x/y to avoid a sudden image jump. An out-of-range pivot
rejects the commit with `VALIDATION_FAILED` and a `details.validationCode` of
`INVALID_SPRITE_PIVOT` or `INVALID_SPRITE_POSE_PIVOT`. Invalid image metrics use
`STALE_SPRITE_IMAGE_METRICS` or `INVALID_SPRITE_IMAGE_METRICS`. A schemaVersion 1
patch that omits `pivot`, `referenceNaturalSize`, `imageOverrideMetrics`,
`activation`, or `pressDurationMs` on an existing element or pose keeps the
stored value. Sending null for a pose pivot links it to the shared pivot.
Changing `baseImage` or `imageOverride` while omitting its metrics clears them.

## Read the Current Document

### `dmn.editor.get(): Promise<EditorGetResult>`
Expand Down Expand Up @@ -94,10 +201,10 @@ platform, while no application can guarantee recovery from hardware or
firmware that violates those barriers.

<Callout type="info">
The app's Undo/Redo path also restores the six editor collections, custom-tab
metadata, selected mode, counters, preset settings, and per-tab note settings
in one backend store transaction. This internal command is intentionally not
part of the public plugin API.
The app's Undo/Redo path also restores the seven editor collections,
custom-tab metadata, selected mode, counters, preset settings, and per-tab
note settings in one backend store transaction. This internal command is
intentionally not part of the public plugin API.
</Callout>

```typescript
Expand Down Expand Up @@ -218,19 +325,19 @@ interface EditorCommitError {
}
```

| Code | Meaning | `retryable` |
| ------------------------ | ------------------------------------------------------------------------- | ----------- |
| `REVISION_CONFLICT` | `baseRevision` is stale; read, reconcile, and commit again | `true` |
| `PLUGIN_REVISION_CONFLICT` | The plugin-scoped revision is stale; read, reconcile, and commit again | `true` |
| `VALIDATION_FAILED` | The completed document violates an editor validation rule | `false` |
| `TOO_MANY_GESTURE_IDS` | `gestureIds` exceeds 32 entries or the combined unique set exceeds 32 IDs | `false` |
| `INVALID_GESTURE_ID` | A gesture ID is not a UUID within the 64-byte limit | `false` |
| `PAIRED_UPDATE_REQUIRED` | A structural key change omitted its paired collection | `false` |
| `MULTI_KEY_UNSUPPORTED` | A `keys` write did not declare `multiKey` while multi-key slots exist | `false` |
| `MUTATION_ID_REUSED` | The same mutation ID was used for a different request | `false` |
| `IO_ERROR` | The document could not be persisted | `true` |
| `HISTORY_IN_PROGRESS` | An undo/redo barrier is in progress; retry after it settles | `true` |
| `HISTORY_EPOCH_CONFLICT` | The observed history epoch is stale; re-read and commit again | `true` |
| Code | Meaning | `retryable` |
| -------------------------- | ------------------------------------------------------------------------- | ----------- |
| `REVISION_CONFLICT` | `baseRevision` is stale; read, reconcile, and commit again | `true` |
| `PLUGIN_REVISION_CONFLICT` | The plugin-scoped revision is stale; read, reconcile, and commit again | `true` |
| `VALIDATION_FAILED` | The completed document violates an editor validation rule | `false` |
| `TOO_MANY_GESTURE_IDS` | `gestureIds` exceeds 32 entries or the combined unique set exceeds 32 IDs | `false` |
| `INVALID_GESTURE_ID` | A gesture ID is not a UUID within the 64-byte limit | `false` |
| `PAIRED_UPDATE_REQUIRED` | A structural key change omitted its paired collection | `false` |
| `MULTI_KEY_UNSUPPORTED` | A `keys` write did not declare `multiKey` while multi-key slots exist | `false` |
| `MUTATION_ID_REUSED` | The same mutation ID was used for a different request | `false` |
| `IO_ERROR` | The document could not be persisted | `true` |
| `HISTORY_IN_PROGRESS` | An undo/redo barrier is in progress; retry after it settles | `true` |
| `HISTORY_EPOCH_CONFLICT` | The observed history epoch is stale; re-read and commit again | `true` |

`details.currentRevision`, `details.validationCode`, `details.field`, or
`details.currentHistoryEpoch` is included when it applies to the error.
Expand Down
36 changes: 31 additions & 5 deletions docs/content/en/api-reference/keys/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -337,9 +337,20 @@ gradient siblings:
interface KeyPosition {
id?: string; // stable element identity (UUID), assigned and owned by the app
// ...position, image, note, and counter fields...
rotation: number; // degrees, -180 to 180. Defaults to 0. Content rotation about the box center
imageMode?: 'replace' | 'overlay'; // image placement, defaults to replace (image replaces the key)
idleImageTransform?: { offsetX: number; offsetY: number; rotation: number; scale: number }; // identity when absent
activeImageTransform?: { offsetX: number; offsetY: number; rotation: number; scale: number };
idleImageTransform?: {
offsetX: number;
offsetY: number;
rotation: number;
scale: number;
}; // identity when absent
activeImageTransform?: {
offsetX: number;
offsetY: number;
rotation: number;
scale: number;
};
noteOpacityTop?: number;
noteOpacityBottom?: number;
noteGradient?: GradientSpec | null;
Expand Down Expand Up @@ -387,10 +398,25 @@ interface ElementShadowSpec {
}
```

`rotation` is the element rotation in degrees. Keys, stats, graphs, and knobs
share the field. The logical box (`dx`/`dy`/`width`/`height`) stays axis
aligned while the face, the note track, and the outside counter rotate
together about the box center. Omitting the field means 0; `null`, non-finite
values, and values outside `-180..180` are rejected. Automatic hitline
correction (`noteAutoYCorrection`) aligns unrotated keys to the content top,
and aligns rotated keys that flow in the same direction (within 0.5°) to the
top edge that is furthest along that direction. A key rotated on its own
starts at its own top edge, and note offsets apply after the correction.
Sprites also have an element-level placement `rotation`, independent of their
idle and per-pose rotations. See [Sprite Position Types](/docs/api-reference/editor#sprite-position-types).

Every element position (`keyPositions`, `statPositions`, `graphPositions`,
`knobPositions`) carries a stable `id`. Treat it as opaque: echo back the value
you read and never invent one. A write whose `id` is missing or unknown gets a
fresh one assigned by the backend. When the same `id` appears more than once,
`knobPositions`, `spritePositions`) carries a stable `id`. Treat it as opaque: echo back the value
you read and never invent one. A write whose `id` is missing or unknown
inherits the `id` of an existing element whose remaining fields match; with no
match, the backend assigns a fresh one. Omitting an `id` does not guarantee a
fresh identity, and editing an element's values while omitting its `id` may
give it one. When the same `id` appears more than once,
for example when you copy an element you read and append it, the element in
its original slot keeps the identity and the remaining copies get fresh ones.
Loading a full preset re-issues every `id`; a tab preset re-issues only the
Expand Down
1 change: 1 addition & 0 deletions docs/content/en/api-reference/overlay/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ interface ResizePayload {
width: number;
height: number;
anchor?: string;
contentLeftOffset?: number;
contentTopOffset?: number;
fixedPositionDeltaX?: number;
fixedPositionDeltaY?: number;
Expand Down
39 changes: 22 additions & 17 deletions docs/content/en/api-reference/presets/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@ Saves the current configuration to a preset file. Opens a file save dialog.
```javascript
const result = await dmn.presets.save();
if (result.success) {
console.log("Preset saved");
console.log('Preset saved');
} else {
console.error("Save failed:", result.error);
console.error('Save failed:', result.error);
}
```

Expand All @@ -37,9 +37,9 @@ picker dialog.
```javascript
const result = await dmn.presets.load();
if (result.success) {
console.log("Preset loaded");
console.log('Preset loaded');
} else {
console.error("Load failed:", result.error);
console.error('Load failed:', result.error);
}
```

Expand All @@ -56,7 +56,10 @@ const result = await dmn.presets.saveTab();

Loads a tab preset file into the currently selected tab. Opens a file picker
dialog. Tab presets carry no `tabOrder` or `barCount`, so they never change the
global tab order.
global tab order. Element collections stored in the file (keys, stats, graphs,
knobs, sprites) replace the tab's contents wholesale, so elements missing from
the file's tab are removed. Older presets that lack a collection entirely leave
those elements untouched.

```javascript
const result = await dmn.presets.loadTab();
Expand All @@ -80,6 +83,7 @@ interface PresetSnapshot {
statPositions: StatItemPositions;
graphPositions: GraphItemPositions;
knobPositions: KnobItemPositions;
spritePositions: SpritePositions;
customTabs: CustomTab[];
tabOrder: string[];
barCount: number;
Expand All @@ -88,48 +92,49 @@ interface PresetSnapshot {
}

const unsub = dmn.presets.onSnapshot((snapshot) => {
console.log("Preset loaded:", snapshot.selectedKeyType);
console.log("Key mappings:", snapshot.keys);
console.log('Preset loaded:', snapshot.selectedKeyType);
console.log('Key mappings:', snapshot.keys);
});
```

<Callout type="info">
Individual change events (`onChanged`, `onPositionsChanged`, etc.) are not emitted during preset load.
Use `onSnapshot` to detect preset loads.
Individual change events (`onChanged`, `onPositionsChanged`, etc.) are not
emitted during preset load. Use `onSnapshot` to detect preset loads.
</Callout>

---

## Example Usage

```javascript
// @id preset-manager

if (dmn.window.type !== "main") return;
if (dmn.window.type !== 'main') return;

// Save preset menu item
dmn.ui.contextMenu.addGridMenuItem({
id: "save-preset",
label: "Save Preset",
id: 'save-preset',
label: 'Save Preset',
onClick: async () => {
const result = await dmn.presets.save();
if (result.success) {
await dmn.ui.dialog.alert("Preset saved!");
await dmn.ui.dialog.alert('Preset saved!');
}
},
});

// Load preset menu item
dmn.ui.contextMenu.addGridMenuItem({
id: "load-preset",
label: "Load Preset",
id: 'load-preset',
label: 'Load Preset',
onClick: async () => {
const confirmed = await dmn.ui.dialog.confirm(
"This will overwrite the current configuration. Continue?"
'This will overwrite the current configuration. Continue?',
);
if (confirmed) {
const result = await dmn.presets.load();
if (result.success) {
await dmn.ui.dialog.alert("Preset loaded!");
await dmn.ui.dialog.alert('Preset loaded!');
}
}
},
Expand Down
Loading
Loading