provisional/decisions.md の仮決定(T-1〜T-28)に従い、mock/ に Vue 3 + Vite + TypeScript のモックを置いた。 T-3 の1〜3(回転したクラスター内のドラッグと保存の往復、IDX・MAPとの連携、ELCと出力の対応表)まで。疑似デバイスとの往復(T-24)は未着手。 - KLE JSONの入出力は、KLE本家の serial.js の deserialize の結果を期待値とする fixture(tests/fixtures/kle-oracle.json)で照合した。本家のコードは取り込んでいない(T-7) - 基本キーコードの表は QMK の data/constants/keycodes/keycodes_0.0.1_basic.hjson(1e340cd)から抽出した - 実装は Codex(Luna)、レビューは Codex(Sol)。検証: Vitest 53件、Playwright 20件、vue-tsc、vite build - TypeScript は vue-tsc が 7 系に未対応のため 6 系に固定した - 操作性の改善(ELCの提供者の表示、一括配線とGeneratorのピン設定の引き継ぎ、キー内の文字のはみ出し)は別途行う Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AATqjRyp6rrnwTNdYw8XBT
136 lines
3.5 KiB
Vue
136 lines
3.5 KiB
Vue
<script setup lang="ts">
|
|
// 選択構成の平坦な配置を読み取り専用で描く。IDX・ELC・MAP・Generatorのパネルで共有する。
|
|
import { computed } from "vue";
|
|
import { flattenKeys } from "../../domain/geometry";
|
|
import type { FlatKey, PhyFile } from "../../domain/phy";
|
|
|
|
const props = withDefaults(
|
|
defineProps<{
|
|
phy: PhyFile;
|
|
selection: Record<string, string>;
|
|
/** キーの中央に出す文字列。省略時は先頭のラベル */
|
|
keyText?: (keyId: string) => string;
|
|
/** キーに付けるCSSクラス(状態の色分け) */
|
|
keyClass?: (keyId: string) => string | undefined;
|
|
selectedIds?: string[];
|
|
unit?: number;
|
|
}>(),
|
|
{ selectedIds: () => [], unit: 48 },
|
|
);
|
|
const emit = defineEmits<{ keyClick: [keyId: string, event: MouseEvent] }>();
|
|
|
|
const keys = computed<FlatKey[]>(() => flattenKeys(props.phy, props.selection));
|
|
|
|
// 回転を含めた四隅から表示範囲を求める
|
|
const viewBox = computed(() => {
|
|
let minX = Infinity,
|
|
minY = Infinity,
|
|
maxX = -Infinity,
|
|
maxY = -Infinity;
|
|
for (const k of keys.value) {
|
|
const r = (k.rotation * Math.PI) / 180,
|
|
c = Math.cos(r),
|
|
s = Math.sin(r);
|
|
const { w, h } = k.key.size;
|
|
for (const [lx, ly] of [
|
|
[0, 0],
|
|
[w, 0],
|
|
[0, h],
|
|
[w, h],
|
|
] as const) {
|
|
const x = k.x + lx * c - ly * s,
|
|
y = k.y + lx * s + ly * c;
|
|
minX = Math.min(minX, x);
|
|
minY = Math.min(minY, y);
|
|
maxX = Math.max(maxX, x);
|
|
maxY = Math.max(maxY, y);
|
|
}
|
|
}
|
|
if (!isFinite(minX)) return "0 0 4 2";
|
|
const pad = 0.25;
|
|
return `${minX - pad} ${minY - pad} ${maxX - minX + pad * 2} ${maxY - minY + pad * 2}`;
|
|
});
|
|
const width = computed(() => {
|
|
const parts = viewBox.value.split(" ").map(Number);
|
|
return (parts[2] ?? 4) * props.unit;
|
|
});
|
|
|
|
function text(k: FlatKey): string {
|
|
return props.keyText ? props.keyText(k.key.id) : (k.key.labels?.find((l) => l) ?? "");
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<svg class="keyboard-view" :viewBox="viewBox" :width="width" data-testid="keyboard-view">
|
|
<g
|
|
v-for="k in keys"
|
|
:key="k.key.id"
|
|
:transform="`translate(${k.x},${k.y}) rotate(${k.rotation})`"
|
|
:class="[
|
|
'kv-key',
|
|
keyClass?.(k.key.id),
|
|
{ selected: selectedIds.includes(k.key.id), decal: k.key.decal, ghost: k.key.ghost },
|
|
]"
|
|
:data-key-id="k.key.id"
|
|
@click="emit('keyClick', k.key.id, $event)"
|
|
>
|
|
<rect x="0.03" y="0.03" :width="k.key.size.w - 0.06" :height="k.key.size.h - 0.06" rx="0.08" />
|
|
<rect
|
|
v-if="k.key.size.w2 !== undefined || k.key.size.h2 !== undefined"
|
|
:x="(k.key.size.x2 ?? 0) + 0.03"
|
|
:y="(k.key.size.y2 ?? 0) + 0.03"
|
|
:width="(k.key.size.w2 ?? k.key.size.w) - 0.06"
|
|
:height="(k.key.size.h2 ?? k.key.size.h) - 0.06"
|
|
rx="0.08"
|
|
/>
|
|
<text :x="k.key.size.w / 2" :y="k.key.size.h / 2" text-anchor="middle" dominant-baseline="central">
|
|
{{ text(k) }}
|
|
</text>
|
|
</g>
|
|
</svg>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.keyboard-view {
|
|
max-width: 100%;
|
|
height: auto;
|
|
user-select: none;
|
|
}
|
|
.kv-key {
|
|
cursor: pointer;
|
|
}
|
|
.kv-key rect {
|
|
fill: #f3f4f6;
|
|
stroke: #6b7280;
|
|
stroke-width: 0.02;
|
|
}
|
|
.kv-key text {
|
|
font-size: 0.22px;
|
|
fill: #111827;
|
|
pointer-events: none;
|
|
}
|
|
.kv-key.selected rect {
|
|
stroke: #2563eb;
|
|
stroke-width: 0.06;
|
|
}
|
|
.kv-key.decal rect {
|
|
stroke-dasharray: 0.06 0.04;
|
|
fill: none;
|
|
}
|
|
.kv-key.ghost {
|
|
opacity: 0.45;
|
|
}
|
|
.kv-key.warn rect {
|
|
fill: #fde68a;
|
|
}
|
|
.kv-key.error rect {
|
|
fill: #fecaca;
|
|
}
|
|
.kv-key.ok rect {
|
|
fill: #d1fae5;
|
|
}
|
|
.kv-key.muted rect {
|
|
fill: #e5e7eb;
|
|
}
|
|
</style>
|