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
109 lines
3.8 KiB
TypeScript
109 lines
3.8 KiB
TypeScript
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 };
|
|
}
|