feat: unified 0 and 90 degree PDF envelope and category descriptors
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
import { useMemo } from 'react';
|
||||
import { OrbitControls, Grid, Environment, Text } from '@react-three/drei';
|
||||
import * as THREE from 'three';
|
||||
import SceneCanvas from '../SceneCanvas';
|
||||
import FallbackDiagram from '../FallbackDiagram';
|
||||
|
||||
export interface Bridge3DInput {
|
||||
/** Maior vão Lₚ (m) */
|
||||
lp: number;
|
||||
/** Largura do tabuleiro B (m) */
|
||||
width: number;
|
||||
/** Altura do tabuleiro z (m) */
|
||||
deckHeight: number;
|
||||
/** Altura equivalente H_eq (m) — soma de áreas expostas por metro */
|
||||
heg: number;
|
||||
/** Coeficiente de arrasto Cx (adimensional) */
|
||||
cx: number;
|
||||
/** Coeficiente de sustentação Cz (adimensional) */
|
||||
cz: number;
|
||||
/** Força de arrasto por unidade de comprimento Fx (kN/m) */
|
||||
fxPerLength: number;
|
||||
/** Força de sustentação por unidade de comprimento Fz (kN/m) */
|
||||
fzPerLength: number;
|
||||
}
|
||||
|
||||
function BridgeModel({
|
||||
lp,
|
||||
width,
|
||||
deckHeight,
|
||||
heg,
|
||||
cx,
|
||||
fxPerLength,
|
||||
fzPerLength,
|
||||
}: Bridge3DInput) {
|
||||
const halfL = lp / 2;
|
||||
const halfW = width / 2;
|
||||
const deckThickness = Math.max(heg, 0.8);
|
||||
const deckY = deckHeight;
|
||||
|
||||
// Cor do tabuleiro baseada em Cx
|
||||
const deckColor = useMemo(() => {
|
||||
const intensity = Math.min(1, Math.abs(cx) / 3);
|
||||
const hue = 200 - intensity * 60;
|
||||
return new THREE.Color(`hsl(${hue}, ${55 + intensity * 30}%, ${50 - intensity * 8}%)`);
|
||||
}, [cx]);
|
||||
|
||||
// Pilar heights: posicionar 3 pilares ao longo do vão
|
||||
const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]);
|
||||
|
||||
// Vetor de força (Fx horizontal)
|
||||
const fxLen = Math.min(Math.max(Math.abs(fxPerLength) * 0.5, 0.3), 4);
|
||||
const fxDir = fxPerLength >= 0 ? 1 : -1;
|
||||
// Vetor de força (Fz vertical)
|
||||
const fzLen = Math.min(Math.max(Math.abs(fzPerLength) * 0.5, 0.3), 4);
|
||||
const fzDir = fzPerLength >= 0 ? 1 : -1;
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Tabuleiro (deck) */}
|
||||
<mesh position={[0, deckY, 0]} castShadow receiveShadow>
|
||||
<boxGeometry args={[lp, deckThickness, width]} />
|
||||
<meshStandardMaterial color={deckColor} roughness={0.5} />
|
||||
</mesh>
|
||||
|
||||
{/* Guarda-rodas/barreira lateral */}
|
||||
<mesh position={[0, deckY + deckThickness / 2 + 0.3, halfW - 0.15]} castShadow>
|
||||
<boxGeometry args={[lp, 0.5, 0.1]} />
|
||||
<meshStandardMaterial color="#94a3b8" roughness={0.7} />
|
||||
</mesh>
|
||||
<mesh position={[0, deckY + deckThickness / 2 + 0.3, -halfW + 0.15]} castShadow>
|
||||
<boxGeometry args={[lp, 0.5, 0.1]} />
|
||||
<meshStandardMaterial color="#94a3b8" roughness={0.7} />
|
||||
</mesh>
|
||||
|
||||
{/* Pilares (3 ao longo do comprimento) */}
|
||||
{pillarHeights.map((h, i) => {
|
||||
const x = i === 0 ? -halfL + halfL * 0.3 : i === 1 ? 0 : halfL - halfL * 0.3;
|
||||
return (
|
||||
<mesh key={`pillar-${i}`} position={[x, h / 2, 0]} castShadow receiveShadow>
|
||||
<boxGeometry args={[1.5, h, 1.5]} />
|
||||
<meshStandardMaterial color="#64748b" roughness={0.7} />
|
||||
</mesh>
|
||||
);
|
||||
})}
|
||||
|
||||
{/* Solo / água */}
|
||||
<mesh position={[0, -0.5, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
|
||||
<planeGeometry args={[lp * 1.6, width * 3]} />
|
||||
<meshStandardMaterial color="#60a5fa" opacity={0.4} transparent roughness={0.3} />
|
||||
</mesh>
|
||||
|
||||
{/* Vetor Cx (horizontal) */}
|
||||
<ForceArrow
|
||||
start={[-halfL * 0.6, deckY + deckThickness + 0.3, halfW + 0.5]}
|
||||
direction={[fxDir, 0, 0]}
|
||||
length={fxLen}
|
||||
color="#ef4444"
|
||||
/>
|
||||
|
||||
{/* Vetor Cz (vertical) */}
|
||||
<ForceArrow
|
||||
start={[halfL * 0.6, deckY + deckThickness + 0.3, halfW + 0.5]}
|
||||
direction={[0, fzDir, 0]}
|
||||
length={fzLen}
|
||||
color="#3b82f6"
|
||||
/>
|
||||
|
||||
{/* Vetor no centro também para destacar */}
|
||||
<ForceArrow
|
||||
start={[0, deckY + deckThickness + 0.3, 0]}
|
||||
direction={[fxDir, 0, 0]}
|
||||
length={fxLen * 0.7}
|
||||
color="#ef4444"
|
||||
/>
|
||||
<Text
|
||||
position={[0, deckY + deckThickness + 1.0, 0]}
|
||||
fontSize={0.6}
|
||||
color="#1e40af"
|
||||
anchorX="center"
|
||||
anchorY="bottom"
|
||||
>
|
||||
Lp={lp}m | B={width}m | Cx={cx.toFixed(2)}
|
||||
</Text>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
function ForceArrow({
|
||||
start,
|
||||
direction,
|
||||
length,
|
||||
color,
|
||||
}: {
|
||||
start: [number, number, number];
|
||||
direction: [number, number, number];
|
||||
length: number;
|
||||
color: string;
|
||||
}) {
|
||||
const startVec = useMemo(() => new THREE.Vector3(...start), [start]);
|
||||
const dirVec = useMemo(() => new THREE.Vector3(...direction), [direction]);
|
||||
const end = useMemo(
|
||||
() => new THREE.Vector3(
|
||||
startVec.x + dirVec.x * length,
|
||||
startVec.y + dirVec.y * length,
|
||||
startVec.z + dirVec.z * length,
|
||||
),
|
||||
[startVec, dirVec, length],
|
||||
);
|
||||
const mid = useMemo(
|
||||
() => new THREE.Vector3().addVectors(startVec, end).multiplyScalar(0.5),
|
||||
[startVec, end],
|
||||
);
|
||||
const quat = useMemo(() => {
|
||||
const dir = new THREE.Vector3().subVectors(end, startVec).normalize();
|
||||
const q = new THREE.Quaternion();
|
||||
q.setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
||||
return new THREE.Euler().setFromQuaternion(q);
|
||||
}, [startVec, end]);
|
||||
const headLen = 0.3;
|
||||
|
||||
return (
|
||||
<group>
|
||||
{length - headLen > 0.01 && (
|
||||
<mesh position={mid.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
||||
<cylinderGeometry args={[0.06, 0.06, length - headLen, 10]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
)}
|
||||
<mesh position={end.toArray()} rotation={[quat.x, quat.y, quat.z]} castShadow>
|
||||
<coneGeometry args={[0.15, headLen, 10]} />
|
||||
<meshStandardMaterial color={color} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
export default function Bridge3DViewer(input: Bridge3DInput) {
|
||||
const { lp, deckHeight, width } = input;
|
||||
const dist = Math.max(lp * 0.6, deckHeight * 2);
|
||||
|
||||
const fallback = (
|
||||
<FallbackDiagram
|
||||
type="bridge"
|
||||
props={input}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<SceneCanvas
|
||||
shadows
|
||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||
camera={{ position: [dist * 0.8, deckHeight + width, dist], fov: 45 }}
|
||||
fallback={fallback}
|
||||
>
|
||||
<ambientLight intensity={0.6} />
|
||||
<directionalLight position={[lp, deckHeight * 3, width * 3]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
|
||||
<BridgeModel {...input} />
|
||||
<Grid infiniteGrid fadeDistance={lp * 0.5} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} />
|
||||
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} />
|
||||
<Environment preset="city" />
|
||||
</SceneCanvas>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user