Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
ac3512b
feat: 요소 안정 ID 발급과 커밋 v2 검증 도입
eun-yeon Aug 11, 2026
20dc2d1
feat: 선택·렌더 신원을 요소 안정 ID로 전환
eun-yeon Aug 11, 2026
db94a94
fix: 커밋 wire 버전을 전송 경로가 결정하도록 일원화
eun-yeon Aug 11, 2026
81129d8
fix: 비동기 완료를 ID 기반 적용자로 전환
eun-yeon Aug 11, 2026
bbc450a
docs: 요소 안정 ID 신원 규칙 문서화
eun-yeon Aug 11, 2026
715899e
fix: 베지어 편집기 캔버스 비율 왜곡 수정
eun-yeon Aug 11, 2026
6b1e844
test: 프리셋 편집의 전 모드 전파를 백엔드 계약으로 고정
eun-yeon Aug 11, 2026
797820b
fix: v1 재정렬에서 안정 ID가 키 슬롯을 따라가도록 수정
eun-yeon Aug 11, 2026
dd2e993
fix: 격리 커밋의 canonical 재동기화로 요소 ID 유실 방지
eun-yeon Aug 11, 2026
e55469c
test: 격리 커밋 테스트 대역을 실백엔드 계약에 맞춤
eun-yeon Aug 11, 2026
142f7f3
fix: 격리 커밋 target을 pending으로 오인하지 않게 수정
eun-yeon Aug 11, 2026
5ea6cb0
fix: 자사 커밋 base가 격리 in-flight target을 참조하지 않게 수정
eun-yeon Aug 11, 2026
1790d73
fix: 비동기 완료 커밋을 직렬 슬롯에서 재생성해 대기 중 저장 손실 방지
eun-yeon Aug 11, 2026
255913f
feat: 배치 패널 비동기 완료를 시작 선택 ID 결합으로 전환
eun-yeon Aug 11, 2026
9bf0f72
test: 삭제 후 재발급 요소에 옛 ID 완료가 닿지 않음을 고정
eun-yeon Aug 11, 2026
86a421d
feat: 키 메뉴 컨텍스트에 요소 안정 ID 노출
eun-yeon Aug 11, 2026
c482330
docs: 프리셋 ID 재발급과 노브 안정 ID 문서화
eun-yeon Aug 11, 2026
dd80351
refactor: 미사용 요소 참조 캡처 제거
eun-yeon Aug 11, 2026
5450aa3
feat: 편집 커밋에 지연 생성 확장과 배타 legacy 실행 기반 추가
eun-yeon Aug 12, 2026
46c024e
fix: 문서 재작성 legacy 커맨드를 배타 직렬화하고 플러그인 위치 쓰기를 격리
eun-yeon Aug 12, 2026
2d4852d
fix: 게스처 정산 의도를 요소 안정 ID로 보존
eun-yeon Aug 12, 2026
f7dad0a
fix: 그리드 지연 대상 액션과 다중 편집을 안정 ID로 결합
eun-yeon Aug 12, 2026
d519452
fix: 요소 의도 커밋 러너와 실패 복원 소유권 정리
eun-yeon Aug 12, 2026
aae45a6
fix: 요소 op의 satisfied 판별과 bounds 의도 커밋 추가
eun-yeon Aug 12, 2026
f232a9f
fix: 리사이즈 정산을 시작 시점 동결 대상으로 내재화
eun-yeon Aug 12, 2026
a369dca
fix: 레이어 DnD를 mouseup 라이브 모델과 이웃 앵커로 재해석
eun-yeon Aug 12, 2026
7d28e70
fix: 게스처 커밋을 슬롯 재생성 generator로 확장
eun-yeon Aug 12, 2026
fcef0b4
fix: 합성 index 의도와 봉인 receipt 계열 추가
eun-yeon Aug 12, 2026
2c3ece0
fix: 혼합 게스처를 슬롯 정합 의도 커밋으로 전환
eun-yeon Aug 12, 2026
58db103
fix: 리사이즈와 DnD 정산의 full-record 커밋 제거
eun-yeon Aug 12, 2026
9e86652
fix: 삭제와 붙여넣기를 동결 계획과 슬롯 재생성으로 재작성
eun-yeon Aug 12, 2026
c00b0b4
fix: 재시도와 충돌 정산에서 거절 프리뷰와 게스처 ID 정리
eun-yeon Aug 12, 2026
1a44c28
fix: 네이티브 전용 붙여넣기를 editor 단독 커밋으로 라우팅
eun-yeon Aug 12, 2026
43e82af
fix: 재정렬 중 리사이즈 프리뷰를 동결 대상으로 라우팅
eun-yeon Aug 12, 2026
13a1615
fix: 영구 저장 실패 안내를 용량 초과와 일반으로 분리
eun-yeon Aug 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/content/en/api-reference/editor/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ that top-level collection. It is not an item-level diff.
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 or duplicate one. A written element with a missing or unknown `id`
gets a fresh one from the backend. See the
[Keys API](/docs/api-reference/keys) for the identity rules.
</Callout>

