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
68 lines
2.7 KiB
TypeScript
68 lines
2.7 KiB
TypeScript
import type { FlatKey } from "../../domain/phy";
|
|
import { insertNextTo, swapKeys, type OrderContext } from "../../domain/idxOrder";
|
|
|
|
type Point = { x: number; y: number };
|
|
export type DropAction =
|
|
{ kind: "insert"; target: string; after: boolean } | { kind: "swap"; target: string };
|
|
|
|
/** World point in the key's own frame (origin at its top-left, x along its width). */
|
|
export function toKeyLocal(k: FlatKey, p: Point): Point {
|
|
const a = (-k.rotation * Math.PI) / 180,
|
|
dx = p.x - k.x,
|
|
dy = p.y - k.y;
|
|
return { x: dx * Math.cos(a) - dy * Math.sin(a), y: dx * Math.sin(a) + dy * Math.cos(a) };
|
|
}
|
|
|
|
/** World center of a key. */
|
|
export function keyCenter(k: FlatKey): Point {
|
|
const a = (k.rotation * Math.PI) / 180,
|
|
hw = k.key.size.w / 2,
|
|
hh = k.key.size.h / 2;
|
|
return { x: k.x + hw * Math.cos(a) - hh * Math.sin(a), y: k.y + hw * Math.sin(a) + hh * Math.cos(a) };
|
|
}
|
|
|
|
/** Topmost key under a world point, with the point's position across the key width (0..1). */
|
|
export function hitKey(keys: readonly FlatKey[], p: Point): { id: string; fraction: number } | undefined {
|
|
for (let i = keys.length - 1; i >= 0; i--) {
|
|
const k = keys[i]!,
|
|
local = toKeyLocal(k, p);
|
|
if (local.x >= 0 && local.y >= 0 && local.x <= k.key.size.w && local.y <= k.key.size.h)
|
|
return { id: k.key.id, fraction: local.x / k.key.size.w };
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
/**
|
|
* Drop result of `source` at a hit: the outer quarters insert before or after the key, the middle
|
|
* swaps. Unregistered keys cannot swap, so their drop splits the key in halves. Unregistered targets
|
|
* and the source itself take no drop. T-31.
|
|
*/
|
|
export function dropAction(
|
|
source: string,
|
|
hit: { id: string; fraction: number } | undefined,
|
|
registered: ReadonlySet<string>,
|
|
): DropAction | undefined {
|
|
if (!hit || hit.id === source || !registered.has(hit.id)) return undefined;
|
|
if (!registered.has(source)) return { kind: "insert", target: hit.id, after: hit.fraction >= 0.5 };
|
|
if (hit.fraction < 0.25) return { kind: "insert", target: hit.id, after: false };
|
|
if (hit.fraction > 0.75) return { kind: "insert", target: hit.id, after: true };
|
|
return { kind: "swap", target: hit.id };
|
|
}
|
|
|
|
/** New order after dropping `source`. */
|
|
export function applyDrop(
|
|
order: readonly string[],
|
|
ctx: OrderContext,
|
|
source: string,
|
|
action: DropAction,
|
|
): string[] {
|
|
return action.kind === "swap"
|
|
? swapKeys(order, source, action.target)
|
|
: insertNextTo(order, ctx, source, action.target, action.after);
|
|
}
|
|
|
|
/** Output number of each shown key: its position after dropping keys outside the configuration. */
|
|
export function outputNumbers(order: readonly string[], visible: ReadonlySet<string>): Map<string, number> {
|
|
return new Map(order.filter((id) => visible.has(id)).map((id, i) => [id, i]));
|
|
}
|