Files
ozone-tech_owl_prime/src/components/ThreeD/RollCageMesh.tsx
Даня Архипов 394c513037 chore: checkpoint premium 3d stages 0 and 1
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-29 19:05:27 +00:00

170 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* RollCageMesh — честная сетчатая модель роллтейнера C/D по ground truth.
*
* Exterior bounding box: 1200 × 800 × 800 мм (включая колёса) — ROLL_CAGE.
* Открытый верх, читаемая сетка стен (~100мм), пол-панель на CAGE_FLOOR_Y.
*
* Вся геометрия — 3 instanced draw call (трубы+прутья, колёса) + 1 mesh (пол).
* Одна и та же модель используется обеими сценами (`/` и `/details`).
*
* Классификация узла (Stage 1 §13.4): PROCEDURAL_FALLBACK — официальной
* CAD-модели роллтейнера в архивах нет; размеры соответствуют спецификации.
*/
import { useEffect, useMemo, useRef } from 'react';
import * as THREE from 'three';
import { ROLL_CAGE, CAGE_FLOOR_Y } from '../../domain/physicalLayout';
const { width: W, depth: D, height: H, wheelRadius: WR, frameThickness: FT } = ROLL_CAGE;
const WHEEL_D = WR * 2; // 0.08m — cage floor height (CAGE_FLOOR_Y)
const BODY_H = H - WHEEL_D; // frame body above wheels; total exterior = H exactly
const ROD = 0.008; // grid rod thickness (8mm wire)
const GRID_STEP = 0.1; // ~100mm grid pitch
interface CageInstances {
boxes: THREE.Matrix4[];
wheels: THREE.Matrix4[];
}
function boxInstance(x: number, y: number, z: number, sx: number, sy: number, sz: number): THREE.Matrix4 {
return new THREE.Matrix4().compose(
new THREE.Vector3(x, y, z),
new THREE.Quaternion(),
new THREE.Vector3(sx, sy, sz),
);
}
/** Deterministic instance layout for the cage (built once). */
function buildInstances(): CageInstances {
const boxes: THREE.Matrix4[] = [];
const yBot = WHEEL_D; // bottom of frame body
const yTop = H; // top of frame body (exterior top)
// 4 corner posts
for (const sx of [-1, 1]) {
for (const sz of [-1, 1]) {
boxes.push(boxInstance(sx * (W / 2 - FT / 2), yBot + BODY_H / 2, sz * (D / 2 - FT / 2), FT, BODY_H, FT));
}
}
// bottom + top frame rectangles
for (const y of [yBot + FT / 2, yTop - FT / 2]) {
boxes.push(boxInstance(0, y, D / 2 - FT / 2, W, FT, FT));
boxes.push(boxInstance(0, y, -(D / 2 - FT / 2), W, FT, FT));
boxes.push(boxInstance(W / 2 - FT / 2, y, 0, FT, FT, D - FT * 2));
boxes.push(boxInstance(-(W / 2 - FT / 2), y, 0, FT, FT, D - FT * 2));
}
// grid walls between frames (interior span)
const yGridBot = yBot + FT;
const yGridTop = yTop - FT;
const gridH = yGridTop - yGridBot;
const yMid = yGridBot + gridH / 2;
const xInner = W / 2 - FT; // inner half-width
const zInner = D / 2 - FT;
// front/back walls (z = ±(D/2 − ROD/2)): vertical + horizontal rods
const vCols = Math.floor((xInner * 2) / GRID_STEP) - 1; // exclude corners (posts)
const hRows = Math.max(1, Math.round(gridH / GRID_STEP) - 1);
for (const sz of [-1, 1]) {
const z = sz * (D / 2 - ROD / 2);
for (let i = 1; i <= vCols; i++) {
const x = -xInner + (i * (xInner * 2)) / (vCols + 1);
boxes.push(boxInstance(x, yMid, z, ROD, gridH, ROD));
}
for (let r = 1; r <= hRows; r++) {
const y = yGridBot + (r * gridH) / (hRows + 1);
boxes.push(boxInstance(0, y, z, W - FT * 2, ROD, ROD));
}
}
// side walls (x = ±(W/2 − ROD/2))
const sCols = Math.floor((zInner * 2) / GRID_STEP) - 1;
for (const sx of [-1, 1]) {
const x = sx * (W / 2 - ROD / 2);
for (let i = 1; i <= sCols; i++) {
const z = -zInner + (i * (zInner * 2)) / (sCols + 1);
boxes.push(boxInstance(x, yMid, z, ROD, gridH, ROD));
}
for (let r = 1; r <= hRows; r++) {
const y = yGridBot + (r * gridH) / (hRows + 1);
boxes.push(boxInstance(x, y, 0, ROD, ROD, D - FT * 2));
}
}
// caster wheels (lying cylinders)
const wheels: THREE.Matrix4[] = [];
const wheelQuat = new THREE.Quaternion().setFromEuler(new THREE.Euler(0, 0, Math.PI / 2));
for (const sx of [-1, 1]) {
for (const sz of [-1, 1]) {
wheels.push(new THREE.Matrix4().compose(
new THREE.Vector3(sx * (W / 2 - 0.08), WR, sz * (D / 2 - 0.08)),
wheelQuat,
new THREE.Vector3(1, 1, 1),
));
}
}
return { boxes, wheels };
}
export default function RollCageMesh({ color, active = false, shadows = false }: {
color: string;
active?: boolean;
shadows?: boolean;
}) {
const instances = useMemo(buildInstances, []);
const boxGeo = useMemo(() => new THREE.BoxGeometry(1, 1, 1), []);
const wheelGeo = useMemo(() => new THREE.CylinderGeometry(WR, WR, 0.03, 12), []);
const boxesRef = useRef<THREE.InstancedMesh>(null);
const wheelsRef = useRef<THREE.InstancedMesh>(null);
useEffect(() => {
const boxes = boxesRef.current;
if (boxes) {
instances.boxes.forEach((m, i) => boxes.setMatrixAt(i, m));
boxes.instanceMatrix.needsUpdate = true;
}
const wheels = wheelsRef.current;
if (wheels) {
instances.wheels.forEach((m, i) => wheels.setMatrixAt(i, m));
wheels.instanceMatrix.needsUpdate = true;
}
}, [instances]);
useEffect(() => () => {
boxGeo.dispose();
wheelGeo.dispose();
}, [boxGeo, wheelGeo]);
const emissiveIntensity = active ? 0.35 : 0;
return (
<group>
{/* frame + grid walls: single instanced draw call */}
<instancedMesh
ref={boxesRef}
args={[boxGeo, undefined, instances.boxes.length]}
castShadow={shadows}
>
<meshStandardMaterial
color={color}
metalness={0.6}
roughness={0.35}
emissive={color}
emissiveIntensity={emissiveIntensity}
/>
</instancedMesh>
{/* caster wheels: single instanced draw call */}
<instancedMesh ref={wheelsRef} args={[wheelGeo, undefined, instances.wheels.length]}>
<meshStandardMaterial color="#475569" metalness={0.7} roughness={0.3} />
</instancedMesh>
{/* interior floor pan where items rest (top at CAGE_FLOOR_Y) */}
<mesh position={[0, CAGE_FLOOR_Y - 0.005, 0]} receiveShadow={shadows}>
<boxGeometry args={[W - FT, 0.01, D - FT]} />
<meshStandardMaterial color="#1e293b" metalness={0.3} roughness={0.7} />
</mesh>
</group>
);
}