From b383fcf8445d437ffbafca28e3f0e9346532ac70 Mon Sep 17 00:00:00 2001 From: EwdErna Date: Fri, 23 Oct 2026 21:07:04 +0900 Subject: [PATCH] =?UTF-8?q?mock:=20=E9=95=B7=E6=8A=BC=E3=81=97=E3=81=AB?= =?UTF-8?q?=E3=83=AB=E3=83=BC=E3=83=97=E6=96=B9=E5=BC=8F=E3=81=A8=E3=83=AA?= =?UTF-8?q?=E3=82=B9=E3=83=88=E6=96=B9=E5=BC=8F=E3=82=92=E5=85=A5=E3=82=8C?= =?UTF-8?q?=E3=80=81=E3=83=84=E3=83=BC=E3=83=AB=E3=83=90=E3=83=BC=E3=81=A7?= =?UTF-8?q?=E5=88=87=E3=82=8A=E6=9B=BF=E3=81=88=E3=82=8B(T-30=20=E5=8D=98?= =?UTF-8?q?=E4=BD=8D7b)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit どちらも押した位置に円形インジケータを、少し離して選択先のリストを出す。 間隔は600msで始まり1000msごとに進む(500ms/450msは早いという所感への対応)。 リスト方式は、ポインタがリストに入ると巡回を止め、離したときに指している行で確定する。 リストへ向かう動きをドラッグと取り違えないよう、リスト方式では長押し後のドラッグをしない。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01ShoEXYqi8PWTjB9E86Ltot --- mock/e2e/phy.spec.ts | 44 ++++++++++- mock/src/ui/phy/PhyEditor.vue | 45 +++++------ mock/src/ui/phy/PhyHoldOverlay.vue | 103 +++++++++++++++++++++++++ mock/src/ui/phy/PhyToolbar.vue | 9 +++ mock/src/ui/phy/usePhyCanvasPointer.ts | 23 ++++-- mock/src/ui/phy/usePhyLongPress.ts | 81 ++++++++++++++----- provisional/decisions.md | 3 +- 7 files changed, 258 insertions(+), 50 deletions(-) create mode 100644 mock/src/ui/phy/PhyHoldOverlay.vue diff --git a/mock/e2e/phy.spec.ts b/mock/e2e/phy.spec.ts index 018bba2..51803a8 100644 --- a/mock/e2e/phy.spec.ts +++ b/mock/e2e/phy.spec.ts @@ -201,7 +201,7 @@ test("長押しでクラスターを選び、ダブルクリックでフォー const b = await key.boundingBox(); await page.mouse.move(b!.x + b!.width / 2, b!.y + b!.height / 2); await page.mouse.down(); - await expect(page.locator(".long-label")).toContainText("Cluster", { timeout: 2000 }); + await expect(page.locator(".hold-row.active")).toContainText("Cluster", { timeout: 2000 }); await page.mouse.up(); await expect(page.locator(`[data-tree-id="${clusterId}"]`).first()).toHaveClass(/chosen/); await page.locator(`[data-tree-id="${clusterId}"]`).first().click(); @@ -588,7 +588,7 @@ test("長押し候補は離すまで選択を変えず、離すとクラスタ const b = await cluster.locator('[data-node-type="key"]').first().boundingBox(); await page.mouse.move(b!.x + b!.width / 2, b!.y + b!.height / 2); await page.mouse.down(); - await expect(page.locator(".long-label")).toContainText("2/", { timeout: 1800 }); + await expect(page.locator(".hold-row.active")).toContainText("Cluster", { timeout: 1800 }); await expect( page.locator(`[data-sel-id="${await keys.nth(4).getAttribute("data-node-id")}"]`), ).toBeVisible(); @@ -597,6 +597,46 @@ test("長押し候補は離すまで選択を変えず、離すとクラスタ await expect(page.locator(`[data-tree-id="${clusterId}"]`).first()).toHaveClass(/chosen/); }); +test("リスト方式では、リストに入ると巡回が止まり、指した行で離すと確定する", async ({ page }) => { + const cluster = await makeCluster(page); + await page.getByRole("button", { name: "長押し: ループ" }).click(); + await expect(page.getByRole("button", { name: "長押し: リスト" })).toBeVisible(); + const key = cluster.locator('[data-node-type="key"]').first(); + const keyId = await key.getAttribute("data-node-id"); + const b = await key.boundingBox(); + await page.mouse.move(b!.x + b!.width / 2, b!.y + b!.height / 2); + await page.mouse.down(); + await expect(page.locator(".hold-row.active")).toContainText("Cluster", { timeout: 2000 }); + const row = await page.locator(".hold-row", { hasText: "キー" }).boundingBox(); + await page.mouse.move(row!.x + row!.width / 2, row!.y + row!.height / 2, { steps: 5 }); + await expect(page.locator(".hold-row.active")).toHaveText("キー"); + await expect(page.locator(".hold-ring")).toHaveCount(0); + await page.waitForTimeout(1200); + await expect(page.locator(".hold-row.active")).toHaveText("キー"); + await page.mouse.up(); + await expect(page.locator(".hold-list")).toHaveCount(0); + await expect(page.locator(`[data-sel-id="${keyId}"]`)).toBeVisible(); + // リストへ動かしてもキーはドラッグされない + expect(await key.boundingBox()).toEqual(b); +}); + +test("ループ方式では、長押しのあと動かすと選んだ先をドラッグする", async ({ page }) => { + const cluster = await makeCluster(page); + const clusterId = await cluster.getAttribute("data-node-id"); + const keys = cluster.locator('[data-node-type="key"]'); + const before = await keys.nth(1).boundingBox(); + const b = (await keys.first().boundingBox())!; + await page.mouse.move(b.x + b.width / 2, b.y + b.height / 2); + await page.mouse.down(); + await expect(page.locator(".hold-ring")).toBeVisible({ timeout: 2000 }); + await page.mouse.move(b.x + b.width / 2 + 80, b.y + b.height / 2, { steps: 5 }); + await expect(page.locator(".hold-list")).toHaveCount(0); + await page.mouse.up(); + await expect(page.locator(`[data-tree-id="${clusterId}"]`).first()).toHaveClass(/chosen/); + // クラスターごと動くので、押していないキーも動く + expect((await keys.nth(1).boundingBox())!.x).toBeGreaterThan(before!.x + 40); +}); + test("「その他」の部品はpartGroupを設定せず、選択後は追加メニューを戻す", async ({ page }) => { const select = page.getByRole("combobox", { name: "部品を足す" }); await select.selectOption("その他"); diff --git a/mock/src/ui/phy/PhyEditor.vue b/mock/src/ui/phy/PhyEditor.vue index 546b7f0..ce778ba 100644 --- a/mock/src/ui/phy/PhyEditor.vue +++ b/mock/src/ui/phy/PhyEditor.vue @@ -11,10 +11,12 @@ import PhySelectionLayer from "./PhySelectionLayer.vue"; import PhyBadgeLayer from "./PhyBadgeLayer.vue"; import PhyToolbar from "./PhyToolbar.vue"; import PhyProperties from "./PhyProperties.vue"; +import PhyHoldOverlay from "./PhyHoldOverlay.vue"; import { normalizeSelection, nextGrid, insertionContainer, structuralNodes, visibleNodeIds } from "./logic"; import { usePhyClipboard } from "./usePhyClipboard"; import { usePhyViewport } from "./usePhyViewport"; import { usePhyCanvasPointer } from "./usePhyCanvasPointer"; +import { HOLD_DELAY, HOLD_INTERVAL, type HoldMode } from "./usePhyLongPress"; import { usePhyKeyboard } from "./usePhyKeyboard"; import { usePhyCommands } from "./usePhyCommands"; import { usePhyCanvasView } from "./usePhyCanvasView"; @@ -217,6 +219,7 @@ function visibleIds(): Set { if (!focus.value) return new Set(allNodes().map((n) => n.id)); return visibleNodeIds(node(focus.value), variants.value); } +const holdMode = ref("loop"); const rangeBox = ref<{ x: number; y: number; width: number; height: number } | null>(null); const pointer = usePhyCanvasPointer({ project: () => props.project, @@ -236,12 +239,13 @@ const pointer = usePhyCanvasPointer({ isAncestor, parentLabel, pressTargets, + holdMode, setSelection: setSel, beginHistory: props.beginHistory, commitHistory: props.commitHistory, dispatch, }); -const { canvasDown, down, spaceUp, longLabel, longLabelPos, spaceHeld, holdCandidates, holdIndex } = pointer; +const { canvasDown, down, spaceUp, hold, spaceHeld } = pointer; const keyHandler = usePhyKeyboard({ project: () => props.project, mismatch: () => props.mismatch, @@ -328,14 +332,18 @@ function onFocus(_id: string) { @tree="treeOpen = !treeOpen" @help="showHelp = true" @focus="focus = $event" + :hold-mode="holdMode" + @hold-mode="holdMode = holdMode === 'loop' ? 'list' : 'loop'" + /> + -
- {{ longLabel }} -
unitSize が一致しないため編集できません。

Ctrl+Z/Y/S Undo・Redo・保存 · ? 一覧

-

長押し: キー上で500ms待つと選択先候補が450msごとに進み、離すと確定します。

+

+ 長押し: キー上で{{ HOLD_DELAY }}ms待つと選択先候補が{{ + HOLD_INTERVAL + }}msごとに進み、離すと確定します。 + ループ方式では、そのまま動かすと選んだ先をドラッグします。リスト方式では、リストに入ると巡回が止まり、指している行で離すと確定します。 +

@@ -595,16 +608,4 @@ function onFocus(_id: string) { font-size: 11px; color: #6b520f; } -.long-label { - position: fixed; - z-index: 3; - left: 0; - top: 0; - transform: translate(10px, 10px); - pointer-events: none; - background: #fff3d8; - border: 1px solid #c87d13; - padding: 3px 7px; - border-radius: 4px; -} diff --git a/mock/src/ui/phy/PhyHoldOverlay.vue b/mock/src/ui/phy/PhyHoldOverlay.vue new file mode 100644 index 0000000..ca4b7b4 --- /dev/null +++ b/mock/src/ui/phy/PhyHoldOverlay.vue @@ -0,0 +1,103 @@ + + + + + diff --git a/mock/src/ui/phy/PhyToolbar.vue b/mock/src/ui/phy/PhyToolbar.vue index 8ba24e6..09754aa 100644 --- a/mock/src/ui/phy/PhyToolbar.vue +++ b/mock/src/ui/phy/PhyToolbar.vue @@ -6,6 +6,7 @@ defineProps<{ moveTargets: { id: string; label: string }[]; focus: string | null; crumbs: { id: string | null; label: string }[]; + holdMode: "loop" | "list"; }>(); const emit = defineEmits<{ keyAdd: []; @@ -23,6 +24,7 @@ const emit = defineEmits<{ tree: []; help: []; focus: [id: string | null]; + holdMode: []; }>(); function addPart(e: Event) { const select = e.target as HTMLSelectElement; @@ -94,6 +96,13 @@ function addPart(e: Event) { +