From 5f7571724220e1fb6d1141bb8bf47200d290ae31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E3=81=82=E3=83=BC=E3=82=8B?= Date: Wed, 4 Nov 2026 23:27:24 +0900 Subject: [PATCH] =?UTF-8?q?F2L=20=E3=81=AE=E3=82=B1=E3=83=BC=E3=82=B9?= =?UTF-8?q?=E3=81=AE=E5=9B=B3=E3=82=92=E3=80=81=E5=8F=B3=E6=89=8B=E5=89=8D?= =?UTF-8?q?=E3=81=AE=E4=B8=8A=E3=81=8B=E3=82=89=E8=A6=8B=E3=81=9F=E7=AB=8B?= =?UTF-8?q?=E4=BD=93=E3=81=AB=E3=81=99=E3=82=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 展開図ではペアがどこにあるか分かりにくかったので、上面・前面・右面を等角図で描く。 FR のペア(角とエッジ)だけ色を付け、上段と FR スロットのほかのパーツは灰色にする。 Co-Authored-By: Claude Opus 5.5 --- web/src/cases/f2l-view.test.ts | 13 +++++ web/src/cases/f2l-view.ts | 35 ++++++++++++++ web/src/components/CaseDiagram.tsx | 76 +++++++++++++++++++++++++----- 3 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 web/src/cases/f2l-view.test.ts create mode 100644 web/src/cases/f2l-view.ts diff --git a/web/src/cases/f2l-view.test.ts b/web/src/cases/f2l-view.test.ts new file mode 100644 index 0000000..ee8d1e8 --- /dev/null +++ b/web/src/cases/f2l-view.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from 'vitest' +import { f2lGrayStickers } from './f2l-view' + +describe('f2lGrayStickers', () => { + it('FR のペアだけ色を残し、上段と FR スロットのほかのパーツを灰色にする', () => { + // F2L 1(F' U' F):角は URF、エッジは UL にある + const gray = f2lGrayStickers('RUURUUUUFRBBURRURRFFDFFRFFRDDBDDDDDDFFLLLLLLLLLUBBBBBB') + for (const i of [8, 9, 20, 3, 37]) expect(gray.has(i)).toBe(false) + for (const i of [6, 18, 38, 0, 36, 47, 7, 19, 29, 26, 15, 23, 12]) expect(gray.has(i)).toBe(true) + // 下の2段のほかの場所(センターや FL スロット)は色のまま + for (const i of [4, 22, 13, 21, 24]) expect(gray.has(i)).toBe(false) + }) +}) diff --git a/web/src/cases/f2l-view.ts b/web/src/cases/f2l-view.ts new file mode 100644 index 0000000..7a6605b --- /dev/null +++ b/web/src/cases/f2l-view.ts @@ -0,0 +1,35 @@ +// F2L のケースの図で、どのシールを色付きで見せるか。 +// facelets は 54 文字(U 0-8・R 9-17・F 18-26・D 27-35・L 36-44・B 45-53)、標準の向き(上面=最終層、FR スロット)。 + +/** 上段と FR スロットの、パーツごとのシールの位置。 */ +const PIECES: number[][] = [ + // 上段の角(URF・UFL・ULB・UBR) + [8, 9, 20], + [6, 18, 38], + [0, 36, 47], + [2, 45, 11], + // 上段のエッジ(UR・UF・UL・UB) + [5, 10], + [7, 19], + [3, 37], + [1, 46], + // FR スロット(DFR の角、FR のエッジ) + [29, 26, 15], + [23, 12], +] + +const sameColors = (a: string[], b: string[]) => a.length === b.length && [...a].sort().join() === [...b].sort().join() + +/** + * 灰色にするシールの位置。上段と FR スロットにあるパーツのうち、FR のペア(D・F・R の角と F・R のエッジ)でないものを灰色にする。 + * 下の2段のほかの場所は揃っているので、色のまま残す。 + */ +export function f2lGrayStickers(facelets: string): Set { + const gray = new Set() + for (const piece of PIECES) { + const colors = piece.map((i) => facelets[i]) + const pair = piece.length === 3 ? ['D', 'F', 'R'] : ['F', 'R'] + if (!sameColors(colors, pair)) for (const i of piece) gray.add(i) + } + return gray +} diff --git a/web/src/components/CaseDiagram.tsx b/web/src/components/CaseDiagram.tsx index c08d128..26cf2ff 100644 --- a/web/src/components/CaseDiagram.tsx +++ b/web/src/components/CaseDiagram.tsx @@ -1,5 +1,6 @@ import { useId } from 'react' import type { CaseInfo } from '../api' +import { f2lGrayStickers } from '../cases/f2l-view' import { pllArrows } from '../cases/pll-arrows' // 標準の向き(上面=黄、前面=緑)での表示色。 @@ -17,7 +18,7 @@ const S = 14 // シール1枚の大きさ const G = 2 // すき間 /** - * ケースの図。OLL/PLL は上面(+側面の上段)、F2L は上面+前面+右面。 + * ケースの図。OLL/PLL は上面(+側面の上段)、F2L は右手前の上から見た立体(上面・前面・右面)。 * OLLは最終層の色以外を灰色にし、向きだけが分かるようにする。 * arrows を付けると、PLL ではどのパーツをどこへ動かすかを矢印で重ねる(小さい図では見づらいので、大きい図だけで使う)。 */ @@ -43,18 +44,7 @@ export function CaseDiagram({ const cells: React.ReactNode[] = [] const step = S + G - if (c.set === 'F2L') { - // 上面(U 0-8)、前面(F 18-26)、右面(R 9-17)を展開図で - for (let i = 0; i < 9; i++) cells.push(cell((i % 3) * step, Math.floor(i / 3) * step, i)) - for (let i = 0; i < 9; i++) cells.push(cell((i % 3) * step, 3 * step + G + Math.floor(i / 3) * step, 18 + i)) - for (let i = 0; i < 9; i++) cells.push(cell(3 * step + G + (i % 3) * step, 3 * step + G + Math.floor(i / 3) * step, 9 + i)) - const w = 6 * step + G - return ( - - {cells} - - ) - } + if (c.set === 'F2L') return // 上面+側面の上段(細長いシール) const o = 6 // 側面ぶんの余白 @@ -110,3 +100,63 @@ export function CaseDiagram({ ) } + +/** 立体の図の1マスの長さ。 */ +const U3 = 10 +const COS30 = Math.cos(Math.PI / 6) +type Vec = [number, number] +// 右手前の上から見た等角図の向き:前面の左→右(X)、右面の手前→奥(Z)、下(Y) +const X: Vec = [COS30 * U3, 0.5 * U3] +const Z: Vec = [COS30 * U3, -0.5 * U3] +const Y: Vec = [0, U3] +// 手前・左・上の角を原点にする +const at = (x: number, z: number, y: number): Vec => [X[0] * x + Z[0] * z + Y[0] * y, X[1] * x + Z[1] * z + Y[1] * y] + +/** 原点を角にして、a・b の向きに広がる1マスのシール(すき間のぶん内側に縮める)。 */ +function quad(o: Vec, a: Vec, b: Vec): string { + const k = 0.08 + const p = (s: number, t: number) => `${(o[0] + a[0] * s + b[0] * t).toFixed(2)},${(o[1] + a[1] * s + b[1] * t).toFixed(2)}` + return [p(k, k), p(1 - k, k), p(1 - k, 1 - k), p(k, 1 - k)].join(' ') +} + +/** + * F2L のケースの図。キューブを右手前の上から見た立体で、上面・前面・右面を描く。 + * FR のペア(角とエッジ)だけ色を付け、上段のほかのパーツは灰色にする。 + */ +function F2LCube({ facelets, size }: { facelets: string; size: number }) { + const gray = f2lGrayStickers(facelets) + const fill = (i: number) => (gray.has(i) ? GRAY : (COLORS[facelets[i]] ?? GRAY)) + const stickers: React.ReactNode[] = [] + const add = (i: number, o: Vec, a: Vec, b: Vec) => + stickers.push() + for (let r = 0; r < 3; r++) { + for (let col = 0; col < 3; col++) { + // 上面:U0 が奥の左。行は奥から手前、列は左から右 + add(3 * r + col, at(col, 2 - r, 0), X, Z) + // 前面:F18 が上の左 + add(18 + 3 * r + col, at(col, 0, r), X, Y) + // 右面:R9 が上の手前。列は手前から奥 + add(9 + 3 * r + col, at(3, col, r), Z, Y) + } + } + // キューブの輪郭(六角形)を黒で敷いて、シールのすき間を黒く見せる + const outline = [at(0, 0, 0), at(0, 3, 0), at(3, 3, 0), at(3, 3, 3), at(3, 0, 3), at(0, 0, 3)] + .map((v) => v.join(',')) + .join(' ') + const w = 6 * X[0] + const top = 3 * Z[1] + const h = 3 * Y[1] - top + 3 * X[1] + const pad = 1.5 + return ( + + + {stickers} + + ) +}