2024/12/16 投稿
```rubik-4
W W W W
W W W W
W W W W
W W W W
R R R R
R R R R
R R R R
R R R R
G G G G Y Y Y Y B B B B
G G G G Y Y Y Y B B B B
G G G G Y Y Y Y B B B B
G G G G Y Y Y Y B B B B
O O O O
O O O O
O O O O
O O O O
```
各面の色は以下表の通りです。
色 | 記号 |
---|---|
白 | W |
赤 | R |
緑 | G |
青 | B |
オレンジ | O |
黄 | Y |
黒 | K |
灰 | H |
```rubik3D-3
W W W
W W W
W W W
R R R
R R R
R R R
G G G Y Y Y B B B
G G G Y Y Y B B B
G G G Y Y Y B B B
O O O
O O O
O O O
```
import { Plugin } from 'unified';
import { visit } from 'unist-util-visit';
import type { Parent, Code, RootContent } from 'mdast';
// ASTノード拡張
type ExtendedRootContent = RootContent | { type: 'html'; value: string };
export const remarkRubik: Plugin = () => {
return (tree) => {
visit(tree, 'code', (node, index, parent) => {
const codeNode = node as Code;
const parentNode = parent as Parent;
if (codeNode.lang && codeNode.lang.startsWith('rubik-') && codeNode.value) {
// サイズ取得
const sizeMatch = codeNode.lang.match(/^rubik-(\d+)$/);
const size = sizeMatch ? parseInt(sizeMatch[1], 10) : 3;
// 正しいパースで面データ取得
const { top, front, right, back, left, bottom } = parseCode(codeNode.value);
// facesConfigは { top, left, front, bottom, right, back } の順を前提としているから
// parseCodeの結果を再配置する
const facesInOriginalOrder = { back, top, left, front, right, bottom };
const svgContent = generateSVGContent(size, facesInOriginalOrder);
const wrappedContent = `
<div class="col-start-1 col-span-4 md:col-span-3 lg:col-span-3">
${svgContent}
</div>
`;
const htmlNode: ExtendedRootContent = {
type: 'html',
value: wrappedContent,
};
parentNode.children[index!] = htmlNode;
}
});
};
};
const parseCode = (code: string): Record<string, string[][]> => {
const lines = code.trim().split("\n").map((line) => line.trim().split(" "));
const size = lines[0].length;
const back = lines.slice(0, size);
const top = lines.slice(size, size * 2);
const left = lines.slice(size * 2, size * 3).map((row) => row.slice(0, size));
const front = lines.slice(size * 2, size * 3).map((row) => row.slice(size, size * 2));
const right = lines.slice(size * 2, size * 3).map((row) => row.slice(size * 2, size * 3));
const bottom = lines.slice(size * 3);
return { top, front, right, back, left, bottom };
};
//--------------------------
// SVG生成関連ヘルパー
//--------------------------
const CELL_SIZE = 30;
type FaceData = string[][];
const colorMap: Record<string, string> = {
w: "white",
r: "red",
g: "green",
b: "blue",
o: "orange",
y: "yellow",
h: "gray",
k: "black",
};
const mapColor = (c: string) => colorMap[c.toLowerCase()] || "gray";
const drawSquare = (x: number, y: number, size: number, color: string) => {
return `<rect x="${x}" y="${y}" width="${size}" height="${size}" fill="${color}" stroke="black"></rect>`;
};
const drawParallelogram = (points: Array<[number, number]>, color: string) => {
const pointsStr = points.map(([px, py]) => `${px},${py}`).join(' ');
return `<polygon points="${pointsStr}" fill="${color}" stroke="black"></polygon>`;
};
const facesConfig = {
back: {
drawCell: (i: number, j: number, color: string) => {
// 元々topとして描かれていた位置関係そのまま
return drawSquare(180 + j * CELL_SIZE, i * CELL_SIZE, CELL_SIZE, color);
}
},
top: {
drawCell: (i: number, j: number, color: string) => {
const x = 180 + j * CELL_SIZE - i * (CELL_SIZE / 2);
const y = 120 + i * (CELL_SIZE / 2);
const dx = -CELL_SIZE / 2;
const dy = CELL_SIZE / 2;
const points: Array<[number, number]> = [
[x, y],
[x + CELL_SIZE, y],
[x + CELL_SIZE + dx, y + dy],
[x + dx, y + dy],
];
return drawParallelogram(points, color);
}
},
left: {
drawCell: (i: number, j: number, color: string) => {
return drawSquare(j * CELL_SIZE, 180 + i * CELL_SIZE, CELL_SIZE, color);
}
},
front: {
drawCell: (i: number, j: number, color: string) => {
return drawSquare(120 + j * CELL_SIZE, 180 + i * CELL_SIZE, CELL_SIZE, color);
}
},
right: {
drawCell: (i: number, j: number, color: string) => {
const x = 240 + j * (CELL_SIZE / 2);
const y = 180 + i * CELL_SIZE - j * (CELL_SIZE / 2);
const dx = CELL_SIZE / 2;
const dy = -CELL_SIZE / 2;
const points: Array<[number, number]> = [
[x, y],
[x + dx, y + dy],
[x + dx, y + dy + CELL_SIZE],
[x, y + CELL_SIZE],
];
return drawParallelogram(points, color);
}
},
bottom: {
drawCell: (i: number, j: number, color: string) => {
return drawSquare(120 + j * CELL_SIZE, 300 + i * CELL_SIZE, CELL_SIZE, color);
}
},
};
type Faces = { top: FaceData; left: FaceData; front: FaceData; bottom: FaceData; right: FaceData; back: FaceData };
const generateSVGContent = (size: number, faces: Faces) => {
let svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 420" class="rubik-cube">`;
for (const [faceName, faceData] of Object.entries(faces) as [keyof Faces, FaceData][]) {
const { drawCell } = facesConfig[faceName];
for (let i = 0; i < size; i++) {
for (let j = 0; j < size; j++) {
const color = mapColor(faceData[i][j]);
svg += drawCell(i, j, color);
}
}
}
svg += `</svg>`;
return svg;
};
"use client";
import React, { useRef, useEffect } from "react";
import * as THREE from "three";
import { OrbitControls, LineSegments2, LineSegmentsGeometry, LineMaterial } from "three-stdlib";
type Props = {
code: string;
};
const MarkdownRubikCode = ({ code }: Props) => {
const mountRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const mount = mountRef.current;
const parent = mount?.parentElement;
const width = parent?.offsetWidth || 0;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xffffff);
const camera = new THREE.PerspectiveCamera(40, 1, 0.1, 1000);
camera.position.set(6, 6, 6);
const renderer = new THREE.WebGLRenderer({
antialias: true,
});
renderer.setSize(width, width);
mount?.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.enableZoom = false;
const colorMap: { [key: string]: number } = {
W: 0xffffff,
R: 0xc41e3a,
G: 0x009e60,
B: 0x0051ba,
O: 0xff5800,
Y: 0xffd500,
H: 0x808080,
K: 0x000000,
};
const cubeSize = 1;
const parseCode = (code: string): Record<string, string[][]> => {
const lines = code.trim().split("\n").map((line) => line.trim().split(" "));
const size = lines[0].length;
const back = lines.slice(0, size);
const top = lines.slice(size, size * 2);
const left = lines.slice(size * 2, size * 3).map((row) => row.slice(0, size));
const front = lines.slice(size * 2, size * 3).map((row) => row.slice(size, size * 2));
const right = lines.slice(size * 2, size * 3).map((row) => row.slice(size * 2, size * 3));
const bottom = lines.slice(size * 3);
return { top, front, right, back, left, bottom };
};
const { top, front, right, back, left, bottom } = parseCode(code);
const createCube = (x: number, y: number, z: number, faces: string[]) => {
const geometry = new THREE.BoxGeometry(cubeSize, cubeSize, cubeSize);
const materials = [
new THREE.MeshBasicMaterial({ color: colorMap[faces[0]] || 0x808080 }),
new THREE.MeshBasicMaterial({ color: colorMap[faces[1]] || 0x808080 }),
new THREE.MeshBasicMaterial({ color: colorMap[faces[2]] || 0x808080 }),
new THREE.MeshBasicMaterial({ color: colorMap[faces[3]] || 0x808080 }),
new THREE.MeshBasicMaterial({ color: colorMap[faces[4]] || 0x808080 }),
new THREE.MeshBasicMaterial({ color: colorMap[faces[5]] || 0x808080 }),
];
const cube = new THREE.Mesh(geometry, materials);
const edges = new THREE.EdgesGeometry(geometry);
const edgeGeometry = new LineSegmentsGeometry().fromEdgesGeometry(edges);
const edgeMaterial = new LineMaterial({
color: 0x000000,
linewidth: 4,
});
edgeMaterial.resolution.set(window.innerWidth, window.innerHeight);
const edgeLine = new LineSegments2(edgeGeometry, edgeMaterial);
const group = new THREE.Group();
group.add(cube);
group.add(edgeLine);
group.position.set(x, y, z);
return group;
};
const size = top.length;
const offset = (size - 1) / 2;
const cubes = new THREE.Group();
for (let x = 0; x < size; x++) {
for (let y = 0; y < size; y++) {
for (let z = 0; z < size; z++) {
const faces = [
x === size - 1 ? right[size - 1 - y][size - 1 - z] : "K",
x === 0 ? left[size - 1 - y][z] : "K",
y === size - 1 ? top[z][x] : "K",
y === 0 ? bottom[size - 1 - z][x] : "K",
z === size - 1 ? front[size - 1 - y][x] : "K",
z === 0 ? back[y][x] : "K",
];
const cube = createCube(x - offset, y - offset, z - offset, faces);
cubes.add(cube);
}
}
}
scene.add(cubes);
const animate = () => {
requestAnimationFrame(animate);
controls.update();
renderer.render(scene, camera);
};
animate();
const handleResize = () => {
if (mount && parent) {
camera.aspect = 1;
camera.updateProjectionMatrix();
renderer.setSize(width, width);
}
};
window.addEventListener("resize", handleResize);
handleResize();
return () => {
window.removeEventListener("resize", handleResize);
mount?.removeChild(renderer.domElement);
renderer.dispose();
};
}, [code]);
return (
<div className="col-start-1 col-span-4 md:col-span-3 lg:col-span-3" >
<div ref={mountRef} style={{ width: "100%" }} />
</div>
);
};
export default MarkdownRubikCode;