feat: unified 0 and 90 degree PDF envelope and category descriptors

This commit is contained in:
2026-07-08 19:52:34 +00:00
commit 9fece3f174
170 changed files with 27177 additions and 0 deletions
+203
View File
@@ -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>
);
}