Repository: ori-cube/OriCube
Pull request #8: スナップ処理の実装 [OC-19]
Author: login: yutteee
Created: 2025-05-05T05:28:43Z | Merged: 2025-05-10T03:35:27Z

Notion: https://www.notion.so/1e58eaf7118c80da8cd6fe821fd942d2?pvs=4

# スナップ機能の実装とコードのリファクタリング

## 概要
折り紙アプリケーションの点選択機能において、スナップ機能を実装し、関連コードをリファクタリングしました。これによって折り紙を綺麗に半分に折ることが可能になりました。

スナップ機能：マウスが頂点、中心点の近くにある場合に最も近い頂点または中心点にフォーカスを当てる機能

## 変更内容

### 1. スナップ機能
- スナップ対象：頂点と辺の中心点
- スナップポイントの視覚化：小さな灰色の点で表示
- ハイライト表示：スナップ可能な点の近くでは水色の点でハイライト
- 選択表示：選択した点は青色の大きい点で表示

### 2. コード構造の改善
```
src/components/OrigamiPost/hooks/useSelectPoints/
├── index.tsx # メインのカスタムフック
├── renderPoint.ts # 点を描画するロジック
├── useInitialRender.ts # 初期描画処理
├── useMouseMove.ts # マウス移動とスナップ処理
└── useClickHandler.ts # クリックイベント処理
```

## スクリーンショット

https://github.com/user-attachments/assets/2d7a5b65-f811-4b52-b655-cf54f8f84c41


---
## 開発中確認したバグ

これらは別のPRで修正する予定。

- 同じ辺上の2点は選択できないようにしたい
- step3で一回折ってからstep1に戻ると板が分割されてしまう



Diff:
diff --git a/.cursor/rules/TypeScript.mdc b/.cursor/rules/TypeScript.mdc
new file mode 100644
index 0000000..28dcd3c
--- /dev/null
+++ b/.cursor/rules/TypeScript.mdc
@@ -0,0 +1,40 @@
+---
+description: 
+globs: 
+alwaysApply: true
+---
+# TypeScriptルール
+
+## カスタムフックの型定義について
+
+- カスタムフック（useXXX）のprops型は、Props型エイリアス（例: XxxProps）として定義せず、式単位（関数型）で定義すること。
+- 具体的には、以下のように型を定義し、関数宣言に適用する。
+
+```
+type UseXxx = (props: {
+  // 必要なpropsをここに記述
+}) => void;
+
+export const useXxx: UseXxx = ({ ... }) => {
+  // ...実装...
+};
+```
+
+- このルールにより、propsの過不足が型レベルで厳密にチェックされ、関数の型と実装が1対1で対応し、可読性・保守性が向上する。
+- props型を他で再利用する必要がある場合のみ、型エイリアスとして切り出すことを許容する。
+
+---
+
+## 例
+
+NG例（Props型エイリアスを使う）
+```ts
+type XxxProps = { ... };
+export const useXxx = (props: XxxProps) => { ... }
+```
+
+OK例（式単位で型を定義）
+```ts
+type UseXxx = (props: { ... }) => void;
+export const useXxx: UseXxx = ({ ... }) => { ... }
+```
diff --git a/src/components/OrigamiPost/atoms/inputStepObjectAtom.ts b/src/components/OrigamiPost/atoms/inputStepObjectAtom.ts
index d82add8..faac5ad 100644
--- a/src/components/OrigamiPost/atoms/inputStepObjectAtom.ts
+++ b/src/components/OrigamiPost/atoms/inputStepObjectAtom.ts
@@ -23,7 +23,7 @@ type InputStep = {
   description: string;
 };
 
