mock: IDX Editorを作り直す(T-31)
provisional/idx-editor-feedback.md の合意事項と T-31 の仮決定に沿って、IdxPanel を捨てて ui/idx/ に一から書き直した。 並びのリストは置かず、左にバリアントのツリー、中央のキャンバスでドラッグして並べ替え、右にプリセット・取り込み・未登録の登録を置いた。 - order の操作(隠れたキーのくっつき先、挿入、入れ替え、連番の並べ直し)を domain/idxOrder.ts に、 走査・半身の分割・行の割り当てのずらし・マトリクス順を domain/idxScan.ts に、純粋関数として置いた。行と列の判定は rows.ts をそのまま使う - QMK keyboard.json・VIA定義・KLE JSON の取り込みは、位置と回転で表示中のキーに照合する(io/layoutImport.ts、domain/idxImport.ts) - 回転したキーでも出力番号を正立させる - Codex の設定ファイルが壊れていて起動できなかったため、実装は Claude が直接行った - 検証: vue-tsc 0件、Vitest 101件(うち新規17件)、Playwright 48件(IDX 8件を書き直し)、prettier --check Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GzX13A7HseQz9n6etGbFDg
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { ref, type Ref } from "vue";
|
||||
import type { FlatKey } from "../../domain/phy";
|
||||
import { dropAction, hitKey, keyCenter, type DropAction } from "./logic";
|
||||
|
||||
type Point = { x: number; y: number };
|
||||
type Options = {
|
||||
svg: Ref<SVGSVGElement | undefined>;
|
||||
zoom: Ref<number>;
|
||||
pan: Ref<Point>;
|
||||
selected: Ref<string[]>;
|
||||
keys: () => FlatKey[];
|
||||
registered: () => ReadonlySet<string>;
|
||||
previewing: () => boolean;
|
||||
onDrop: (source: string, action: DropAction) => void;
|
||||
onPreviewPick: (id: string) => void;
|
||||
};
|
||||
/** Pointer travel (px) before a press on a key becomes a drag. */
|
||||
const DRAG_START = 4;
|
||||
|
||||
/** Canvas pointer handling: click/Shift+click selection, dragging a key to reorder, range selection, pan. T-31. */
|
||||
export function useIdxPointer(o: Options) {
|
||||
const drag = ref<{ id: string; point: Point; action?: DropAction } | null>(null);
|
||||
const band = ref<{ start: Point; end: Point } | null>(null);
|
||||
const spaceHeld = ref(false);
|
||||
let press: { id?: string; client: Point; add: boolean; moved: boolean; pan?: Point } | null = null;
|
||||
|
||||
function world(event: PointerEvent): Point {
|
||||
const svg = o.svg.value!;
|
||||
const local = new DOMPoint(event.clientX, event.clientY).matrixTransform(svg.getScreenCTM()!.inverse());
|
||||
return { x: (local.x - o.pan.value.x) / o.zoom.value, y: (local.y - o.pan.value.y) / o.zoom.value };
|
||||
}
|
||||
|
||||
function begin(event: PointerEvent, id?: string) {
|
||||
const add = event.shiftKey || event.ctrlKey || event.metaKey;
|
||||
const panning = event.button === 1 || spaceHeld.value;
|
||||
if (event.button !== 0 && !panning) return;
|
||||
event.preventDefault();
|
||||
if (!panning && id && o.previewing()) {
|
||||
o.onPreviewPick(id);
|
||||
return;
|
||||
}
|
||||
o.svg.value?.setPointerCapture?.(event.pointerId);
|
||||
press = {
|
||||
id: panning ? undefined : id,
|
||||
client: { x: event.clientX, y: event.clientY },
|
||||
add,
|
||||
moved: false,
|
||||
};
|
||||
if (panning) press.pan = { ...o.pan.value };
|
||||
else if (!id && !o.previewing()) band.value = { start: world(event), end: world(event) };
|
||||
}
|
||||
|
||||
function move(event: PointerEvent) {
|
||||
if (!press) return;
|
||||
const dx = event.clientX - press.client.x,
|
||||
dy = event.clientY - press.client.y;
|
||||
if (!press.moved && Math.hypot(dx, dy) < DRAG_START) return;
|
||||
press.moved = true;
|
||||
if (press.pan) {
|
||||
// viewBox matches the SVG's pixel size, so client deltas are viewBox deltas
|
||||
o.pan.value = { x: press.pan.x + dx, y: press.pan.y + dy };
|
||||
return;
|
||||
}
|
||||
const point = world(event);
|
||||
if (press.id) {
|
||||
const action = dropAction(press.id, hitKey(o.keys(), point), o.registered());
|
||||
drag.value = { id: press.id, point, action };
|
||||
} else if (band.value) band.value = { ...band.value, end: point };
|
||||
}
|
||||
|
||||
function end() {
|
||||
if (!press) return;
|
||||
const { id, add, moved } = press;
|
||||
if (id && !moved) {
|
||||
o.selected.value = add
|
||||
? o.selected.value.includes(id)
|
||||
? o.selected.value.filter((x) => x !== id)
|
||||
: [...o.selected.value, id]
|
||||
: [id];
|
||||
} else if (id && drag.value?.action) o.onDrop(id, drag.value.action);
|
||||
else if (!id && !press.pan) {
|
||||
const b = band.value;
|
||||
if (!moved || !b) {
|
||||
if (!add) o.selected.value = [];
|
||||
} else {
|
||||
const [x0, x1] = [Math.min(b.start.x, b.end.x), Math.max(b.start.x, b.end.x)];
|
||||
const [y0, y1] = [Math.min(b.start.y, b.end.y), Math.max(b.start.y, b.end.y)];
|
||||
const inside = o
|
||||
.keys()
|
||||
.filter((k) => {
|
||||
const c = keyCenter(k);
|
||||
return c.x >= x0 && c.x <= x1 && c.y >= y0 && c.y <= y1;
|
||||
})
|
||||
.map((k) => k.key.id);
|
||||
o.selected.value = add ? [...new Set([...o.selected.value, ...inside])] : inside;
|
||||
}
|
||||
}
|
||||
cancel();
|
||||
}
|
||||
|
||||
function cancel() {
|
||||
press = null;
|
||||
drag.value = null;
|
||||
band.value = null;
|
||||
}
|
||||
|
||||
return { drag, band, spaceHeld, begin, move, end, cancel };
|
||||
}
|
||||
Reference in New Issue
Block a user