mock: 長押しの処理を usePhyLongPress に切り出す(T-30 単位7a)
候補・巡回のタイマー・表示ラベルを usePhyLongPress に移し、 usePhyCanvasPointer はドラッグ・クリックとの接続だけを持つ。挙動は変えない。 7b で2方式を入れる土台にする。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BKxNtqjjPyttUfT1TgC7Qt
This commit is contained in:
@@ -4,6 +4,7 @@ import type { KeyNode, Node } from "../../domain/phy";
|
|||||||
import { apply, worldTransform } from "../../domain/geometry";
|
import { apply, worldTransform } from "../../domain/geometry";
|
||||||
import { moveNode } from "../../domain/phyCommands";
|
import { moveNode } from "../../domain/phyCommands";
|
||||||
import { normalizeSelection, snap } from "./logic";
|
import { normalizeSelection, snap } from "./logic";
|
||||||
|
import { usePhyLongPress } from "./usePhyLongPress";
|
||||||
import { autoPanVelocity } from "./viewport";
|
import { autoPanVelocity } from "./viewport";
|
||||||
|
|
||||||
type Point = { x: number; y: number };
|
type Point = { x: number; y: number };
|
||||||
@@ -33,18 +34,11 @@ type Options = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function usePhyCanvasPointer(options: Options) {
|
export function usePhyCanvasPointer(options: Options) {
|
||||||
const longLabel = ref("");
|
const longPress = usePhyLongPress(options);
|
||||||
const longLabelPos = ref({ x: 0, y: 0 });
|
|
||||||
const spaceHeld = ref(false);
|
const spaceHeld = ref(false);
|
||||||
const holdCandidates = ref<string[]>([]);
|
|
||||||
const holdIndex = ref(0);
|
|
||||||
let preClickSelection: string[] = [];
|
let preClickSelection: string[] = [];
|
||||||
let preClickTarget: string | null = null;
|
let preClickTarget: string | null = null;
|
||||||
let preClickAt = 0;
|
let preClickAt = 0;
|
||||||
let holdTimer: number | undefined;
|
|
||||||
let holdCycle: number | undefined;
|
|
||||||
let holdShift = false;
|
|
||||||
let holdActivated = false;
|
|
||||||
let lastPointer: Point | null = null;
|
let lastPointer: Point | null = null;
|
||||||
let autoPanFrame: number | undefined;
|
let autoPanFrame: number | undefined;
|
||||||
let lastAutoPanAt = 0;
|
let lastAutoPanAt = 0;
|
||||||
@@ -90,7 +84,7 @@ export function usePhyCanvasPointer(options: Options) {
|
|||||||
autoPanFrame = undefined;
|
autoPanFrame = undefined;
|
||||||
const svg = options.svg.value;
|
const svg = options.svg.value;
|
||||||
const pointer = lastPointer;
|
const pointer = lastPointer;
|
||||||
if (!svg || !pointer || !dragState?.moved || longLabel.value) return;
|
if (!svg || !pointer || !dragState?.moved || longPress.cycling()) return;
|
||||||
const rect = svg.getBoundingClientRect();
|
const rect = svg.getBoundingClientRect();
|
||||||
const velocity = autoPanVelocity(
|
const velocity = autoPanVelocity(
|
||||||
{ x: pointer.x, y: pointer.y },
|
{ x: pointer.x, y: pointer.y },
|
||||||
@@ -117,7 +111,7 @@ export function usePhyCanvasPointer(options: Options) {
|
|||||||
|
|
||||||
function scheduleAutoPan() {
|
function scheduleAutoPan() {
|
||||||
const svg = options.svg.value;
|
const svg = options.svg.value;
|
||||||
if (!svg || !lastPointer || !dragState?.moved || longLabel.value) {
|
if (!svg || !lastPointer || !dragState?.moved || longPress.cycling()) {
|
||||||
stopAutoPan();
|
stopAutoPan();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -210,7 +204,7 @@ export function usePhyCanvasPointer(options: Options) {
|
|||||||
}
|
}
|
||||||
preClickTarget = node.id;
|
preClickTarget = node.id;
|
||||||
preClickAt = Date.now();
|
preClickAt = Date.now();
|
||||||
holdActivated = false;
|
longPress.reset();
|
||||||
dragState = {
|
dragState = {
|
||||||
id: node.id,
|
id: node.id,
|
||||||
start: { x: event.clientX, y: event.clientY },
|
start: { x: event.clientX, y: event.clientY },
|
||||||
@@ -223,24 +217,7 @@ export function usePhyCanvasPointer(options: Options) {
|
|||||||
};
|
};
|
||||||
window.addEventListener("pointermove", move, true);
|
window.addEventListener("pointermove", move, true);
|
||||||
window.addEventListener("pointerup", up, true);
|
window.addEventListener("pointerup", up, true);
|
||||||
if (node.type === "key") {
|
if (node.type === "key") longPress.start(event, node.id);
|
||||||
holdCandidates.value = options.pressTargets(node.id);
|
|
||||||
holdIndex.value = 0;
|
|
||||||
holdShift = event.shiftKey;
|
|
||||||
holdTimer = window.setTimeout(() => {
|
|
||||||
if (holdCandidates.value.length <= 1) return;
|
|
||||||
holdActivated = true;
|
|
||||||
holdIndex.value = 1;
|
|
||||||
longLabelPos.value = { x: event.clientX, y: event.clientY };
|
|
||||||
longLabel.value = `${options.parentLabel(holdCandidates.value[1]!)} 2/${holdCandidates.value.length}`;
|
|
||||||
holdCycle = window.setInterval(() => {
|
|
||||||
holdIndex.value = (holdIndex.value + 1) % holdCandidates.value.length;
|
|
||||||
const id = holdCandidates.value[holdIndex.value]!;
|
|
||||||
const label = holdIndex.value === 0 ? "キー" : options.parentLabel(id);
|
|
||||||
longLabel.value = `${label} ${holdIndex.value + 1}/${holdCandidates.value.length}`;
|
|
||||||
}, 450);
|
|
||||||
}, 500);
|
|
||||||
}
|
|
||||||
(event.currentTarget as Element).setPointerCapture?.(event.pointerId);
|
(event.currentTarget as Element).setPointerCapture?.(event.pointerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,15 +240,12 @@ export function usePhyCanvasPointer(options: Options) {
|
|||||||
lastPointer = { x: event.clientX, y: event.clientY };
|
lastPointer = { x: event.clientX, y: event.clientY };
|
||||||
const state = dragState;
|
const state = dragState;
|
||||||
if (Math.hypot(event.clientX - state.start.x, event.clientY - state.start.y) < 4) return;
|
if (Math.hypot(event.clientX - state.start.x, event.clientY - state.start.y) < 4) return;
|
||||||
if (holdTimer && !longLabel.value) {
|
longPress.cancelPending();
|
||||||
window.clearTimeout(holdTimer);
|
if (longPress.cycling()) {
|
||||||
holdTimer = undefined;
|
const { id, shift } = longPress.take();
|
||||||
}
|
|
||||||
if (longLabel.value) {
|
|
||||||
const id = holdCandidates.value[holdIndex.value]!;
|
|
||||||
options.selected.value = normalizeSelection(
|
options.selected.value = normalizeSelection(
|
||||||
options.project().phy,
|
options.project().phy,
|
||||||
holdShift ? [...preClickSelection, id] : [id],
|
shift ? [...preClickSelection, id] : [id],
|
||||||
);
|
);
|
||||||
state.ids = [id];
|
state.ids = [id];
|
||||||
state.base = options.project();
|
state.base = options.project();
|
||||||
@@ -280,8 +254,6 @@ export function usePhyCanvasPointer(options: Options) {
|
|||||||
[id, options.localPoint(state.parents.get(id)!, state.start.x, state.start.y)],
|
[id, options.localPoint(state.parents.get(id)!, state.start.x, state.start.y)],
|
||||||
]);
|
]);
|
||||||
options.beginHistory();
|
options.beginHistory();
|
||||||
if (holdCycle) window.clearInterval(holdCycle);
|
|
||||||
longLabel.value = "";
|
|
||||||
} else if (!state.moved) {
|
} else if (!state.moved) {
|
||||||
const includesAncestor = preClickSelection.some(
|
const includesAncestor = preClickSelection.some(
|
||||||
(id) => id === state.id || options.isAncestor(id, state.id),
|
(id) => id === state.id || options.isAncestor(id, state.id),
|
||||||
@@ -366,19 +338,16 @@ export function usePhyCanvasPointer(options: Options) {
|
|||||||
options.rangeBox.value = null;
|
options.rangeBox.value = null;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (holdTimer) window.clearTimeout(holdTimer);
|
const pick = longPress.finish();
|
||||||
if (holdCycle) window.clearInterval(holdCycle);
|
if (pick) {
|
||||||
if (longLabel.value && holdCandidates.value[holdIndex.value]) {
|
|
||||||
const id = holdCandidates.value[holdIndex.value]!;
|
|
||||||
options.selected.value = normalizeSelection(
|
options.selected.value = normalizeSelection(
|
||||||
options.project().phy,
|
options.project().phy,
|
||||||
holdShift ? [...options.selected.value, id] : [id],
|
pick.shift ? [...options.selected.value, pick.id] : [pick.id],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
longLabel.value = "";
|
|
||||||
const moved = dragState?.moved ?? false;
|
const moved = dragState?.moved ?? false;
|
||||||
if (dragState?.moved) options.commitHistory();
|
if (dragState?.moved) options.commitHistory();
|
||||||
else if (dragState && !holdActivated) {
|
else if (dragState && !longPress.wasActivated()) {
|
||||||
const id = dragState.id;
|
const id = dragState.id;
|
||||||
const next = dragState.shift
|
const next = dragState.shift
|
||||||
? options.selected.value.includes(id)
|
? options.selected.value.includes(id)
|
||||||
@@ -387,7 +356,7 @@ export function usePhyCanvasPointer(options: Options) {
|
|||||||
: [id];
|
: [id];
|
||||||
options.selected.value = normalizeSelection(options.project().phy, next);
|
options.selected.value = normalizeSelection(options.project().phy, next);
|
||||||
}
|
}
|
||||||
if (holdActivated || moved) preClickTarget = null;
|
if (longPress.wasActivated() || moved) preClickTarget = null;
|
||||||
dragState = null;
|
dragState = null;
|
||||||
lastPointer = null;
|
lastPointer = null;
|
||||||
}
|
}
|
||||||
@@ -404,11 +373,11 @@ export function usePhyCanvasPointer(options: Options) {
|
|||||||
move,
|
move,
|
||||||
up,
|
up,
|
||||||
spaceUp,
|
spaceUp,
|
||||||
longLabel,
|
longLabel: longPress.label,
|
||||||
longLabelPos,
|
longLabelPos: longPress.labelPos,
|
||||||
spaceHeld,
|
spaceHeld,
|
||||||
holdCandidates,
|
holdCandidates: longPress.candidates,
|
||||||
holdIndex,
|
holdIndex: longPress.index,
|
||||||
priorSelection,
|
priorSelection,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import { ref } from "vue";
|
||||||
|
|
||||||
|
type Options = {
|
||||||
|
parentLabel: (id: string | null) => string;
|
||||||
|
pressTargets: (id: string) => string[];
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LongPressPick = { id: string; shift: boolean };
|
||||||
|
|
||||||
|
// キーの長押しで、キー → 親 → … の順に選択先を巡回する。
|
||||||
|
// ポインタの扱い(ドラッグ・クリック)は呼び出し側が持ち、ここは候補と表示だけを持つ。
|
||||||
|
export function usePhyLongPress(options: Options) {
|
||||||
|
const label = ref("");
|
||||||
|
const labelPos = ref({ x: 0, y: 0 });
|
||||||
|
const candidates = ref<string[]>([]);
|
||||||
|
const index = ref(0);
|
||||||
|
let timer: number | undefined;
|
||||||
|
let cycle: number | undefined;
|
||||||
|
let shift = false;
|
||||||
|
let activated = false;
|
||||||
|
|
||||||
|
function reset() {
|
||||||
|
activated = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function start(event: PointerEvent, keyId: string) {
|
||||||
|
candidates.value = options.pressTargets(keyId);
|
||||||
|
index.value = 0;
|
||||||
|
shift = event.shiftKey;
|
||||||
|
timer = window.setTimeout(() => {
|
||||||
|
if (candidates.value.length <= 1) return;
|
||||||
|
activated = true;
|
||||||
|
index.value = 1;
|
||||||
|
labelPos.value = { x: event.clientX, y: event.clientY };
|
||||||
|
label.value = `${options.parentLabel(candidates.value[1]!)} 2/${candidates.value.length}`;
|
||||||
|
cycle = window.setInterval(() => {
|
||||||
|
index.value = (index.value + 1) % candidates.value.length;
|
||||||
|
const id = candidates.value[index.value]!;
|
||||||
|
const name = index.value === 0 ? "キー" : options.parentLabel(id);
|
||||||
|
label.value = `${name} ${index.value + 1}/${candidates.value.length}`;
|
||||||
|
}, 450);
|
||||||
|
}, 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
function cycling() {
|
||||||
|
return label.value !== "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function cancelPending() {
|
||||||
|
if (timer && !label.value) {
|
||||||
|
window.clearTimeout(timer);
|
||||||
|
timer = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function take(): LongPressPick {
|
||||||
|
const id = candidates.value[index.value]!;
|
||||||
|
if (cycle) window.clearInterval(cycle);
|
||||||
|
label.value = "";
|
||||||
|
return { id, shift };
|
||||||
|
}
|
||||||
|
|
||||||
|
function finish(): LongPressPick | null {
|
||||||
|
if (timer) window.clearTimeout(timer);
|
||||||
|
if (cycle) window.clearInterval(cycle);
|
||||||
|
const id = label.value ? candidates.value[index.value] : undefined;
|
||||||
|
label.value = "";
|
||||||
|
return id ? { id, shift } : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function wasActivated() {
|
||||||
|
return activated;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
label,
|
||||||
|
labelPos,
|
||||||
|
candidates,
|
||||||
|
index,
|
||||||
|
reset,
|
||||||
|
start,
|
||||||
|
cycling,
|
||||||
|
cancelPending,
|
||||||
|
take,
|
||||||
|
finish,
|
||||||
|
wasActivated,
|
||||||
|
};
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user