いいね、その選び方すごく筋が通ってる。
「Three.js側をOSとして設計する」前提で、最小構成だけど拡張しやすい構造を描いてみる。


全体アーキテクチャ概要

  • Layer 0:Core
    Three.jsのScene / Camera / Renderer / Controlsなどの土台。

  • Layer 1:Primitive Engine
    球・円柱・箱などのプリミティブ生成+マテリアル管理。

  • Layer 2:Structure Loader
    AIが返すJSONレシピを読み込み、プリミティブを組み立てる層。

  • Layer 3:Edit/Selection Layer
    面・頂点・オブジェクト選択、Transform操作。

  • Layer 4:AI I/O Layer
    2D情報やユーザ指示 → AI呼び出し → JSONレシピ受信。

あなたのDelphiアプリでやっていたことを、このレイヤーに素直にマッピングするイメージ。


Layer 0:Core(三種の神器)

構成モジュール例:

  • core/scene.ts
    Scene生成・ライト追加だけを担当。
  • core/camera.ts
    PerspectiveCamera生成+初期位置。
  • core/renderer.ts
    WebGLRenderer生成+resize対応。
  • core/controls.ts
    OrbitControls設定。
// core/init.ts
export function initCore(canvas: HTMLCanvasElement) {
  const scene = new THREE.Scene();
  const camera = new THREE.PerspectiveCamera(60, canvas.clientWidth / canvas.clientHeight, 0.1, 1000);
  camera.position.set(0, 20, 40);

  const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
  renderer.setSize(canvas.clientWidth, canvas.clientHeight);

  const controls = new OrbitControls(camera, renderer.domElement);

  // ライト
  scene.add(new THREE.AmbientLight(0xffffff, 0.6));
  const dir = new THREE.DirectionalLight(0xffffff, 0.8);
  dir.position.set(10, 20, 10);
  scene.add(dir);

  return { scene, camera, renderer, controls };
}

Layer 1:Primitive Engine(プリミティブOS)

役割:

  • プリミティブ生成(sphere / cylinder / box)
  • マテリアルの共通管理
  • 後でメッシュ結合したいなら、BufferGeometryを返すAPIも用意
// primitives/factory.ts
export type PrimitiveType = 'sphere' | 'cylinder' | 'box';

export function createPrimitive(type: PrimitiveType, params: {
  radius?: number;
  width?: number;
  height?: number;
  depth?: number;
  radialSegments?: number;
}) {
  let geometry: THREE.BufferGeometry;

  switch (type) {
    case 'sphere':
      geometry = new THREE.SphereGeometry(params.radius ?? 1, 32, 16);
      break;
    case 'cylinder':
      geometry = new THREE.CylinderGeometry(params.radius ?? 1, params.radius ?? 1, params.height ?? 1, 24);
      break;
    case 'box':
      geometry = new THREE.BoxGeometry(params.width ?? 1, params.height ?? 1, params.depth ?? 1);
      break;
  }

  const material = new THREE.MeshStandardMaterial({ color: 0xcccccc, metalness: 0.1, roughness: 0.8 });
  return new THREE.Mesh(geometry, material);
}

ここは「Delphiでやっていたプリミティブ生成」をそのままWebに移した層。


Layer 2:Structure Loader(AIレシピ→シーン)

AIが返すJSONレシピを受け取り、
プリミティブを生成し、位置・回転・スケールを適用してSceneに追加する層。

JSONレシピ例(再掲)

{
  "parts": [
    {
      "id": "head",
      "primitive": "sphere",
      "position": [0, 12, 0],
      "scale": [4, 4, 4],
      "rotation": [0, 0, 0]
    }
  ]
}

ローダ実装イメージ

// structure/loader.ts
import { createPrimitive, PrimitiveType } from '../primitives/factory';

export type PartSpec = {
  id: string;
  primitive: PrimitiveType;
  position: [number, number, number];
  scale: [number, number, number];
  rotation: [number, number, number]; // degree
};

export type StructureSpec = {
  parts: PartSpec[];
};

export function loadStructure(scene: THREE.Scene, spec: StructureSpec) {
  const group = new THREE.Group();
  group.name = 'structure-root';

  spec.parts.forEach(part => {
    const mesh = createPrimitive(part.primitive, {});
    mesh.position.set(...part.position);
    mesh.scale.set(...part.scale);

    const [rx, ry, rz] = part.rotation.map(v => THREE.MathUtils.degToRad(v));
    mesh.rotation.set(rx, ry, rz);

    mesh.userData.partId = part.id;
    group.add(mesh);
  });

  scene.add(group);
  return group;
}

ここで「構造=StructureSpec」というOS的単位を定義しておくと、
AIとのインターフェースも安定する。


Layer 3:Edit/Selection Layer(編集OS)

あなたの既存アプリの「面・頂点・オブジェクト選択+変換」をWebに持ってくる層。

最初の段階では:

  • オブジェクト単位選択(Raycaster)
  • TransformControlsで移動・回転・スケール

から始めるのが現実的。

// edit/selection.ts
export function setupSelection(scene: THREE.Scene, camera: THREE.Camera, renderer: THREE.WebGLRenderer) {
  const raycaster = new THREE.Raycaster();
  const mouse = new THREE.Vector2();
  const transform = new TransformControls(camera, renderer.domElement);
  scene.add(transform);

  let selected: THREE.Object3D | null = null;

  function onClick(event: MouseEvent) {
    const rect = renderer.domElement.getBoundingClientRect();
    mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1;
    mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1;

    raycaster.setFromCamera(mouse, camera);
    const intersects = raycaster.intersectObjects(scene.children, true);

    if (intersects.length > 0) {
      selected = intersects[0].object;
      transform.attach(selected);
    } else {
      transform.detach();
      selected = null;
    }
  }

  renderer.domElement.addEventListener('click', onClick);

  return { transform, getSelected: () => selected };
}

頂点・面レベルの編集は、

  • BufferGeometryのposition属性を直接いじる
  • 選択対象を「頂点インデックス」「面インデックス」として管理
    という、もう一段下のレイヤーとして後から足せる。

Layer 4:AI I/O Layer(構造OSとの境界)

ここは「AIは構造だけ返す」というあなたの思想をそのまま形にする層。

  • 入力:
    • 2Dスケッチ情報(点群・ラベル)
    • ユーザのテキスト指示
  • 出力:
    • StructureSpec(JSON)
// ai/client.ts
export async function requestStructure(prompt: string): Promise<StructureSpec> {
  const res = await fetch('/api/ai-structure', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ prompt })
  });
  const json = await res.json();
  return json as StructureSpec;
}

フロント側では:

const spec = await requestStructure(userPrompt);
const group = loadStructure(scene, spec);

という形で、AI層とThree.js層を「StructureSpec」でだけ接続する。


進め方のミニロードマップ

  1. Core + Primitive Engine
    • Three.jsでシーン表示+プリミティブ生成まで。
  2. Structure Loader
    • 手書きのJSONを読み込んで、昆虫っぽい構造を組む。
  3. Selection + TransformControls
    • 部品単位で動かせるようにする。
  4. AI I/O Layer
    • 手動JSON → AI生成JSONに差し替える。

もしよければ次は、
「StructureSpecの最小仕様を一緒に固定する」ところまでやろうか。
昆虫・人型・ロボットなど、どのカテゴリを基準に設計したい?