-type InputStepObject = {
+export type InputStepObject = {
   [key: string]: InputStep;
 };
 
diff --git a/src/components/OrigamiPost/hooks/useSelectPoints/index.tsx b/src/components/OrigamiPost/hooks/useSelectPoints/index.tsx
index 4395e05..c8d0e30 100644
--- a/src/components/OrigamiPost/hooks/useSelectPoints/index.tsx
+++ b/src/components/OrigamiPost/hooks/useSelectPoints/index.tsx
@@ -2,13 +2,14 @@
 step1での、点の選択を管理するカスタムフック
 inputStepがaxisの時の板の描画と点の選択を行う
 **/
-import React, { useEffect } from "react";
+import React from "react";
 import * as THREE from "three";
-import { renderPoint } from "../../logics/renderPoint";
-import { renderBoard } from "../../logics/renderBoard";
 import { currentStepAtom } from "../../atoms/currentStepAtom";
 import { inputStepObjectAtom } from "../../atoms/inputStepObjectAtom";
 import { useAtom, useAtomValue } from "jotai";
+import { useInitialRender } from "./useInitialRender";
+import { useMouseMove } from "./useMouseMove";
+import { useClickHandler } from "./useClickHandler";
 
 type UseSelectPoints = (props: {
   canvasRef: React.MutableRefObject<HTMLCanvasElement | null>;
@@ -29,6 +30,8 @@ export const useSelectPoints: UseSelectPoints = ({
 }) => {
   const currentStep = useAtomValue(currentStepAtom);
   const [inputStepObject, setInputStepObject] = useAtom(inputStepObjectAtom);
+  const [highlightedVertex, setHighlightedVertex] =
+    React.useState<THREE.Vector3 | null>(null);
 
   const inputStep = currentStep.inputStep;
   const procedureIndex = currentStep.procedureIndex;
@@ -36,100 +39,36 @@ export const useSelectPoints: UseSelectPoints = ({
   const selectedPoints =
     inputStepObject[procedureIndex.toString()].selectedPoints;
 
-  // boardとpointの初期描画をする処理
-  useEffect(() => {
-    if (inputStep !== "axis") return;
-    const scene = sceneRef.current!;
-    const renderer = rendererRef.current!;
-    const camera = cameraRef.current!;
-
-    // sceneの初期化
-    scene.children = [];
-
-    // pointsを描画
-    selectedPoints.forEach((point) => {
-      renderPoint({ scene, point });
-    });
-    // boardsを描画
-    fixBoards.forEach((board) => {
-      renderBoard({ scene, board, color: origamiColor });
-    });
-
-    renderer.render(scene, camera);
-
-    // selectedPointsが変更されたときの描画は、下のuseEffectで行う
-    // eslint-disable-next-line react-hooks/exhaustive-deps
-  }, [inputStep, sceneRef, rendererRef, cameraRef, fixBoards, origamiColor]);
-
-  // モデル上の点を選択できるようにする処理
-  useEffect(() => {
-    const sizes = {
-      width: window.innerWidth - 320,
-      height: window.innerHeight,
-    };
-    if (inputStep !== "axis") return;
-    const canvas = canvasRef.current!;
-    const scene = sceneRef.current!;
-    const renderer = rendererRef.current!;
-    const camera = cameraRef.current!;
-    const raycaster = raycasterRef.current!;
-
-    const clickListener = (event: MouseEvent) => {
-      // マウスの座標を正規化して取得
-      const mouse = new THREE.Vector2();
-      mouse.x = (event.clientX / sizes.width) * 2 - 1;
-      mouse.y = -(event.clientY / sizes.height) * 2 + 1;
-
-      // エッジに対してRaycasterを適用して交差を調べる
-      raycaster.setFromCamera(mouse, camera);
-      const edges = scene.children.filter(
-        (child) => child.type === "LineSegments"
-      );
-      const intersects = raycaster.intersectObjects(edges, true);
-
-      // 現在記録している点の数が2個未満の場合は、新しい点を追加, 2個以上の場合は最初の点を消して、新しい点を追加
-      if (intersects.length > 0) {
-        const point = intersects[0].point; // 新しく選択された点
-        let newPoints = [...selectedPoints];
-
-        if (selectedPoints.length < 2) {
-          newPoints.push([point.x, point.y, point.z]);
-        } else {
-          newPoints = newPoints.slice(1);
-          newPoints.push([point.x, point.y, point.z]);
-        }
-
-        // 既存のpointを削除する
-        scene.children = scene.children.filter(
-          (child) => child.name !== "Point"
-        );
-        // pointsを描画し直す
-        newPoints.forEach((point) => {
-          renderPoint({ scene, point });
-        });
-        setInputStepObject((prev) => ({
-          ...prev,
-          [procedureIndex.toString()]: {
-            ...prev[procedureIndex.toString()],
-            selectedPoints: newPoints,
-          },
-        }));
-
-        renderer.render(scene, camera);
-      }
-    };
-
-    canvas.addEventListener("click", clickListener);
-    return () => {
-      canvas.removeEventListener("click", clickListener);
-    };
-  }, [
+  useInitialRender({
+    inputStep,
+    sceneRef,
+    rendererRef,
+    cameraRef,
+    fixBoards,
     selectedPoints,
+    origamiColor,
+  });
+
+  useMouseMove({
     inputStep,
     canvasRef,
     sceneRef,
-    rendererRef,
     cameraRef,
+    rendererRef,
     raycasterRef,
-  ]);
+    fixBoards,
+    setHighlightedVertex,
+  });
+
+  useClickHandler({
+    inputStep,
+    canvasRef,
+    sceneRef,
+    cameraRef,
+    rendererRef,
+    selectedPoints,
+    highlightedVertex,
+    procedureIndex,
+    setInputStepObject,
+  });
 };
diff --git a/src/components/OrigamiPost/hooks/useSelectPoints/renderPoint.ts b/src/components/OrigamiPost/hooks/useSelectPoints/renderPoint.ts
new file mode 100644
index 0000000..fa06eae
--- /dev/null
+++ b/src/components/OrigamiPost/hooks/useSelectPoints/renderPoint.ts
@@ -0,0 +1,61 @@
+import * as THREE from "three";
+import { Point } from "@/types/model";
+
+// 共通化した型定義
+type RenderPoint = (props: {
+  scene: THREE.Scene;
+  point: Point;
+  color: number;
+  scale: number;
+  name: string;
+}) => void;
+
+export const renderPoint: RenderPoint = ({
+  scene,
+  point,
+  color,
+  scale,
+  name,
+}) => {
+  const geometry = new THREE.SphereGeometry(1, 32, 32).scale(
+    scale,
+    scale,
+    scale
+  );
+  const material = new THREE.MeshBasicMaterial({ color });
+  const mesh = new THREE.Mesh(geometry, material);
+  mesh.position.set(point[0], point[1], point[2]);
+  mesh.name = name;
+  scene.add(mesh);
+};
+
+// 既存の関数はラップするだけ
+export const renderSelectedPoint = (props: {
+  scene: THREE.Scene;
+  point: Point;
+}) =>
+  renderPoint({
+    ...props,
+    color: 0x001eff,
+    scale: 1,
+    name: "Point",
+  });
+
+export const renderHighlightPoint = (props: {
+  scene: THREE.Scene;
+  point: Point;
+}) =>
+  renderPoint({
+    ...props,
+    color: 0x00aaff,
+    scale: 0.7,
+    name: "HighlightPoint",
+  });
+
+export const renderSnapPoint = (props: { scene: THREE.Scene; point: Point }) =>
+  renderPoint({
+    ...props,
+    color: 0x007b94,
+    scale: 0.3,
+    name: "SnapPoint",
+  });
diff --git a/src/components/OrigamiPost/hooks/useSelectPoints/useClickHandler.ts b/src/components/OrigamiPost/hooks/useSelectPoints/useClickHandler.ts
new file mode 100644
index 0000000..9358752
--- /dev/null
+++ b/src/components/OrigamiPost/hooks/useSelectPoints/useClickHandler.ts
@@ -0,0 +1,100 @@
+/* 
+点の選択処理を行うフック
+- ハイライトされている点を選択点として追加
+- 選択点が2つ以上ある場合
+  - 2個のうち先に選択した点を削除
+  - 新しい点を選択点として追加
+**/
+
+import { useEffect } from "react";
+import * as THREE from "three";
+import { Point } from "@/types/model";
+import { renderSelectedPoint } from "./renderPoint";
+import { InputStepObject } from "../../atoms/inputStepObjectAtom";
+
+type UseClickHandler = (props: {
+  inputStep: string;
+  canvasRef: React.MutableRefObject<HTMLCanvasElement | null>;
+  sceneRef: React.MutableRefObject<THREE.Scene | null>;
+  cameraRef: React.MutableRefObject<THREE.PerspectiveCamera | null>;
+  rendererRef: React.MutableRefObject<THREE.WebGLRenderer | null>;
+  selectedPoints: Point[];
+  highlightedVertex: THREE.Vector3 | null;
+  procedureIndex: number;
+  setInputStepObject: (
+    updater: (prev: InputStepObject) => InputStepObject
+  ) => void;
+}) => void;
+
+export const useClickHandler: UseClickHandler = ({
+  inputStep,
+  canvasRef,
+  sceneRef,
+  cameraRef,
+  rendererRef,
+  selectedPoints,
+  highlightedVertex,
+  procedureIndex,
+  setInputStepObject,
+}) => {
+  useEffect(() => {
+    if (inputStep !== "axis") return;
+    const canvas = canvasRef.current!;
+    const scene = sceneRef.current!;
+    const renderer = rendererRef.current!;
+    const camera = cameraRef.current!;
+
+    const clickListener = () => {
+      if (!highlightedVertex) return;
+
+      // ハイライトされている頂点を選択
+      let newPoints = [...selectedPoints];
+
+      if (selectedPoints.length < 2) {
+        newPoints.push([
+          highlightedVertex.x,
+          highlightedVertex.y,
+          highlightedVertex.z,
+        ]);
+      } else {
+        newPoints = newPoints.slice(1);
+        newPoints.push([
+          highlightedVertex.x,
+          highlightedVertex.y,
+          highlightedVertex.z,
+        ]);
+      }
+
+      // 既存のpointを削除する
+      scene.children = scene.children.filter((child) => child.name !== "Point");
+      // pointsを描画し直す
+      newPoints.forEach((point) => {
+        renderSelectedPoint({ scene, point });
+      });
+      setInputStepObject((prev) => ({
+        ...prev,
+        [procedureIndex.toString()]: {
+          ...prev[procedureIndex.toString()],
+          selectedPoints: newPoints,
+        },
+      }));
+
+      renderer.render(scene, camera);
+    };
+
+    canvas.addEventListener("click", clickListener);
+    return () => {
+      canvas.removeEventListener("click", clickListener);
+    };
+  }, [
+    selectedPoints,
+    inputStep,
+    canvasRef,
+    sceneRef,
+    rendererRef,
+    cameraRef,
+    highlightedVertex,
+    procedureIndex,
+    setInputStepObject,
+  ]);
+};
diff --git a/src/components/OrigamiPost/hooks/useSelectPoints/useInitialRender.ts b/src/components/OrigamiPost/hooks/useSelectPoints/useInitialRender.ts
new file mode 100644
index 0000000..5cdd6df
--- /dev/null
+++ b/src/components/OrigamiPost/hooks/useSelectPoints/useInitialRender.ts
@@ -0,0 +1,83 @@
+/* 
+step1で、点の選択を行うための初期描画を行うフック
+- sceneの初期化
+- 板の描画
+- スナップポイント(頂点と辺の中心点)の描画
+**/
+
+import { useEffect } from "react";
+import * as THREE from "three";
+import { renderSelectedPoint, renderSnapPoint } from "./renderPoint";
+import { renderBoard } from "../../logics/renderBoard";
+import { Point } from "@/types/model";
+
+type UseInitialRender = (props: {
+  inputStep: string;
+  sceneRef: React.MutableRefObject<THREE.Scene | null>;
+  rendererRef: React.MutableRefObject<THREE.WebGLRenderer | null>;
+  cameraRef: React.MutableRefObject<THREE.PerspectiveCamera | null>;
+  fixBoards: Point[][];
+  selectedPoints: Point[];
+  origamiColor: string;
+}) => void;
+
+export const useInitialRender: UseInitialRender = ({
+  inputStep,
+  sceneRef,
+  rendererRef,
+  cameraRef,
+  fixBoards,
+  selectedPoints,
+  origamiColor,
+}) => {
+  useEffect(() => {
+    if (inputStep !== "axis") return;
+    const scene = sceneRef.current!;
+    const renderer = rendererRef.current!;
+    const camera = cameraRef.current!;
+
+    // sceneの初期化
+    scene.children = [];
+
+    // pointsを描画
+    selectedPoints.forEach((point) => {
+      renderSelectedPoint({ scene, point });
+    });
+
+    // boardsを描画
+    fixBoards.forEach((board) => {
+      renderBoard({ scene, board, color: origamiColor });
+
+      // 頂点のスナップポイントを描画
+      board.forEach((vertex) => {
+        renderSnapPoint({ scene, point: vertex });
+      });
+
+      // 辺の中心点のスナップポイントを描画
+      for (let i = 0; i < board.length; i++) {
+        const nextIndex = (i + 1) % board.length;
+        const currentVertex = board[i];
+        const nextVertex = board[nextIndex];
+
+        // 中心点を計算
+        const centerPoint: Point = [
+          (currentVertex[0] + nextVertex[0]) / 2,
+          (currentVertex[1] + nextVertex[1]) / 2,
+          (currentVertex[2] + nextVertex[2]) / 2,
+        ];
+
+        renderSnapPoint({ scene, point: centerPoint });
+      }
+    });
+
+    renderer.render(scene, camera);
+  }, [
+    inputStep,
+    sceneRef,
+    rendererRef,
+    cameraRef,
+    fixBoards,
+    origamiColor,
+    selectedPoints,
+  ]);
+};
diff --git a/src/components/OrigamiPost/hooks/useSelectPoints/useMouseMove.ts b/src/components/OrigamiPost/hooks/useSelectPoints/useMouseMove.ts
new file mode 100644
index 0000000..cd47a78
--- /dev/null
+++ b/src/components/OrigamiPost/hooks/useSelectPoints/useMouseMove.ts
@@ -0,0 +1,150 @@
+/*
+マウス移動時のスナップ処理を行うフック
+- マウスの監視
+- マウスが頂点、中心点の近くにある場合
+  - 最も近くにある頂点または中心点をハイライト
+- 近くにない場合
+  - マウスと板上の辺の交点をハイライト
+**/
+
+import { useEffect } from "react";
+import * as THREE from "three";
+import { Point } from "@/types/model";
+import { renderHighlightPoint } from "./renderPoint";
+
+type UseMouseMove = (props: {
+  inputStep: string;
+  canvasRef: React.MutableRefObject<HTMLCanvasElement | null>;
+  sceneRef: React.MutableRefObject<THREE.Scene | null>;
+  cameraRef: React.MutableRefObject<THREE.PerspectiveCamera | null>;
+  rendererRef: React.MutableRefObject<THREE.WebGLRenderer | null>;
+  raycasterRef: React.MutableRefObject<THREE.Raycaster | null>;
+  fixBoards: Point[][];
+  setHighlightedVertex: (vertex: THREE.Vector3 | null) => void;
+}) => void;
+
+export const useMouseMove: UseMouseMove = ({
+  inputStep,
+  canvasRef,
+  sceneRef,
+  cameraRef,
+  rendererRef,
+  raycasterRef,
+  fixBoards,
+  setHighlightedVertex,
+}) => {
+  const SNAP_THRESHOLD = 0.1; // スナップする距離の閾値
+
+  useEffect(() => {
+    if (inputStep !== "axis") return;
+    const canvas = canvasRef.current!;
+    const scene = sceneRef.current!;
+    const camera = cameraRef.current!;
+    const renderer = rendererRef.current!;
+    const raycaster = raycasterRef.current!;
+
+    const mouseMoveListener = (event: MouseEvent) => {
+      const sizes = {
+        width: window.innerWidth - 320,
+        height: window.innerHeight,
+      };
+
+      // マウス座標の正規化
+      const mouse = new THREE.Vector2();
+      mouse.x = (event.clientX / sizes.width) * 2 - 1;
+      mouse.y = -(event.clientY / sizes.height) * 2 + 1;
+
+      // 全ての板の頂点と辺の中心点を取得
+      let closestVertex: THREE.Vector3 | null = null;
+      let minDistance = Infinity;
+
+      // 頂点と辺の中心点のチェック
+      fixBoards.forEach((board) => {
+        // 頂点のチェック
+        board.forEach((vertex) => {
+          const vertexVector = new THREE.Vector3(
+            vertex[0],
+            vertex[1],
+            vertex[2]
+          );
+          const vertexScreenPos = vertexVector.clone().project(camera);
+          const distance = new THREE.Vector2(mouse.x, mouse.y).distanceTo(
+            new THREE.Vector2(vertexScreenPos.x, vertexScreenPos.y)
+          );
+          if (distance < SNAP_THRESHOLD && distance < minDistance) {
+            minDistance = distance;
+            closestVertex = vertexVector;
+          }
+        });
+
+        // 辺の中心点のチェック
+        for (let i = 0; i < board.length; i++) {
+          const nextIndex = (i + 1) % board.length;
+          const currentVertex = board[i];
+          const nextVertex = board[nextIndex];
+
+          const centerPoint = new THREE.Vector3(
+            (currentVertex[0] + nextVertex[0]) / 2,
+            (currentVertex[1] + nextVertex[1]) / 2,
+            (currentVertex[2] + nextVertex[2]) / 2
+          );
+
+          const centerScreenPos = centerPoint.clone().project(camera);
+          const distance = new THREE.Vector2(mouse.x, mouse.y).distanceTo(
+            new THREE.Vector2(centerScreenPos.x, centerScreenPos.y)
+          );
+
+          if (distance < SNAP_THRESHOLD && distance < minDistance) {
+            minDistance = distance;
+            closestVertex = centerPoint;
+          }
+        }
+      });
+
+      // 頂点も辺の中心点も見つからない場合は、エッジ上の最近点を探す
+      if (!closestVertex) {
+        raycaster.setFromCamera(mouse, camera);
+        const edges = scene.children.filter(
+          (child) => child.type === "LineSegments"
+        );
+        const intersects = raycaster.intersectObjects(edges, true);
+
+        if (intersects.length > 0) {
+          closestVertex = intersects[0].point;
+        }
+      }
+
+      // 既存のハイライトを削除
+      scene.children = scene.children.filter(
+        (child) => child.name !== "HighlightPoint"
+      );
+
+      // 新しいハイライトを描画
+      if (closestVertex) {
+        renderHighlightPoint({
+          scene,
+          point: [closestVertex.x, closestVertex.y, closestVertex.z],
+        });
+        setHighlightedVertex(closestVertex);
+      } else {
+        setHighlightedVertex(null);
+      }
+
+      renderer.render(scene, camera);
+    };
+
+    canvas.addEventListener("mousemove", mouseMoveListener);
+    return () => {
+      canvas.removeEventListener("mousemove", mouseMoveListener);
+    };
+  }, [
+    inputStep,
+    canvasRef,
+    sceneRef,
+    cameraRef,
+    rendererRef,
+    raycasterRef,
+    fixBoards,
+    setHighlightedVertex,
+  ]);
+};
diff --git a/src/components/OrigamiPost/logics/renderPoint.ts b/src/components/OrigamiPost/logics/renderPoint.ts
deleted file mode 100644
index 4e0c3a0..0000000
--- a/src/components/OrigamiPost/logics/renderPoint.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-import * as THREE from "three";
-import { Point } from "@/types/model";
-
-type RenderPoint = (props: { scene: THREE.Scene; point: Point }) => void;
-
-export const renderPoint: RenderPoint = ({ scene, point }) => {
-  const geometry = new THREE.SphereGeometry(1, 32, 32).scale(0.7, 0.7, 0.7);
-  const material = new THREE.MeshBasicMaterial({ color: 0xff00ff });
-  const pointMesh = new THREE.Mesh(geometry, material);
-  pointMesh.position.set(point[0], point[1], point[2]);
-  pointMesh.name = "Point";
-  scene.add(pointMesh);
-};


Reviews:
user: login: YutaK1026
body: 確認お願いします！（うち3つは変更依頼というよりは確認です）
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T11:54:22Z
user: login: yutteee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T01:28:23Z
user: login: yutteee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-06T01:51:42Z
user: login: YutaK1026
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T01:43:36Z
user: login: YutaK1026
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T01:43:55Z
user: login: YutaK1026
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-07T01:44:25Z

Review comments:
diff_hunk: @@ -2,13 +2,14 @@
 step1での、点の選択を管理するカスタムフック
 inputStepがaxisの時の板の描画と点の選択を行う
 **/
-import React, { useEffect } from "react";
+import React from "react";
 import * as THREE from "three";
-import { renderPoint } from "../../logics/renderPoint";
-import { renderBoard } from "../../logics/renderBoard";
 import { currentStepAtom } from "../../atoms/currentStepAtom";
 import { inputStepObjectAtom } from "../../atoms/inputStepObjectAtom";
 import { useAtom, useAtomValue } from "jotai";
+import { useInitialRender } from "./useInitialRender";
+import { useMouseMove } from "./useMouseMove";
+import { useClickHandler } from "./useClickHandler";
 
 type UseSelectPoints = (props: {
path: src/components/OrigamiPost/hooks/useSelectPoints/index.tsx
user: login: YutaK1026
body: 新しく作成してくれたtypeの命名だとPropsがついているもんで、どっちかに統一したいな
created_at: 2025-05-05T11:45:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,43 @@
+import * as THREE from "three";
+import { Point } from "@/types/model";
+
+type RenderPoint = (props: { scene: THREE.Scene; point: Point }) => void;
+
+export const renderPoint: RenderPoint = ({ scene, point }) => {
+  const geometry = new THREE.SphereGeometry(1, 32, 32).scale(1, 1, 1);
+  const material = new THREE.MeshBasicMaterial({ color: 0x001eff });
path: src/components/OrigamiPost/hooks/useSelectPoints/renderPoint.ts
user: login: YutaK1026
body: 個人的にcolorとかはハードコーディングするんじゃなくて、設定ファイルからimportするようにしたいなって気持ちがあります。理由は、もし折り紙の色を変えたりしたときに、RenterPointが見えにくくなったりするんじゃないかな？って思うからです。（俺はこのmaterialを選択した点だと思って読んでるけど、もし違ったら教えてほしい！）
created_at: 2025-05-05T11:50:24Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,43 @@
+import * as THREE from "three";
+import { Point } from "@/types/model";
+
+type RenderPoint = (props: { scene: THREE.Scene; point: Point }) => void;
+
+export const renderPoint: RenderPoint = ({ scene, point }) => {
+  const geometry = new THREE.SphereGeometry(1, 32, 32).scale(1, 1, 1);
+  const material = new THREE.MeshBasicMaterial({ color: 0x001eff });
+  const pointMesh = new THREE.Mesh(geometry, material);
+  pointMesh.position.set(point[0], point[1], point[2]);
+  pointMesh.name = "Point";
+  scene.add(pointMesh);
+};
+
+export const renderHighlightPoint = ({
+  scene,
+  point,
+}: {
+  scene: THREE.Scene;
+  point: Point;
+}) => {
+  const geometry = new THREE.SphereGeometry(1, 32, 32).scale(0.7, 0.7, 0.7);
+  const material = new THREE.MeshBasicMaterial({ color: 0x00aaff });
path: src/components/OrigamiPost/hooks/useSelectPoints/renderPoint.ts
user: login: YutaK1026
body: 同上
created_at: 2025-05-05T11:50:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,43 @@
+import * as THREE from "three";
+import { Point } from "@/types/model";
+
+type RenderPoint = (props: { scene: THREE.Scene; point: Point }) => void;
+
+export const renderPoint: RenderPoint = ({ scene, point }) => {
+  const geometry = new THREE.SphereGeometry(1, 32, 32).scale(1, 1, 1);
+  const material = new THREE.MeshBasicMaterial({ color: 0x001eff });
+  const pointMesh = new THREE.Mesh(geometry, material);
+  pointMesh.position.set(point[0], point[1], point[2]);
+  pointMesh.name = "Point";
+  scene.add(pointMesh);
+};
+
+export const renderHighlightPoint = ({
+  scene,
+  point,
+}: {
+  scene: THREE.Scene;
+  point: Point;
+}) => {
+  const geometry = new THREE.SphereGeometry(1, 32, 32).scale(0.7, 0.7, 0.7);
+  const material = new THREE.MeshBasicMaterial({ color: 0x00aaff });
+  const sphere = new THREE.Mesh(geometry, material);
+  sphere.position.set(point[0], point[1], point[2]);
+  sphere.name = "HighlightPoint";
+  scene.add(sphere);
+};
+
+export const renderSnapPoint = ({
+  scene,
+  point,
+}: {
+  scene: THREE.Scene;
+  point: Point;
+}) => {
+  const geometry = new THREE.SphereGeometry(1, 32, 32).scale(0.3, 0.3, 0.3);
+  const material = new THREE.MeshBasicMaterial({ color: 0x007b94 });
path: src/components/OrigamiPost/hooks/useSelectPoints/renderPoint.ts
user: login: YutaK1026
body: 同上
created_at: 2025-05-05T11:50:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,13 +2,14 @@
 step1での、点の選択を管理するカスタムフック
 inputStepがaxisの時の板の描画と点の選択を行う
 **/
-import React, { useEffect } from "react";
+import React from "react";
 import * as THREE from "three";
-import { renderPoint } from "../../logics/renderPoint";
-import { renderBoard } from "../../logics/renderBoard";
 import { currentStepAtom } from "../../atoms/currentStepAtom";
 import { inputStepObjectAtom } from "../../atoms/inputStepObjectAtom";
 import { useAtom, useAtomValue } from "jotai";
+import { useInitialRender } from "./useInitialRender";
+import { useMouseMove } from "./useMouseMove";
+import { useClickHandler } from "./useClickHandler";
 
 type UseSelectPoints = (props: {
path: src/components/OrigamiPost/hooks/useSelectPoints/index.tsx
user: login: yutteee
body: props単位ではなく式単位で型定義するように修正しました！
created_at: 2025-05-06T01:28:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,43 @@
+import * as THREE from "three";
+import { Point } from "@/types/model";
+
+type RenderPoint = (props: { scene: THREE.Scene; point: Point }) => void;
+
+export const renderPoint: RenderPoint = ({ scene, point }) => {
+  const geometry = new THREE.SphereGeometry(1, 32, 32).scale(1, 1, 1);
+  const material = new THREE.MeshBasicMaterial({ color: 0x001eff });
path: src/components/OrigamiPost/hooks/useSelectPoints/renderPoint.ts
user: login: yutteee
body: 色を管理するファイルを追加しました！
また、関数が全部似てたので共通化をしました。
created_at: 2025-05-06T01:51:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -2,13 +2,14 @@
 step1での、点の選択を管理するカスタムフック
 inputStepがaxisの時の板の描画と点の選択を行う
 **/
-import React, { useEffect } from "react";
+import React from "react";
 import * as THREE from "three";
-import { renderPoint } from "../../logics/renderPoint";
-import { renderBoard } from "../../logics/renderBoard";
 import { currentStepAtom } from "../../atoms/currentStepAtom";
 import { inputStepObjectAtom } from "../../atoms/inputStepObjectAtom";
 import { useAtom, useAtomValue } from "jotai";
+import { useInitialRender } from "./useInitialRender";
+import { useMouseMove } from "./useMouseMove";
+import { useClickHandler } from "./useClickHandler";
 
 type UseSelectPoints = (props: {
path: src/components/OrigamiPost/hooks/useSelectPoints/index.tsx
user: login: YutaK1026
body: ありがとう！
created_at: 2025-05-07T01:43:36Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,43 @@
+import * as THREE from "three";
+import { Point } from "@/types/model";
+
+type RenderPoint = (props: { scene: THREE.Scene; point: Point }) => void;
+
+export const renderPoint: RenderPoint = ({ scene, point }) => {
+  const geometry = new THREE.SphereGeometry(1, 32, 32).scale(1, 1, 1);
+  const material = new THREE.MeshBasicMaterial({ color: 0x001eff });
path: src/components/OrigamiPost/hooks/useSelectPoints/renderPoint.ts
user: login: YutaK1026
body: ありがとう！共通化までしてくれて助かります 🙇 
created_at: 2025-05-07T01:43:55Z
author_association: CONTRIBUTOR

PR comments:
user: login: vercel[bot]
created_at: 2025-05-05T05:28:49Z
body: [vc]: #iptPahzlmva7huKYE153fqG8dkQzBjLcvJy0HTadjUc=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJvcmktY3ViZSIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJvcmktY3ViZS1naXQtb2MtMTktb3JpLWN1YmUudmVyY2VsLmFwcCJ9LCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vb3JpLWN1YmUvb3JpLWN1YmUvQmh4eHd6ckVxVWROYUVHUzNpTlkxb3Y0WVRXWiIsInByZXZpZXdVcmwiOiJvcmktY3ViZS1naXQtb2MtMTktb3JpLWN1YmUudmVyY2VsLmFwcCIsIm5leHRDb21taXRTdGF0dXMiOiJERVBMT1lFRCIsInJvb3REaXJlY3RvcnkiOm51bGx9XX0=
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **ori-cube** | ✅ Ready ([Inspect](https://vercel.com/ori-cube/ori-cube/BhxxwzrEqUdNaEGS3iNY1ov4YTWZ)) | [Visit Preview](https://ori-cube-git-oc-19-ori-cube.vercel.app) | 💬 [**Add feedback**](https://vercel.live/open-feedback/ori-cube-git-oc-19-ori-cube.vercel.app?via=pr-comment-feedback-link) | May 6, 2025 1:41am |


author_association: NONE
user: login: notion-workspace[bot]
created_at: 2025-05-05T06:00:24Z
body: <a href="https://www.notion.so/1e58eaf7118c80da8cd6fe821fd942d2?pvs=8&n=github_linkback">スナップ機能(折り線選択時の辺の中心にカーソルが吸い寄せられるやつ)の実装</a>
author_association: NONE