## Read the Current Document

### `dmn.editor.get(): Promise<EditorGetResult>`
Expand Down
17 changes: 17 additions & 0 deletions docs/content/en/api-reference/keys/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -258,6 +258,7 @@ gradient siblings:

```typescript
interface KeyPosition {
id?: string; // stable element identity (UUID), assigned and owned by the app
// ...position, image, note, and counter fields...
backgroundColor?: string;
activeBackgroundColor?: string;
Expand Down Expand Up @@ -293,6 +294,13 @@ interface ElementShadowSpec {
}
```

Every element position (`keyPositions`, `statPositions`, `graphPositions`,
`knobPositions`) carries a stable `id`. Treat it as opaque: echo back the value
you read, never invent one, and never copy an `id` onto another element. A
write whose `id` is missing or unknown gets a fresh one assigned by the
backend, and loading a preset re-issues every `id`. Use `id` to track an
element across reorders instead of its array index.

When a gradient field is present it takes priority over the matching solid
field, and the solid field is kept in sync with the first stop color on save.
To return to a solid color, set the gradient field to `null` and update the
Expand All @@ -315,6 +323,15 @@ positions['4key'][0].dx = 100;
const committed = await dmn.keys.updatePositions(positions);
```

Position writes from plugins are serialized on a dedicated queue and settle
against the committed document: the promise resolves only after the commit and
the follow-up read complete, and the returned collections carry the stable
element `id` values the app assigned (elements submitted without an `id` get
one issued). On failure the original error is rejected as-is; if a commit
might have succeeded but the follow-up read failed, read the current value
before retrying instead of resubmitting blindly. The same applies to
`statItems`, `graphItems`, and `knobItems` `updatePositions`.

<Callout type="warning">
`keys[mode][i]` and `keyPositions[mode][i]` are coupled by index. A standalone
`update()` or `updatePositions()` may fail with `PAIRED_UPDATE_REQUIRED` if it
Expand Down
2 changes: 2 additions & 0 deletions docs/content/en/api-reference/knobs/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ identifiers are stored in a knob element's `axisId`.

```typescript
// KeyPosition styling fields are inherited (position, size, colors, images...)
// including the stable element `id` (UUID): it keeps identifying the same knob
// across reorders and mode switches, and preset loads reissue new IDs
type KnobItemPosition = KeyPosition & {
axisId: string; // bound HID axis ("HIDA:..."), empty if unbound
sensitivity: number; // rotation multiplier (default 1)
Expand Down
2 changes: 2 additions & 0 deletions docs/content/en/api-reference/presets/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ const result = await dmn.presets.loadTab();

Subscribes to preset load completions. Receives the full snapshot of applied data (key mappings, positions, tabs, mode, etc.) in a single event.

Position collections in the snapshot carry element `id` values that were freshly issued during this load. Applying a preset rekeys every element with new UUIDs, so IDs from the preset file or from a previous load never survive into the applied state, and loading the same preset twice produces two different ID sets. Re-resolve any cached element IDs from this snapshot.

```typescript
interface PresetSnapshot {
// Entries use the KeySlot union (string | MultiKeySlot)
Expand Down
3 changes: 2 additions & 1 deletion docs/content/en/ui-api/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ const menuId = dmn.ui.contextMenu.addKeyMenuItem({
| Property | Type | Description |
| ---------- | ----------- | ----------------------- |
| `keyCode` | string | Canonical slot identifier (e.g. "D", "LEFT CTRL+Z", "F\|NUMPAD 4") |
| `index` | number | Key index |
| `id` | string | Stable element ID (UUID). Keeps identifying the same key across reorders and mode switches |
| `index` | number | Key index. Deprecated: only valid for the current snapshot, use `id` for identity |
| `position` | KeyPosition | Key position info |
| `mode` | string | Current key mode |

Expand Down
9 changes: 9 additions & 0 deletions docs/content/ko/api-reference/editor/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,15 @@ type EditorPatchV1 = {
식별자 규칙은 [Keys API](/docs/api-reference/keys)를 참고하세요.
</Callout>

<Callout>
위치 컬렉션(`keyPositions`, `statPositions`, `graphPositions`,
`knobPositions`)의 모든 요소는 앱이 발급·소유하는 안정 `id`(UUID)를
가집니다. 불투명 값으로 다루세요. 읽은 값을 그대로 되돌려 보내고, 직접
만들거나 중복시키지 마세요. `id`가 없거나 미확인인 요소를 쓰면 백엔드가 새
값을 발급합니다. 신원 규칙은 [Keys API](/docs/api-reference/keys)를
참고하세요.
</Callout>

## 현재 문서 조회

### `dmn.editor.get(): Promise<EditorGetResult>`
Expand Down
15 changes: 15 additions & 0 deletions docs/content/ko/api-reference/keys/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ fail-closed 게이트입니다. 같은 규칙이 `updateWithPositions()`와, `ke

```typescript
interface KeyPosition {
id?: string; // 요소 안정 ID (UUID). 앱이 발급·소유
dx: number;
dy: number;
width: number;
Expand Down Expand Up @@ -160,6 +161,13 @@ interface ElementShadowSpec {
}
```

모든 요소 위치(`keyPositions`, `statPositions`, `graphPositions`,
`knobPositions`)는 안정 `id`를 가집니다. 불투명 값으로 다루세요. 읽은 값을
그대로 되돌려 보내고, 직접 만들거나 다른 요소에 복사하지 마세요. `id`가
없거나 미확인인 쓰기는 백엔드가 새 값을 발급하며, 프리셋을 불러오면 모든
`id`가 재발급됩니다. 재정렬을 가로질러 요소를 추적할 때는 배열 index 대신
`id`를 사용하세요.

그라데이션 필드가 있으면 렌더에서 대응 단색 필드보다 우선하며, 저장 시
대응 단색 필드는 첫 스톱 색으로 자동 동기화됩니다. 단색으로 되돌리려면
그라데이션 필드를 `null`로 두고 단색 필드를 갱신하세요.
Expand All @@ -186,6 +194,13 @@ current['4key'][0].dx = 100;
await dmn.keys.updatePositions(current);
```

플러그인의 위치 쓰기는 전용 큐로 직렬화되어 확정 문서 기준으로 정산됩니다.
promise는 커밋과 후속 조회까지 끝난 뒤 resolve되고, 반환 컬렉션에는 앱이
부여한 요소 안정 `id`가 담깁니다 (`id` 없이 제출한 요소는 새로 발급됨).
실패 시 원 오류가 그대로 reject되며, 커밋은 성공했는데 후속 조회만 실패했을
가능성이 있으면 무작정 재제출하지 말고 현재 값을 먼저 조회해 확인하세요.
`statItems`·`graphItems`·`knobItems`의 `updatePositions`도 동일합니다.

### updateWithPositions(mappings, positions, options?)

키 매핑과 인덱스로 결합된 위치 정보를 한 번의 원자적 커밋으로 갱신합니다.
Expand Down
2 changes: 2 additions & 0 deletions docs/content/ko/api-reference/knobs/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ DmNote는 HID 입력에 고정 문자열 식별자를 부여합니다:

```typescript
// KeyPosition의 스타일 필드를 상속 (위치, 크기, 색상, 이미지 등)
// 요소 안정 id(UUID)도 포함: 재정렬·모드 전환에도 같은 노브를 가리키며,
// 프리셋 로드 시에는 새 ID가 발급됩니다
type KnobItemPosition = KeyPosition & {
axisId: string; // 바인딩된 HID 축("HIDA:..."), 미바인딩이면 빈 문자열
sensitivity: number; // 회전 배율 (기본 1)
Expand Down
2 changes: 2 additions & 0 deletions docs/content/ko/api-reference/presets/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,8 @@ dmn.plugin.registerCleanup(() => {

프리셋 로드 완료 시 적용된 스냅샷 데이터를 수신합니다. 키 매핑, 위치, 탭, 모드 등이 한 번에 전달됩니다.

스냅샷의 위치 컬렉션에는 이번 로드에서 새로 발급된 요소 `id`가 담깁니다. 프리셋 적용은 모든 요소를 새 UUID로 재발급하므로, 프리셋 파일 안의 ID나 이전 로드의 ID는 적용 상태로 이어지지 않고, 같은 프리셋을 두 번 적용하면 서로 다른 ID 집합이 생깁니다. 캐시해 둔 요소 ID는 이 스냅샷에서 다시 조회하세요.

```typescript
interface PresetSnapshot {
// 항목은 KeySlot union(string | MultiKeySlot)
Expand Down
3 changes: 2 additions & 1 deletion docs/content/ko/ui-api/page.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ const menuId = dmn.ui.contextMenu.addKeyMenuItem({
| 속성 | 타입 | 설명 |
| ---------- | ----------- | -------------------- |
| `keyCode` | string | canonical 슬롯 식별자 (예: "D", "LEFT CTRL+Z", "F\|NUMPAD 4") |
| `index` | number | 키 인덱스 |
| `id` | string | 요소 안정 ID (UUID). 재정렬·모드 전환에도 같은 키를 가리킵니다 |
| `index` | number | 키 인덱스. deprecated: 현재 스냅샷에서만 유효하며 신원은 `id`를 사용하세요 |
| `position` | KeyPosition | 키 위치 정보 |
| `mode` | string | 현재 키 모드 |

Expand Down
32 changes: 31 additions & 1 deletion src-tauri/src/commands/keys/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ fn reset_all_editor_data(store: &mut AppStoreData, keys: &KeyMappings, positions
store.selected_key_type = "4key".to_string();
store.tab_note_overrides.clear();
store.tab_css_overrides.clear();
crate::state::native_element_id::rekey_store_element_ids(store);
}

fn reset_mode_kind(store: &AppStoreData, mode: &str) -> Option<ModeResetKind> {
Expand Down Expand Up @@ -156,6 +157,7 @@ fn reset_mode_data(store: &mut AppStoreData, mode: &str, kind: ModeResetKind) {
.map(|key| (key.canonical(), 0))
.collect(),
);
crate::state::native_element_id::rekey_mode_element_ids(store, mode);
}

fn plan_custom_tab_delete(store: &AppStoreData, id: &str) -> Option<CustomTabDeletePlan> {
Expand Down Expand Up @@ -950,7 +952,7 @@ mod tests {
KnobPosition, LayerGroupDef, StatPosition, StatType, TabCss, TabNoteSettings,
},
};
use std::cell::Cell;
use std::{cell::Cell, collections::HashSet};

const TARGET_TAB: &str = "custom-target";

Expand Down Expand Up @@ -1064,6 +1066,34 @@ mod tests {
.all(|count| *count == 0));
}

#[test]
fn reset_all_issues_a_fresh_globally_unique_id_generation_each_time() {
let mut store = populated_custom_tab_store();
reset_all_editor_data(&mut store, default_keys(), default_positions());
let first = store
.key_positions
.values()
.flatten()
.map(|position| position.id.clone())
.collect::<HashSet<_>>();
let first_count = store.key_positions.values().map(Vec::len).sum::<usize>();

reset_all_editor_data(&mut store, default_keys(), default_positions());
let second = store
.key_positions
.values()
.flatten()
.map(|position| position.id.clone())
.collect::<HashSet<_>>();

assert_eq!(first.len(), first_count);
assert_eq!(second.len(), first_count);
assert!(first.is_disjoint(&second));
assert!(second
.iter()
.all(|id| crate::state::native_element_id::is_valid_element_id(id)));
}

#[test]
fn custom_mode_reset_is_supported_and_preserves_tab_identity() {
let mut store = populated_custom_tab_store();
Expand Down
35 changes: 35 additions & 0 deletions src-tauri/src/commands/media/counter_animation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,41 @@ mod tests {
);
}

// 프리셋 편집은 백엔드가 모든 모드의 바인딩된 요소를 갱신한다.
// 프론트가 index로 한 번 더 얹지 않는 근거라 값까지 고정한다
#[test]
fn preset_update_rewrites_every_mode_and_leaves_unbound_alone() {
const OTHER_MODE: &str = "8key";
let preset = target_preset();
let mut store = counter_store(true, true, true);
store
.keys
.insert(OTHER_MODE.to_string(), vec!["KeyB".into()]);
store.key_positions.insert(
OTHER_MODE.to_string(),
vec![position(true), position(false)],
);

let affected = apply_preset_to_bound_counters(&mut store, TARGET_PRESET_ID, &preset);

assert_eq!(affected, 4);

let other = &store.key_positions[OTHER_MODE];
let bound = &other[0].counter.animation;
assert_eq!(bound.preset_id.as_deref(), Some(TARGET_PRESET_ID));
assert_eq!(bound.bezier, preset.bezier);
assert_eq!(bound.scale, preset.scale);
assert_eq!(bound.duration_ms, preset.duration_ms);

// 바인딩되지 않은 요소는 그대로 둔다
let untouched = &other[1].counter.animation;
let default_animation = KeyPosition::default().counter.animation;
assert_eq!(untouched.preset_id, default_animation.preset_id);
assert_eq!(untouched.bezier, default_animation.bezier);
assert_eq!(untouched.scale, default_animation.scale);
assert_eq!(untouched.duration_ms, default_animation.duration_ms);
}

#[test]
fn preset_update_reports_only_actually_changed_collections() {
let mut store = counter_store(false, true, false);
Expand Down
Loading