🚀 Auto-deploy: BrainWind atualizado em 29/07/2026 12:00:49
This commit is contained in:
@@ -400,7 +400,7 @@ function TowerDiagram({ baseWidth, height, panels, phi, forceKN }: TowerProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IsolatedRoofProps {
|
interface IsolatedRoofProps {
|
||||||
type: 'shed' | 'gable';
|
type: 'shed' | 'gable' | 'butterfly';
|
||||||
theta: number;
|
theta: number;
|
||||||
height: number;
|
height: number;
|
||||||
width: number;
|
width: number;
|
||||||
|
|||||||
@@ -132,9 +132,9 @@ export function AirflowSystem({ windAngle, width, length, height, permeabilityCa
|
|||||||
if (inY && inX && inZ) {
|
if (inY && inX && inZ) {
|
||||||
// Perto das paredes
|
// Perto das paredes
|
||||||
if (Math.abs(pz - (-halfL)) < margin) localCpe = wallCpe.A;
|
if (Math.abs(pz - (-halfL)) < margin) localCpe = wallCpe.A;
|
||||||
else if (Math.abs(pz - (halfL)) < margin) localCpe = wallCpe.B;
|
else if (Math.abs(pz - (halfL)) < margin) localCpe = (windAngle === 90 ? 0.5 : wallCpe.B);
|
||||||
else if (Math.abs(px - (-halfW)) < margin) localCpe = wallCpe.C;
|
else if (Math.abs(px - (-halfW)) < margin) localCpe = wallCpe.C;
|
||||||
else if (Math.abs(px - (halfW)) < margin) localCpe = wallCpe.D;
|
else if (Math.abs(px - (halfW)) < margin) localCpe = (windAngle === 0 ? 0.5 : wallCpe.D);
|
||||||
else localCpe = cpi;
|
else localCpe = cpi;
|
||||||
|
|
||||||
// Colisão dura (Impede entrar na parede sólida)
|
// Colisão dura (Impede entrar na parede sólida)
|
||||||
@@ -195,12 +195,12 @@ export function AirflowSystem({ windAngle, width, length, height, permeabilityCa
|
|||||||
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
if (p.currentCpe > 0) {
|
if (p.currentCpe > 0) {
|
||||||
colorObj.set(isDark ? '#ef4444' : '#dc2626').lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe));
|
colorObj.set(isDark ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão)
|
||||||
} else if (p.currentCpe < 0) {
|
} else if (p.currentCpe < 0) {
|
||||||
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
colorObj.set(isDark ? '#7dd3fc' : '#38bdf8').lerp(new THREE.Color(isDark ? '#1e40af' : '#1d4ed8'), intensity);
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
|
||||||
} else {
|
} else {
|
||||||
colorObj.set(isDark ? '#f1f5f9' : '#64748b');
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
|
||||||
}
|
}
|
||||||
meshRef.current!.setColorAt(i, colorObj);
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import SceneCanvas from '../SceneCanvas';
|
|||||||
import FallbackDiagram from '../FallbackDiagram';
|
import FallbackDiagram from '../FallbackDiagram';
|
||||||
import { WindArrow } from './WindArrow';
|
import { WindArrow } from './WindArrow';
|
||||||
import { useCanvasTheme } from '@/lib/theme';
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
import { BridgeAirflowSystem } from './BridgeAirflowSystem';
|
||||||
|
|
||||||
export interface Bridge3DInput {
|
export interface Bridge3DInput {
|
||||||
/** Maior vão Lₚ (m) */
|
/** Maior vão Lₚ (m) */
|
||||||
@@ -26,6 +27,8 @@ export interface Bridge3DInput {
|
|||||||
fzPerLength: number;
|
fzPerLength: number;
|
||||||
/** Ângulo de ataque do vento (graus) */
|
/** Ângulo de ataque do vento (graus) */
|
||||||
alpha: number;
|
alpha: number;
|
||||||
|
/** Modo de visualização */
|
||||||
|
viewMode?: 'solid' | 'airflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
function BridgeModel({
|
function BridgeModel({
|
||||||
@@ -34,10 +37,15 @@ function BridgeModel({
|
|||||||
deckHeight,
|
deckHeight,
|
||||||
heg,
|
heg,
|
||||||
cx,
|
cx,
|
||||||
|
cz,
|
||||||
|
fxPerLength,
|
||||||
|
fzPerLength,
|
||||||
alpha,
|
alpha,
|
||||||
|
viewMode = 'solid',
|
||||||
}: Bridge3DInput) {
|
}: Bridge3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
const isAirflow = viewMode === 'airflow';
|
||||||
|
|
||||||
const halfL = lp / 2;
|
const halfL = lp / 2;
|
||||||
const halfW = width / 2;
|
const halfW = width / 2;
|
||||||
@@ -46,33 +54,34 @@ function BridgeModel({
|
|||||||
|
|
||||||
// Cor do tabuleiro baseada em Cx
|
// Cor do tabuleiro baseada em Cx
|
||||||
const deckColor = useMemo(() => {
|
const deckColor = useMemo(() => {
|
||||||
|
if (isAirflow) return new THREE.Color(isDark ? '#334155' : '#e2e8f0');
|
||||||
const intensity = Math.min(1, Math.abs(cx) / 3);
|
const intensity = Math.min(1, Math.abs(cx) / 3);
|
||||||
const hue = 200 - intensity * 60;
|
const hue = 200 - intensity * 60;
|
||||||
const l = isDark ? (60 - intensity * 8) : (50 - intensity * 8);
|
const l = isDark ? (60 - intensity * 8) : (50 - intensity * 8);
|
||||||
return new THREE.Color(`hsl(${hue}, ${55 + intensity * 30}%, ${l}%)`);
|
return new THREE.Color(`hsl(${hue}, ${55 + intensity * 30}%, ${l}%)`);
|
||||||
}, [cx, isDark]);
|
}, [cx, isDark, isAirflow]);
|
||||||
|
|
||||||
// Pilar heights: posicionar 3 pilares ao longo do vão
|
// Pilar heights: posicionar 3 pilares ao longo do vão
|
||||||
const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]);
|
const pillarHeights = useMemo(() => [deckY - 0.5, deckY - 0.5, deckY - 0.5], [deckY]);
|
||||||
const barrierColor = isDark ? '#cbd5e1' : '#94a3b8';
|
const barrierColor = isDark ? (isAirflow ? '#475569' : '#cbd5e1') : (isAirflow ? '#cbd5e1' : '#94a3b8');
|
||||||
const pillarColor = isDark ? '#94a3b8' : '#64748b';
|
const pillarColor = isDark ? (isAirflow ? '#334155' : '#94a3b8') : (isAirflow ? '#e2e8f0' : '#64748b');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
{/* Tabuleiro (deck) */}
|
{/* Tabuleiro (deck) */}
|
||||||
<mesh position={[0, deckY, 0]} castShadow receiveShadow>
|
<mesh position={[0, deckY, 0]} castShadow receiveShadow>
|
||||||
<boxGeometry args={[lp, deckThickness, width]} />
|
<boxGeometry args={[lp, deckThickness, width]} />
|
||||||
<meshStandardMaterial color={deckColor} roughness={0.5} />
|
<meshStandardMaterial color={deckColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.5} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Guarda-rodas/barreira lateral */}
|
{/* Guarda-rodas/barreira lateral */}
|
||||||
<mesh position={[0, deckY + deckThickness / 2 + 0.3, halfW - 0.15]} castShadow>
|
<mesh position={[0, deckY + deckThickness / 2 + 0.3, halfW - 0.15]} castShadow>
|
||||||
<boxGeometry args={[lp, 0.5, 0.1]} />
|
<boxGeometry args={[lp, 0.5, 0.1]} />
|
||||||
<meshStandardMaterial color={barrierColor} roughness={0.7} />
|
<meshStandardMaterial color={barrierColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.7} />
|
||||||
</mesh>
|
</mesh>
|
||||||
<mesh position={[0, deckY + deckThickness / 2 + 0.3, -halfW + 0.15]} castShadow>
|
<mesh position={[0, deckY + deckThickness / 2 + 0.3, -halfW + 0.15]} castShadow>
|
||||||
<boxGeometry args={[lp, 0.5, 0.1]} />
|
<boxGeometry args={[lp, 0.5, 0.1]} />
|
||||||
<meshStandardMaterial color={barrierColor} roughness={0.7} />
|
<meshStandardMaterial color={barrierColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.7} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
{/* Pilares (3 ao longo do comprimento) */}
|
{/* Pilares (3 ao longo do comprimento) */}
|
||||||
@@ -87,53 +96,68 @@ function BridgeModel({
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Solo / água */}
|
{/* Solo / água */}
|
||||||
<mesh position={[0, -0.5, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
|
{!isAirflow && (
|
||||||
<planeGeometry args={[lp * 1.6, width * 3]} />
|
<mesh position={[0, -0.5, 0]} rotation={[-Math.PI / 2, 0, 0]} receiveShadow>
|
||||||
<meshStandardMaterial color="#60a5fa" opacity={0.4} transparent roughness={0.3} />
|
<planeGeometry args={[lp * 1.6, width * 3]} />
|
||||||
</mesh>
|
<meshStandardMaterial color="#60a5fa" opacity={0.4} transparent roughness={0.3} />
|
||||||
|
</mesh>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Seta de Vento (bate no meio do tabuleiro) */}
|
{/* Seta de Vento */}
|
||||||
<WindArrow
|
{isAirflow ? (
|
||||||
target={[0, deckY + deckThickness / 2, 0]}
|
<group>
|
||||||
direction={[0, Math.sin((alpha * Math.PI) / 180), Math.cos((alpha * Math.PI) / 180)]}
|
<WindArrow
|
||||||
scale={4}
|
target={[-halfL * 0.8, deckY, -halfW - 2]}
|
||||||
/>
|
direction={[0, 0, 1]}
|
||||||
<Text
|
scale={4}
|
||||||
position={[0, deckY + deckThickness + 1.5, 0]}
|
/>
|
||||||
fontSize={1.2}
|
<Text
|
||||||
color={isDark ? "#93c5fd" : "#1e40af"}
|
position={[-halfL * 0.8, deckY + 1.5, -halfW - 4]}
|
||||||
anchorX="center"
|
rotation={[0, Math.PI / 4, 0]}
|
||||||
anchorY="bottom"
|
fontSize={1.2}
|
||||||
>
|
color={isDark ? '#60a5fa' : '#3b82f6'}
|
||||||
Lp={lp}m | B={width}m | Cx={cx.toFixed(2)}
|
anchorX="center"
|
||||||
</Text>
|
anchorY="bottom"
|
||||||
|
>
|
||||||
|
Vento
|
||||||
|
</Text>
|
||||||
|
<BridgeAirflowSystem width={width} lp={lp} deckHeight={deckHeight} cx={cx} />
|
||||||
|
</group>
|
||||||
|
) : (
|
||||||
|
<WindArrow
|
||||||
|
target={[0, deckY + deckThickness / 2, 0]}
|
||||||
|
direction={[0, Math.sin((alpha * Math.PI) / 180), Math.cos((alpha * Math.PI) / 180)]}
|
||||||
|
scale={4}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Bridge3DViewer(input: Bridge3DInput) {
|
export default function Bridge3DViewer(props: Bridge3DInput) {
|
||||||
|
const { lp, width, deckHeight, heg, cx, cz, fxPerLength, fzPerLength, alpha, viewMode = 'solid' } = props;
|
||||||
|
const cameraDistance = Math.max(lp * 1.2, width * 2, deckHeight * 2, 15);
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
const { lp, deckHeight, width } = input;
|
|
||||||
const dist = Math.max(lp * 0.6, deckHeight * 2);
|
|
||||||
|
|
||||||
const fallback = (
|
const fallback = (
|
||||||
<FallbackDiagram
|
<FallbackDiagram
|
||||||
type="bridge"
|
type="bridge"
|
||||||
props={input}
|
props={{ lp, width, deckHeight, heg, cx, cz, fxPerLength, fzPerLength }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneCanvas moduleId="ponte"
|
<SceneCanvas moduleId="pontes"
|
||||||
shadows
|
shadows
|
||||||
|
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [dist * 0.8, deckHeight + width, dist], fov: 45 }}
|
camera={{ position: [-cameraDistance * 0.8, cameraDistance * 0.6, -cameraDistance], fov: 45 }}
|
||||||
fallback={fallback}
|
fallback={fallback}
|
||||||
>
|
>
|
||||||
<ambientLight intensity={0.6} />
|
<ambientLight intensity={0.6} />
|
||||||
<directionalLight position={[lp, deckHeight * 3, width * 3]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
|
<directionalLight position={[lp, deckHeight * 3, width * 3]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
|
||||||
<BridgeModel {...input} />
|
<BridgeModel {...props} viewMode={viewMode} />
|
||||||
<Grid
|
<Grid
|
||||||
infiniteGrid
|
infiniteGrid
|
||||||
fadeDistance={lp * 0.5}
|
fadeDistance={lp * 0.5}
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
import React, { useRef, useMemo, useEffect } from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
|
||||||
|
interface Particle {
|
||||||
|
position: THREE.Vector3;
|
||||||
|
baseSpeed: number;
|
||||||
|
wobbleSpeed: number;
|
||||||
|
wobbleOffset: number;
|
||||||
|
currentCpe: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BridgeAirflowSystemProps {
|
||||||
|
width: number;
|
||||||
|
lp: number;
|
||||||
|
deckHeight: number;
|
||||||
|
cx: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BridgeAirflowSystem: React.FC<BridgeAirflowSystemProps> = ({
|
||||||
|
width,
|
||||||
|
lp,
|
||||||
|
deckHeight,
|
||||||
|
cx,
|
||||||
|
}) => {
|
||||||
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||||
|
const theme = useCanvasTheme();
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
|
const COUNT = 100;
|
||||||
|
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||||
|
const colorObj = useMemo(() => new THREE.Color(), []);
|
||||||
|
|
||||||
|
const halfWidth = width / 2;
|
||||||
|
const halfLp = lp / 2;
|
||||||
|
|
||||||
|
const particles = useMemo(() => {
|
||||||
|
const arr: Particle[] = [];
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
arr.push({
|
||||||
|
position: new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * lp,
|
||||||
|
Math.random() * (deckHeight + 10),
|
||||||
|
-halfWidth - 10 - Math.random() * 20
|
||||||
|
),
|
||||||
|
baseSpeed: 0.075 + Math.random() * 0.05,
|
||||||
|
wobbleSpeed: 0.5 + Math.random() * 1.5,
|
||||||
|
wobbleOffset: Math.random() * Math.PI * 2,
|
||||||
|
currentCpe: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, [width, lp, deckHeight]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (meshRef.current) {
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
|
||||||
|
meshRef.current.setColorAt(i, colorObj);
|
||||||
|
}
|
||||||
|
meshRef.current.instanceColor!.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}, [isDark, colorObj]);
|
||||||
|
|
||||||
|
useFrame((state) => {
|
||||||
|
if (!meshRef.current) return;
|
||||||
|
const time = state.clock.elapsedTime;
|
||||||
|
|
||||||
|
particles.forEach((p, i) => {
|
||||||
|
const px = p.position.x;
|
||||||
|
const py = p.position.y;
|
||||||
|
const pz = p.position.z;
|
||||||
|
|
||||||
|
let currentSpeed = p.baseSpeed;
|
||||||
|
let localCpe = 0;
|
||||||
|
let deflectY = 0;
|
||||||
|
|
||||||
|
const inX = Math.abs(px) < halfLp + 1;
|
||||||
|
const inZ = Math.abs(pz) < halfWidth + 1;
|
||||||
|
|
||||||
|
if (inX && inZ) {
|
||||||
|
localCpe = cx;
|
||||||
|
// Tabuleiro deflete o vento
|
||||||
|
if (Math.abs(py - deckHeight) < 2) {
|
||||||
|
const dist = -pz;
|
||||||
|
const lift = Math.max(0, deckHeight + 1 - py) / (dist + 1);
|
||||||
|
deflectY += lift * currentSpeed * 0.6 * (py < deckHeight ? -1 : 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.currentCpe += (localCpe - p.currentCpe) * 0.1;
|
||||||
|
p.position.z += currentSpeed;
|
||||||
|
p.position.y += deflectY;
|
||||||
|
p.position.y += Math.sin(time * p.wobbleSpeed * 10 + p.wobbleOffset) * 0.01;
|
||||||
|
|
||||||
|
if (p.position.z > halfWidth + 10) {
|
||||||
|
p.position.z = -halfWidth - 10 - Math.random() * 5;
|
||||||
|
p.position.y = Math.random() * (deckHeight + 10);
|
||||||
|
p.position.x = (Math.random() - 0.5) * lp;
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dummy.position.copy(p.position);
|
||||||
|
dummy.rotation.set(0, 0, 0);
|
||||||
|
dummy.rotateX(Math.PI / 2);
|
||||||
|
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
|
if (p.currentCpe > 0) {
|
||||||
|
colorObj.set(isDark ? '#f87171' : '#ef4444');
|
||||||
|
} else if (p.currentCpe < 0) {
|
||||||
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity);
|
||||||
|
} else {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
|
||||||
|
}
|
||||||
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||||
|
meshRef.current.instanceColor!.needsUpdate = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<instancedMesh ref={meshRef} args={[undefined, undefined, COUNT]}>
|
||||||
|
<cylinderGeometry args={[0.04, 0.04, 1.0, 4]} />
|
||||||
|
<meshBasicMaterial transparent opacity={0.6} depthWrite={false} />
|
||||||
|
</instancedMesh>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -95,7 +95,7 @@ export function CylinderAirflowSystem({ diameter, height }: CylinderAirflowSyste
|
|||||||
} else if (Math.abs(px) <= R * 0.6 && r < R * 1.5) {
|
} else if (Math.abs(px) <= R * 0.6 && r < R * 1.5) {
|
||||||
p.currentCpe = -1.2; // Laterais (forte sucção)
|
p.currentCpe = -1.2; // Laterais (forte sucção)
|
||||||
} else if (px > R * 0.3 && Math.abs(pz) < R * 1.2) {
|
} else if (px > R * 0.3 && Math.abs(pz) < R * 1.2) {
|
||||||
p.currentCpe = -0.5; // Esteira (sucção moderada)
|
p.currentCpe = 0.5; // Esteira (agora positivo para ficar vermelho)
|
||||||
} else {
|
} else {
|
||||||
p.currentCpe = 0;
|
p.currentCpe = 0;
|
||||||
}
|
}
|
||||||
@@ -123,16 +123,12 @@ export function CylinderAirflowSystem({ diameter, height }: CylinderAirflowSyste
|
|||||||
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
if (p.currentCpe > 0) {
|
if (p.currentCpe > 0) {
|
||||||
colorObj
|
colorObj.set(isDark ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão)
|
||||||
.set(isDark ? '#ef4444' : '#dc2626')
|
|
||||||
.lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe));
|
|
||||||
} else if (p.currentCpe < 0) {
|
} else if (p.currentCpe < 0) {
|
||||||
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
colorObj
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
|
||||||
.set(isDark ? '#7dd3fc' : '#38bdf8')
|
|
||||||
.lerp(new THREE.Color(isDark ? '#1e40af' : '#1d4ed8'), intensity);
|
|
||||||
} else {
|
} else {
|
||||||
colorObj.set(isDark ? '#f1f5f9' : '#64748b');
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
|
||||||
}
|
}
|
||||||
meshRef.current!.setColorAt(i, colorObj);
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -133,7 +133,7 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
|
|||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Seta indicativa de direção do vento */}
|
{/* Seta indicativa de direção do vento */}
|
||||||
<group position={[radius + 2.5, wallHeight / 2, 0]} rotation={[0, 0, Math.PI / 2]}>
|
<group position={[-radius - 2.5, wallHeight / 2, 0]} rotation={[0, 0, -Math.PI / 2]}>
|
||||||
<mesh castShadow>
|
<mesh castShadow>
|
||||||
<coneGeometry args={[0.3, 0.8, 16]} />
|
<coneGeometry args={[0.3, 0.8, 16]} />
|
||||||
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
||||||
@@ -146,7 +146,7 @@ function DomeModel({ diameter, rise, wallHeight, cpi, cpeBarlavento, cpeTopo, cp
|
|||||||
|
|
||||||
{/* Texto Vento */}
|
{/* Texto Vento */}
|
||||||
<Text
|
<Text
|
||||||
position={[radius + 2.5, wallHeight / 2 + 1.2, 0]}
|
position={[-radius - 2.5, wallHeight / 2 + 1.2, 0]}
|
||||||
rotation={[0, 0, 0]}
|
rotation={[0, 0, 0]}
|
||||||
fontSize={1.2}
|
fontSize={1.2}
|
||||||
color="#3b82f6"
|
color="#3b82f6"
|
||||||
|
|||||||
@@ -123,8 +123,10 @@ export function DomeAirflowSystem({ diameter, rise, wallHeight }: DomeAirflowSys
|
|||||||
p.currentCpe = 0.6; // Barlavento
|
p.currentCpe = 0.6; // Barlavento
|
||||||
} else if (Math.abs(px) <= R * 0.4 && py > wallHeight) {
|
} else if (Math.abs(px) <= R * 0.4 && py > wallHeight) {
|
||||||
p.currentCpe = -1.0; // Topo da cúpula (forte sucção)
|
p.currentCpe = -1.0; // Topo da cúpula (forte sucção)
|
||||||
|
} else if (px > R * 0.2) {
|
||||||
|
p.currentCpe = 0.5; // Sotavento / Esteira (agora positivo para ficar vermelho)
|
||||||
} else {
|
} else {
|
||||||
p.currentCpe = -0.5; // Lateral / Sotavento
|
p.currentCpe = -0.5; // Laterais / transição
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
p.currentCpe = 0;
|
p.currentCpe = 0;
|
||||||
@@ -151,16 +153,12 @@ export function DomeAirflowSystem({ diameter, rise, wallHeight }: DomeAirflowSys
|
|||||||
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
if (p.currentCpe > 0) {
|
if (p.currentCpe > 0) {
|
||||||
colorObj
|
colorObj.set(isDark ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão)
|
||||||
.set(isDark ? '#ef4444' : '#dc2626')
|
|
||||||
.lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe));
|
|
||||||
} else if (p.currentCpe < 0) {
|
} else if (p.currentCpe < 0) {
|
||||||
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
colorObj
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
|
||||||
.set(isDark ? '#7dd3fc' : '#38bdf8')
|
|
||||||
.lerp(new THREE.Color(isDark ? '#1e40af' : '#1d4ed8'), intensity);
|
|
||||||
} else {
|
} else {
|
||||||
colorObj.set(isDark ? '#f1f5f9' : '#64748b');
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
|
||||||
}
|
}
|
||||||
meshRef.current!.setColorAt(i, colorObj);
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -4,12 +4,12 @@ import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls';
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import SceneCanvas from '../SceneCanvas';
|
import SceneCanvas from '../SceneCanvas';
|
||||||
import FallbackDiagram from '../FallbackDiagram';
|
import FallbackDiagram from '../FallbackDiagram';
|
||||||
import { WindArrow } from './WindArrow';
|
|
||||||
import { useCanvasTheme } from '@/lib/theme';
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
import { IsolatedRoofAirflowSystem } from './IsolatedRoofAirflowSystem';
|
||||||
|
|
||||||
export interface IsolatedRoof3DInput {
|
export interface IsolatedRoof3DInput {
|
||||||
/** Tipo de cobertura: 'shed' (uma água) ou 'gable' (duas águas) */
|
/** Tipo de cobertura: 'shed' (uma água), 'gable' (duas águas) ou 'butterfly' (borboleta) */
|
||||||
type: 'shed' | 'gable';
|
type: 'shed' | 'gable' | 'butterfly';
|
||||||
/** Inclinação θ (graus) */
|
/** Inclinação θ (graus) */
|
||||||
theta: number;
|
theta: number;
|
||||||
/** Altura livre dos suportes (m) */
|
/** Altura livre dos suportes (m) */
|
||||||
@@ -26,6 +26,7 @@ export interface IsolatedRoof3DInput {
|
|||||||
cpeTop: number;
|
cpeTop: number;
|
||||||
/** Força resultante na cobertura (kN) */
|
/** Força resultante na cobertura (kN) */
|
||||||
forceKN: number;
|
forceKN: number;
|
||||||
|
viewMode?: 'solid' | 'airflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
function pressureColor(cpe: number, isDark: boolean): THREE.Color {
|
function pressureColor(cpe: number, isDark: boolean): THREE.Color {
|
||||||
@@ -50,9 +51,11 @@ function IsolatedRoofModel({
|
|||||||
cpeWindward,
|
cpeWindward,
|
||||||
cpeLeeward,
|
cpeLeeward,
|
||||||
forceKN,
|
forceKN,
|
||||||
|
viewMode = 'solid',
|
||||||
}: IsolatedRoof3DInput) {
|
}: IsolatedRoof3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
const isAirflow = viewMode === 'airflow';
|
||||||
const labelColor = isDark ? '#cbd5e1' : '#475569';
|
const labelColor = isDark ? '#cbd5e1' : '#475569';
|
||||||
const textColor = isDark ? '#93c5fd' : '#1a202c';
|
const textColor = isDark ? '#93c5fd' : '#1a202c';
|
||||||
const lineMaterialColor = isDark ? '#94a3b8' : '#64748b';
|
const lineMaterialColor = isDark ? '#94a3b8' : '#64748b';
|
||||||
@@ -81,7 +84,7 @@ function IsolatedRoofModel({
|
|||||||
{ pos: [-halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
{ pos: [-halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
||||||
{ pos: [halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
{ pos: [halfWidth, (height + h_diff) / 2, halfDepth], h: height + h_diff },
|
||||||
);
|
);
|
||||||
} else {
|
} else if (type === 'gable') {
|
||||||
list.push(
|
list.push(
|
||||||
{ pos: [-halfWidth, height / 2, -halfDepth], h: height },
|
{ pos: [-halfWidth, height / 2, -halfDepth], h: height },
|
||||||
{ pos: [halfWidth, height / 2, -halfDepth], h: height },
|
{ pos: [halfWidth, height / 2, -halfDepth], h: height },
|
||||||
@@ -90,6 +93,15 @@ function IsolatedRoofModel({
|
|||||||
{ pos: [-halfWidth, (height + h_half) / 2, 0], h: height + h_half },
|
{ pos: [-halfWidth, (height + h_half) / 2, 0], h: height + h_half },
|
||||||
{ pos: [halfWidth, (height + h_half) / 2, 0], h: height + h_half },
|
{ pos: [halfWidth, (height + h_half) / 2, 0], h: height + h_half },
|
||||||
);
|
);
|
||||||
|
} else if (type === 'butterfly') {
|
||||||
|
list.push(
|
||||||
|
{ pos: [-halfWidth, (height + h_half) / 2, -halfDepth], h: height + h_half },
|
||||||
|
{ pos: [halfWidth, (height + h_half) / 2, -halfDepth], h: height + h_half },
|
||||||
|
{ pos: [-halfWidth, (height + h_half) / 2, halfDepth], h: height + h_half },
|
||||||
|
{ pos: [halfWidth, (height + h_half) / 2, halfDepth], h: height + h_half },
|
||||||
|
{ pos: [-halfWidth, height / 2, 0], h: height },
|
||||||
|
{ pos: [halfWidth, height / 2, 0], h: height },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
}, [type, depth, width, height, h_diff, h_half, halfDepth, halfWidth]);
|
}, [type, depth, width, height, h_diff, h_half, halfDepth, halfWidth]);
|
||||||
@@ -106,25 +118,9 @@ function IsolatedRoofModel({
|
|||||||
{type === 'shed' ? (
|
{type === 'shed' ? (
|
||||||
// Uma água (Shed): dividida em metade barlavento e metade sotavento
|
// Uma água (Shed): dividida em metade barlavento e metade sotavento
|
||||||
<group>
|
<group>
|
||||||
{/* Metade Barlavento (Z < 0) */}
|
<mesh position={[0, height + h_diff / 2, 0]} rotation={[-thetaRad, 0, 0]} castShadow receiveShadow>
|
||||||
<mesh
|
<boxGeometry args={[width, 0.08, depth / Math.cos(thetaRad)]} />
|
||||||
position={[0, height + h_diff / 4, -depth / 4]}
|
<meshStandardMaterial color={windwardColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.4} />
|
||||||
rotation={[-thetaRad, 0, 0]}
|
|
||||||
castShadow
|
|
||||||
receiveShadow
|
|
||||||
>
|
|
||||||
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
|
||||||
<meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} />
|
|
||||||
</mesh>
|
|
||||||
{/* Metade Sotavento (Z > 0) */}
|
|
||||||
<mesh
|
|
||||||
position={[0, height + (3 * h_diff) / 4, depth / 4]}
|
|
||||||
rotation={[-thetaRad, 0, 0]}
|
|
||||||
castShadow
|
|
||||||
receiveShadow
|
|
||||||
>
|
|
||||||
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
|
||||||
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
|
|
||||||
</mesh>
|
</mesh>
|
||||||
</group>
|
</group>
|
||||||
) : (
|
) : (
|
||||||
@@ -133,22 +129,22 @@ function IsolatedRoofModel({
|
|||||||
{/* Água Esquerda / Barlavento (Z < 0) */}
|
{/* Água Esquerda / Barlavento (Z < 0) */}
|
||||||
<mesh
|
<mesh
|
||||||
position={[0, height + h_half / 2, -depth / 4]}
|
position={[0, height + h_half / 2, -depth / 4]}
|
||||||
rotation={[-thetaRad, 0, 0]}
|
rotation={[type === 'butterfly' ? thetaRad : -thetaRad, 0, 0]}
|
||||||
castShadow
|
castShadow
|
||||||
receiveShadow
|
receiveShadow
|
||||||
>
|
>
|
||||||
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||||
<meshStandardMaterial color={windwardColor} opacity={0.9} transparent roughness={0.4} />
|
<meshStandardMaterial color={windwardColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.4} />
|
||||||
</mesh>
|
</mesh>
|
||||||
{/* Água Direita / Sotavento (Z > 0) */}
|
{/* Água Direita / Sotavento (Z > 0) */}
|
||||||
<mesh
|
<mesh
|
||||||
position={[0, height + h_half / 2, depth / 4]}
|
position={[0, height + h_half / 2, depth / 4]}
|
||||||
rotation={[thetaRad, 0, 0]}
|
rotation={[type === 'butterfly' ? -thetaRad : thetaRad, 0, 0]}
|
||||||
castShadow
|
castShadow
|
||||||
receiveShadow
|
receiveShadow
|
||||||
>
|
>
|
||||||
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
<boxGeometry args={[width, 0.08, depth / (2 * Math.cos(thetaRad))]} />
|
||||||
<meshStandardMaterial color={leewardColor} opacity={0.9} transparent roughness={0.4} />
|
<meshStandardMaterial color={leewardColor} opacity={isAirflow ? 0.35 : 0.9} transparent roughness={0.4} />
|
||||||
</mesh>
|
</mesh>
|
||||||
</group>
|
</group>
|
||||||
)}
|
)}
|
||||||
@@ -161,11 +157,27 @@ function IsolatedRoofModel({
|
|||||||
</mesh>
|
</mesh>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Vetor de vento (Seta verde/azul batendo no elemento) */}
|
{/* Vetor de vento (Seta mais visível com texto) */}
|
||||||
<WindArrow
|
<group position={[0, centerY, -halfDepth - 3.5]} rotation={[Math.PI / 2, 0, 0]}>
|
||||||
target={new THREE.Vector3(0, centerY, 0)}
|
<mesh castShadow>
|
||||||
direction={new THREE.Vector3(0, 0, 1)}
|
<coneGeometry args={[0.4, 1.2, 16]} />
|
||||||
/>
|
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, -1.0, 0]} castShadow>
|
||||||
|
<cylinderGeometry args={[0.15, 0.15, 2.0, 16]} />
|
||||||
|
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
<Text
|
||||||
|
position={[0, centerY + 1.2, -halfDepth - 3.5]}
|
||||||
|
rotation={[0, -3 * Math.PI / 4, 0]}
|
||||||
|
fontSize={1.2}
|
||||||
|
color="#3b82f6"
|
||||||
|
anchorX="center"
|
||||||
|
anchorY="bottom"
|
||||||
|
>
|
||||||
|
Vento
|
||||||
|
</Text>
|
||||||
|
|
||||||
{/* === LINHAS DE COTA (CAD-Style) === */}
|
{/* === LINHAS DE COTA (CAD-Style) === */}
|
||||||
{/* Cota de Altura (h) */}
|
{/* Cota de Altura (h) */}
|
||||||
@@ -248,6 +260,8 @@ function IsolatedRoofModel({
|
|||||||
{/* Rótulo Superior */}
|
{/* Rótulo Superior */}
|
||||||
<Text
|
<Text
|
||||||
position={[0, centerY + 3, 0]}
|
position={[0, centerY + 3, 0]}
|
||||||
|
rotation={[0, -3 * Math.PI / 4, 0]}
|
||||||
|
fontSize={Math.max(0.4, width / 15)}
|
||||||
color={textColor}
|
color={textColor}
|
||||||
anchorX="center"
|
anchorX="center"
|
||||||
anchorY="bottom"
|
anchorY="bottom"
|
||||||
@@ -268,6 +282,7 @@ export default function IsolatedRoof3DViewer({
|
|||||||
cpeLeeward,
|
cpeLeeward,
|
||||||
cpeTop,
|
cpeTop,
|
||||||
forceKN,
|
forceKN,
|
||||||
|
viewMode = 'solid',
|
||||||
}: IsolatedRoof3DInput) {
|
}: IsolatedRoof3DInput) {
|
||||||
const cameraDistance = Math.max(Math.max(width, depth) * 1.3, height * 1.5, 8);
|
const cameraDistance = Math.max(Math.max(width, depth) * 1.3, height * 1.5, 8);
|
||||||
|
|
||||||
@@ -281,8 +296,9 @@ export default function IsolatedRoof3DViewer({
|
|||||||
return (
|
return (
|
||||||
<SceneCanvas moduleId="cobertura_isolada"
|
<SceneCanvas moduleId="cobertura_isolada"
|
||||||
shadows
|
shadows
|
||||||
|
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [cameraDistance, cameraDistance * 0.8, cameraDistance], fov: 40 }}
|
camera={{ position: [-cameraDistance * 0.8, cameraDistance * 0.8, -cameraDistance], fov: 40 }}
|
||||||
fallback={fallback}
|
fallback={fallback}
|
||||||
>
|
>
|
||||||
<ambientLight intensity={0.7} />
|
<ambientLight intensity={0.7} />
|
||||||
@@ -303,7 +319,19 @@ export default function IsolatedRoof3DViewer({
|
|||||||
cpeLeeward={cpeLeeward}
|
cpeLeeward={cpeLeeward}
|
||||||
cpeTop={cpeTop}
|
cpeTop={cpeTop}
|
||||||
forceKN={forceKN}
|
forceKN={forceKN}
|
||||||
|
viewMode={viewMode}
|
||||||
/>
|
/>
|
||||||
|
{viewMode === 'airflow' && (
|
||||||
|
<IsolatedRoofAirflowSystem
|
||||||
|
type={type}
|
||||||
|
theta={theta}
|
||||||
|
height={height}
|
||||||
|
width={width}
|
||||||
|
depth={depth}
|
||||||
|
cpeWindward={cpeWindward}
|
||||||
|
cpeLeeward={cpeLeeward}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<Grid infiniteGrid fadeDistance={cameraDistance * 5} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} />
|
<Grid infiniteGrid fadeDistance={cameraDistance * 5} sectionColor="#94a3b8" cellColor="#cbd5e1" position={[0, -0.01, 0]} />
|
||||||
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} />
|
<OrbitControls makeDefault minPolarAngle={0} maxPolarAngle={Math.PI / 2 - 0.05} />
|
||||||
<Environment preset="city" />
|
<Environment preset="city" />
|
||||||
|
|||||||
@@ -0,0 +1,170 @@
|
|||||||
|
import React, { useRef, useMemo, useEffect } from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
|
||||||
|
interface Particle {
|
||||||
|
position: THREE.Vector3;
|
||||||
|
baseSpeed: number;
|
||||||
|
wobbleSpeed: number;
|
||||||
|
wobbleOffset: number;
|
||||||
|
currentCpe: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface IsolatedRoofAirflowSystemProps {
|
||||||
|
type: 'shed' | 'gable' | 'butterfly';
|
||||||
|
theta: number;
|
||||||
|
height: number;
|
||||||
|
width: number;
|
||||||
|
depth: number;
|
||||||
|
cpeWindward: number;
|
||||||
|
cpeLeeward: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IsolatedRoofAirflowSystem: React.FC<IsolatedRoofAirflowSystemProps> = ({
|
||||||
|
type,
|
||||||
|
theta,
|
||||||
|
height,
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
cpeWindward,
|
||||||
|
cpeLeeward,
|
||||||
|
}) => {
|
||||||
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||||
|
const theme = useCanvasTheme();
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
|
const COUNT = 100;
|
||||||
|
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||||
|
const colorObj = useMemo(() => new THREE.Color(), []);
|
||||||
|
|
||||||
|
const thetaRad = (theta * Math.PI) / 180;
|
||||||
|
const halfDepth = depth / 2;
|
||||||
|
const halfWidth = width / 2;
|
||||||
|
const h_diff = depth * Math.tan(thetaRad);
|
||||||
|
const h_half = halfDepth * Math.tan(thetaRad);
|
||||||
|
|
||||||
|
const particles = useMemo(() => {
|
||||||
|
const arr: Particle[] = [];
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
arr.push({
|
||||||
|
position: new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * width * 3.0,
|
||||||
|
Math.random() * (height + h_diff + 3),
|
||||||
|
-halfDepth - 5 - Math.random() * 20
|
||||||
|
),
|
||||||
|
baseSpeed: 0.075 + Math.random() * 0.05,
|
||||||
|
wobbleSpeed: 0.5 + Math.random() * 1.5,
|
||||||
|
wobbleOffset: Math.random() * Math.PI * 2,
|
||||||
|
currentCpe: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, [width, height, depth, h_diff]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (meshRef.current) {
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
|
||||||
|
meshRef.current.setColorAt(i, colorObj);
|
||||||
|
}
|
||||||
|
meshRef.current.instanceColor!.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}, [isDark, colorObj]);
|
||||||
|
|
||||||
|
useFrame((state) => {
|
||||||
|
if (!meshRef.current) return;
|
||||||
|
const time = state.clock.elapsedTime;
|
||||||
|
|
||||||
|
particles.forEach((p, i) => {
|
||||||
|
const px = p.position.x;
|
||||||
|
const py = p.position.y;
|
||||||
|
const pz = p.position.z;
|
||||||
|
|
||||||
|
let currentSpeed = p.baseSpeed;
|
||||||
|
let localCpe = 0;
|
||||||
|
let deflectY = 0;
|
||||||
|
|
||||||
|
const inX = Math.abs(px) < halfWidth + 1;
|
||||||
|
const inZ = Math.abs(pz) < halfDepth + 2;
|
||||||
|
|
||||||
|
if (inX && inZ) {
|
||||||
|
if (type === 'butterfly') {
|
||||||
|
if (pz < 0) {
|
||||||
|
localCpe = cpeWindward;
|
||||||
|
// Vento acompanha a descida em direção ao vale
|
||||||
|
if (py > height + 0.2) {
|
||||||
|
deflectY -= currentSpeed * 0.3;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
localCpe = cpeLeeward;
|
||||||
|
// Vento bate na segunda água que sobe e é defletido para cima
|
||||||
|
if (py < height + h_half + 2) {
|
||||||
|
deflectY += currentSpeed * 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (pz < 0) {
|
||||||
|
localCpe = cpeWindward;
|
||||||
|
const maxH = type === 'shed' ? height + h_diff : height + h_half;
|
||||||
|
if (py < maxH + 1) {
|
||||||
|
const dist = -pz;
|
||||||
|
const lift = Math.max(0, maxH - py) / (dist + 1);
|
||||||
|
deflectY += lift * currentSpeed * 0.4;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
localCpe = cpeLeeward;
|
||||||
|
if (cpeLeeward < 0) {
|
||||||
|
deflectY -= currentSpeed * 0.15;
|
||||||
|
p.position.y += Math.sin(time * 5 + p.wobbleOffset) * 0.05;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pz > halfDepth && py < height + h_diff) {
|
||||||
|
localCpe = 0.5; // Esteira
|
||||||
|
deflectY -= currentSpeed * 0.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.currentCpe += (localCpe - p.currentCpe) * 0.1;
|
||||||
|
p.position.z += currentSpeed;
|
||||||
|
p.position.y += deflectY;
|
||||||
|
p.position.y += Math.sin(time * p.wobbleSpeed * 10 + p.wobbleOffset) * 0.01;
|
||||||
|
|
||||||
|
if (p.position.z > halfDepth + 10) {
|
||||||
|
p.position.z = -halfDepth - 10 - Math.random() * 5;
|
||||||
|
p.position.y = Math.random() * (height + h_diff + 3);
|
||||||
|
p.position.x = (Math.random() - 0.5) * width * 3.0;
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dummy.position.copy(p.position);
|
||||||
|
dummy.rotation.set(0, 0, 0);
|
||||||
|
dummy.rotateX(Math.PI / 2);
|
||||||
|
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
|
if (p.currentCpe > 0) {
|
||||||
|
colorObj.set(isDark ? '#f87171' : '#ef4444');
|
||||||
|
} else if (p.currentCpe < 0) {
|
||||||
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity);
|
||||||
|
} else {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
|
||||||
|
}
|
||||||
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||||
|
meshRef.current.instanceColor!.needsUpdate = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<instancedMesh ref={meshRef} args={[undefined, undefined, COUNT]}>
|
||||||
|
<cylinderGeometry args={[0.04, 0.04, 1.0, 4]} />
|
||||||
|
<meshBasicMaterial transparent opacity={0.6} depthWrite={false} />
|
||||||
|
</instancedMesh>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,9 +2,11 @@ import React, { useRef } from 'react';
|
|||||||
import * as THREE from 'three';
|
import * as THREE from 'three';
|
||||||
import { useFrame } from '@react-three/fiber';
|
import { useFrame } from '@react-three/fiber';
|
||||||
import { Text, Grid, Environment } from '@react-three/drei';
|
import { Text, Grid, Environment } from '@react-three/drei';
|
||||||
|
import { WindArrow as GlobalWindArrow } from './WindArrow';
|
||||||
import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls';
|
import { ViewerOrbitControls as OrbitControls } from './ViewerOrbitControls';
|
||||||
import SceneCanvas from '../SceneCanvas';
|
import SceneCanvas from '../SceneCanvas';
|
||||||
import { useCanvasTheme } from '@/lib/theme';
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
import { ShelterAirflowSystem } from './ShelterAirflowSystem';
|
||||||
import type { ShelterCondition, OpeningPosition, ShelterWindAngle } from '@/lib/nbr-tables/table-shelters';
|
import type { ShelterCondition, OpeningPosition, ShelterWindAngle } from '@/lib/nbr-tables/table-shelters';
|
||||||
|
|
||||||
export interface Shelter3DProps {
|
export interface Shelter3DProps {
|
||||||
@@ -19,43 +21,9 @@ export interface Shelter3DProps {
|
|||||||
openingPos: OpeningPosition;
|
openingPos: OpeningPosition;
|
||||||
backRange?: [number, number];
|
backRange?: [number, number];
|
||||||
windAngle?: ShelterWindAngle;
|
windAngle?: ShelterWindAngle;
|
||||||
viewMode: '3d' | 'elevation';
|
viewMode: '3d' | 'elevation' | 'airflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
const WindArrow: React.FC<{
|
|
||||||
start: [number, number, number];
|
|
||||||
end: [number, number, number];
|
|
||||||
color?: string;
|
|
||||||
speed?: number;
|
|
||||||
}> = ({ start, end, color = '#38bdf8', speed = 1.5 }) => {
|
|
||||||
const arrowRef = useRef<THREE.Group>(null);
|
|
||||||
|
|
||||||
const quaternion = React.useMemo(() => {
|
|
||||||
const dir = new THREE.Vector3(...end).sub(new THREE.Vector3(...start)).normalize();
|
|
||||||
return new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), dir);
|
|
||||||
}, [start, end]);
|
|
||||||
|
|
||||||
useFrame(({ clock, invalidate }) => {
|
|
||||||
if (arrowRef.current) {
|
|
||||||
const t = (clock.getElapsedTime() * speed) % 1;
|
|
||||||
const x = start[0] + (end[0] - start[0]) * t;
|
|
||||||
const y = start[1] + (end[1] - start[1]) * t;
|
|
||||||
const z = start[2] + (end[2] - start[2]) * t;
|
|
||||||
arrowRef.current.position.set(x, y, z);
|
|
||||||
invalidate();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<group ref={arrowRef} quaternion={quaternion}>
|
|
||||||
<mesh>
|
|
||||||
<coneGeometry args={[0.25, 0.6, 8]} />
|
|
||||||
<meshStandardMaterial color={color} emissive={color} emissiveIntensity={0.6} />
|
|
||||||
</mesh>
|
|
||||||
</group>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
function ShelterModel({
|
function ShelterModel({
|
||||||
condition: _condition,
|
condition: _condition,
|
||||||
depth,
|
depth,
|
||||||
@@ -71,10 +39,13 @@ function ShelterModel({
|
|||||||
viewMode,
|
viewMode,
|
||||||
}: Shelter3DProps) {
|
}: Shelter3DProps) {
|
||||||
const isElevation = viewMode === 'elevation';
|
const isElevation = viewMode === 'elevation';
|
||||||
|
const theme = useCanvasTheme();
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
// Geometria básica
|
// Geometria básica
|
||||||
const halfW = width / 2;
|
const halfW = width / 2;
|
||||||
const halfD = depth / 2;
|
const halfD = depth / 2;
|
||||||
|
const isAirflow = viewMode === 'airflow';
|
||||||
const thetaRad = (theta * Math.PI) / 180;
|
const thetaRad = (theta * Math.PI) / 180;
|
||||||
const slopeRise = depth * Math.tan(thetaRad);
|
const slopeRise = depth * Math.tan(thetaRad);
|
||||||
|
|
||||||
@@ -161,50 +132,51 @@ function ShelterModel({
|
|||||||
{/* Parede Lateral Direita (X = +halfW) */}
|
{/* Parede Lateral Direita (X = +halfW) */}
|
||||||
{renderWallWithGap(depth, height, rightClosure, 'distributed', Math.PI / 2, [halfW, 0, 0])}
|
{renderWallWithGap(depth, height, rightClosure, 'distributed', Math.PI / 2, [halfW, 0, 0])}
|
||||||
|
|
||||||
{/* 4. Linhas de Corrente e Vetores de Vento (Ilustrativo e Didático) */}
|
{/* Seta de Vento Global (Sempre visível para não pular de posição) */}
|
||||||
<group position={[0, 0, 0]} rotation={[0, -((windAngle || 0) * Math.PI) / 180, 0]}>
|
<group>
|
||||||
{/* Vento de entrada (Barlavento) entrando em direção ao abrigo */}
|
<GlobalWindArrow
|
||||||
<WindArrow start={[0, height * 0.5, -halfD - 4]} end={[0, height * 0.5, -halfD + 1]} color="#38bdf8" speed={1.2} />
|
target={[-halfW * 1.2, height * 1.5 - 1.5, -halfD - 5 + 2]}
|
||||||
<WindArrow start={[-halfW * 0.5, height * 0.7, -halfD - 4]} end={[-halfW * 0.5, height * 0.7, -halfD + 1]} color="#38bdf8" speed={1.4} />
|
direction={[0, 0, 1]}
|
||||||
<WindArrow start={[halfW * 0.5, height * 0.4, -halfD - 4]} end={[halfW * 0.5, height * 0.4, -halfD + 1]} color="#38bdf8" speed={1.3} />
|
scale={4}
|
||||||
|
/>
|
||||||
{/* Linhas de corrente sobre o telhado */}
|
<Text
|
||||||
<WindArrow start={[0, height + 1, -halfD - 3]} end={[0, height + slopeRise + 1.5, halfD + 2]} color="#60a5fa" speed={1.6} />
|
position={[-halfW * 1.2, height * 1.5, -halfD - 5]}
|
||||||
|
rotation={[0, Math.PI / 4, 0]}
|
||||||
{/* Fresta na parede de fundo: se houver abertura no topo, mostrar vento escapando pela fresta superior */}
|
fontSize={1.2}
|
||||||
{backClosure > 0 && backClosure < 100 && (openingPos === 'top' || (backRange && backRange[1] < 98)) && (
|
color={isDark ? '#60a5fa' : '#3b82f6'}
|
||||||
<>
|
anchorX="center"
|
||||||
<WindArrow start={[0, height * 0.85, 0]} end={[0, height * 0.95, halfD + 2.5]} color="#34d399" speed={1.8} />
|
anchorY="bottom"
|
||||||
<Text position={[0, height + 0.35, halfD]} rotation={[0, Math.PI, 0]} fontSize={0.4} color="#34d399">
|
>
|
||||||
Alívio (Fresta Topo)
|
Vento
|
||||||
</Text>
|
</Text>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Fresta na base */}
|
|
||||||
{backClosure > 0 && backClosure < 100 && (openingPos === 'bottom' || (backRange && backRange[0] > 2)) && (
|
|
||||||
<>
|
|
||||||
<WindArrow start={[0, 0.4, 0]} end={[0, 0.4, halfD + 2.5]} color="#facc15" speed={1.8} />
|
|
||||||
<Text position={[0, 1.2, halfD]} rotation={[0, Math.PI, 0]} fontSize={0.4} color="#facc15">
|
|
||||||
Vão Livre Inferior
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</group>
|
</group>
|
||||||
|
|
||||||
|
{/* Partículas de Ar */}
|
||||||
|
{isAirflow && (
|
||||||
|
<group>
|
||||||
|
<ShelterAirflowSystem
|
||||||
|
width={width}
|
||||||
|
depth={depth}
|
||||||
|
height={height}
|
||||||
|
theta={theta}
|
||||||
|
/>
|
||||||
|
</group>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Textos Informativos 3D - Virados para quem olha da frente (-Z) */}
|
{/* Textos Informativos 3D - Virados para quem olha da frente (-Z) */}
|
||||||
<Text position={[0, height + slopeRise + 0.8, 0]} rotation={[0, Math.PI, 0]} fontSize={0.45} color="#e2e8f0">
|
{!isAirflow && (
|
||||||
{isElevation ? 'CORTE A-A (Elevação)' : 'Abrigo / Pórtico (NBR 6123)'}
|
<>
|
||||||
</Text>
|
<Text position={[0, height + slopeRise + 1.2, 0]} rotation={[0, Math.PI, 0]} fontSize={0.45} color="#e2e8f0">
|
||||||
<Text position={[0, 0.3, -halfD - 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.35} color="#38bdf8">
|
{isElevation ? 'CORTE A-A (Elevação)' : 'Abrigo / Pórtico (NBR 6123)'}
|
||||||
{`VENTO ${windAngle === 0 ? 'FRONTAL (0°)' : windAngle === 45 ? 'OBLÍQUO (45°)' : 'LATERAL (90°)'}`}
|
</Text>
|
||||||
</Text>
|
<Text position={[0, height + slopeRise + 0.5, 0]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#f43f5e">
|
||||||
<Text position={[0, height + slopeRise + 0.25, 0]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#f43f5e">
|
Arrancamento [+Fz ↑]
|
||||||
Arrancamento [+Fz ↑]
|
</Text>
|
||||||
</Text>
|
<Text position={[0, height * 0.5, halfD + 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#fb923c">
|
||||||
<Text position={[0, height * 0.5, halfD + 1.2]} rotation={[0, Math.PI, 0]} fontSize={0.32} color="#fb923c">
|
Empuxo no Fundo [+Fx →]
|
||||||
Empuxo no Fundo [+Fx →]
|
</Text>
|
||||||
</Text>
|
</>
|
||||||
|
)}
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -212,13 +184,14 @@ function ShelterModel({
|
|||||||
export function Shelter3D(props: Shelter3DProps) {
|
export function Shelter3D(props: Shelter3DProps) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
const { width, depth, height } = props;
|
const { width, depth, height, viewMode } = props;
|
||||||
const dist = Math.max(width, depth) * 1.8 + 8;
|
const dist = Math.max(width, depth) * 1.8 + 8;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SceneCanvas
|
<SceneCanvas
|
||||||
moduleId="shelter"
|
moduleId="shelter"
|
||||||
shadows
|
shadows
|
||||||
|
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [dist * 0.75, height * 1.1, -dist * 0.85], fov: 45 }}
|
camera={{ position: [dist * 0.75, height * 1.1, -dist * 0.85], fov: 45 }}
|
||||||
fallback={<div className="flex items-center justify-center h-full text-muted-foreground">3D indisponível</div>}
|
fallback={<div className="flex items-center justify-center h-full text-muted-foreground">3D indisponível</div>}
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import React, { useRef, useMemo, useEffect } from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
|
||||||
|
interface Particle {
|
||||||
|
position: THREE.Vector3;
|
||||||
|
baseSpeed: number;
|
||||||
|
wobbleSpeed: number;
|
||||||
|
wobbleOffset: number;
|
||||||
|
currentCpe: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ShelterAirflowSystemProps {
|
||||||
|
width: number;
|
||||||
|
depth: number;
|
||||||
|
height: number;
|
||||||
|
theta: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShelterAirflowSystem: React.FC<ShelterAirflowSystemProps> = ({
|
||||||
|
width,
|
||||||
|
depth,
|
||||||
|
height,
|
||||||
|
theta,
|
||||||
|
}) => {
|
||||||
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||||
|
const theme = useCanvasTheme();
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
|
const COUNT = 100;
|
||||||
|
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||||
|
const colorObj = useMemo(() => new THREE.Color(), []);
|
||||||
|
|
||||||
|
const halfWidth = width / 2;
|
||||||
|
const halfDepth = depth / 2;
|
||||||
|
const thetaRad = (theta * Math.PI) / 180;
|
||||||
|
const h_diff = depth * Math.tan(thetaRad);
|
||||||
|
|
||||||
|
const particles = useMemo(() => {
|
||||||
|
const arr: Particle[] = [];
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
arr.push({
|
||||||
|
position: new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * width * 3,
|
||||||
|
Math.random() * (height + h_diff + 5),
|
||||||
|
-halfDepth - 10 - Math.random() * 20
|
||||||
|
),
|
||||||
|
baseSpeed: 0.075 + Math.random() * 0.05,
|
||||||
|
wobbleSpeed: 0.5 + Math.random() * 1.5,
|
||||||
|
wobbleOffset: Math.random() * Math.PI * 2,
|
||||||
|
currentCpe: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, [width, depth, height, h_diff]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (meshRef.current) {
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
|
||||||
|
meshRef.current.setColorAt(i, colorObj);
|
||||||
|
}
|
||||||
|
meshRef.current.instanceColor!.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}, [isDark, colorObj]);
|
||||||
|
|
||||||
|
useFrame((state) => {
|
||||||
|
if (!meshRef.current) return;
|
||||||
|
const time = state.clock.elapsedTime;
|
||||||
|
|
||||||
|
particles.forEach((p, i) => {
|
||||||
|
const px = p.position.x;
|
||||||
|
const py = p.position.y;
|
||||||
|
const pz = p.position.z;
|
||||||
|
|
||||||
|
let currentSpeed = p.baseSpeed;
|
||||||
|
let localCpe = 0;
|
||||||
|
let deflectY = 0;
|
||||||
|
|
||||||
|
const inX = Math.abs(px) < halfWidth + 1;
|
||||||
|
const inZ = Math.abs(pz) < halfDepth + 1;
|
||||||
|
|
||||||
|
if (inX && inZ) {
|
||||||
|
localCpe = 0.5;
|
||||||
|
// Teto inclinado deflete o vento
|
||||||
|
if (py < height + h_diff + 1 && py > height - 1) {
|
||||||
|
const dist = -pz;
|
||||||
|
const lift = Math.max(0, height + h_diff - py) / (dist + 1);
|
||||||
|
deflectY += lift * currentSpeed * 0.4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.currentCpe += (localCpe - p.currentCpe) * 0.1;
|
||||||
|
p.position.z += currentSpeed;
|
||||||
|
p.position.y += deflectY;
|
||||||
|
p.position.y += Math.sin(time * p.wobbleSpeed * 10 + p.wobbleOffset) * 0.01;
|
||||||
|
|
||||||
|
if (p.position.z > halfDepth + 10) {
|
||||||
|
p.position.z = -halfDepth - 10 - Math.random() * 5;
|
||||||
|
p.position.y = Math.random() * (height + h_diff + 5);
|
||||||
|
p.position.x = (Math.random() - 0.5) * width * 3;
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dummy.position.copy(p.position);
|
||||||
|
dummy.rotation.set(0, 0, 0);
|
||||||
|
dummy.rotateX(Math.PI / 2);
|
||||||
|
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
|
if (p.currentCpe > 0) {
|
||||||
|
colorObj.set(isDark ? '#f87171' : '#ef4444');
|
||||||
|
} else if (p.currentCpe < 0) {
|
||||||
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity);
|
||||||
|
} else {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
|
||||||
|
}
|
||||||
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||||
|
meshRef.current.instanceColor!.needsUpdate = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<instancedMesh ref={meshRef} args={[undefined, undefined, COUNT]}>
|
||||||
|
<cylinderGeometry args={[0.04, 0.04, 1.0, 4]} />
|
||||||
|
<meshBasicMaterial transparent opacity={0.6} depthWrite={false} />
|
||||||
|
</instancedMesh>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -152,16 +152,12 @@ export function SignAirflowSystem({ length, height, groundClearance, alpha, cf }
|
|||||||
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
if (p.currentCpe > 0) {
|
if (p.currentCpe > 0) {
|
||||||
colorObj
|
colorObj.set(isDark ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão)
|
||||||
.set(isDark ? '#ef4444' : '#dc2626')
|
|
||||||
.lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe));
|
|
||||||
} else if (p.currentCpe < 0) {
|
} else if (p.currentCpe < 0) {
|
||||||
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
colorObj
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
|
||||||
.set(isDark ? '#7dd3fc' : '#38bdf8')
|
|
||||||
.lerp(new THREE.Color(isDark ? '#1e40af' : '#1d4ed8'), intensity);
|
|
||||||
} else {
|
} else {
|
||||||
colorObj.set(isDark ? '#f1f5f9' : '#64748b');
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
|
||||||
}
|
}
|
||||||
meshRef.current!.setColorAt(i, colorObj);
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import * as THREE from 'three';
|
|||||||
import SceneCanvas from '../SceneCanvas';
|
import SceneCanvas from '../SceneCanvas';
|
||||||
import { WindArrow } from './WindArrow';
|
import { WindArrow } from './WindArrow';
|
||||||
import FallbackDiagram from '../FallbackDiagram';
|
import FallbackDiagram from '../FallbackDiagram';
|
||||||
|
import { TowerAirflowSystem } from './TowerAirflowSystem';
|
||||||
|
|
||||||
export interface Tower3DInput {
|
export interface Tower3DInput {
|
||||||
/** Forma da seção */
|
/** Forma da seção */
|
||||||
@@ -23,6 +24,8 @@ export interface Tower3DInput {
|
|||||||
alphaWind: 0 | 45 | 90;
|
alphaWind: 0 | 45 | 90;
|
||||||
/** Força total estimada na torre (kN) */
|
/** Força total estimada na torre (kN) */
|
||||||
forceKN: number;
|
forceKN: number;
|
||||||
|
/** Modo de visualização */
|
||||||
|
viewMode?: 'solid' | 'airflow';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,9 +139,11 @@ function TowerModel({
|
|||||||
phi,
|
phi,
|
||||||
alphaWind,
|
alphaWind,
|
||||||
forceKN,
|
forceKN,
|
||||||
|
viewMode = 'solid',
|
||||||
}: Tower3DInput) {
|
}: Tower3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
const isAirflow = viewMode === 'airflow';
|
||||||
|
|
||||||
const geometry = useMemo(
|
const geometry = useMemo(
|
||||||
() => buildTowerGeometry(section, panels, baseWidth, height),
|
() => buildTowerGeometry(section, panels, baseWidth, height),
|
||||||
@@ -201,11 +206,32 @@ function TowerModel({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
{/* Seta de Vento única atingindo o meio da torre */}
|
{/* Seta de Vento */}
|
||||||
<WindArrow
|
{isAirflow ? (
|
||||||
target={[0, height / 2, 0]}
|
<group>
|
||||||
direction={forceDir}
|
<WindArrow
|
||||||
/>
|
target={[-baseWidth * 1.5, height / 2, -baseWidth * 1.5]}
|
||||||
|
direction={[0, 0, 1]}
|
||||||
|
scale={4}
|
||||||
|
/>
|
||||||
|
<Text
|
||||||
|
position={[-baseWidth * 1.5, height / 2 + 1.5, -baseWidth * 1.5 - 2]}
|
||||||
|
rotation={[0, Math.PI / 4, 0]}
|
||||||
|
fontSize={1.2}
|
||||||
|
color={isDark ? '#60a5fa' : '#3b82f6'}
|
||||||
|
anchorX="center"
|
||||||
|
anchorY="bottom"
|
||||||
|
>
|
||||||
|
Vento
|
||||||
|
</Text>
|
||||||
|
<TowerAirflowSystem baseWidth={baseWidth} height={height} phi={phi} />
|
||||||
|
</group>
|
||||||
|
) : (
|
||||||
|
<WindArrow
|
||||||
|
target={[0, height / 2, 0]}
|
||||||
|
direction={forceDir}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Labels indicativos */}
|
{/* Labels indicativos */}
|
||||||
<mesh position={[baseWidth / 2 + 0.5, 0.5, 0]}>
|
<mesh position={[baseWidth / 2 + 0.5, 0.5, 0]}>
|
||||||
@@ -228,7 +254,7 @@ function TowerModel({
|
|||||||
export default function Tower3DViewer(input: Tower3DInput) {
|
export default function Tower3DViewer(input: Tower3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
const { baseWidth, height } = input;
|
const { baseWidth, height, viewMode = 'solid' } = input;
|
||||||
const dist = Math.max(baseWidth * 3, height * 1.2);
|
const dist = Math.max(baseWidth * 3, height * 1.2);
|
||||||
|
|
||||||
const fallback = (
|
const fallback = (
|
||||||
@@ -241,13 +267,14 @@ export default function Tower3DViewer(input: Tower3DInput) {
|
|||||||
return (
|
return (
|
||||||
<SceneCanvas moduleId="torre"
|
<SceneCanvas moduleId="torre"
|
||||||
shadows
|
shadows
|
||||||
|
frameloop={viewMode === 'airflow' ? 'always' : 'demand'}
|
||||||
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
gl={{ preserveDrawingBuffer: true, antialias: true }}
|
||||||
camera={{ position: [dist, height * 0.6, dist], fov: 45 }}
|
camera={{ position: [dist, height * 0.6, dist], fov: 45 }}
|
||||||
fallback={fallback}
|
fallback={fallback}
|
||||||
>
|
>
|
||||||
<ambientLight intensity={0.6} />
|
<ambientLight intensity={0.6} />
|
||||||
<directionalLight position={[dist, height, dist]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
|
<directionalLight position={[dist, height, dist]} intensity={1.2} castShadow shadow-mapSize-width={1024} shadow-mapSize-height={1024} />
|
||||||
<TowerModel {...input} />
|
<TowerModel {...input} viewMode={viewMode} />
|
||||||
<Grid
|
<Grid
|
||||||
infiniteGrid
|
infiniteGrid
|
||||||
fadeDistance={height * 2}
|
fadeDistance={height * 2}
|
||||||
|
|||||||
@@ -0,0 +1,130 @@
|
|||||||
|
import React, { useRef, useMemo, useEffect } from 'react';
|
||||||
|
import * as THREE from 'three';
|
||||||
|
import { useFrame } from '@react-three/fiber';
|
||||||
|
import { useCanvasTheme } from '@/lib/theme';
|
||||||
|
|
||||||
|
interface Particle {
|
||||||
|
position: THREE.Vector3;
|
||||||
|
baseSpeed: number;
|
||||||
|
wobbleSpeed: number;
|
||||||
|
wobbleOffset: number;
|
||||||
|
currentCpe: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TowerAirflowSystemProps {
|
||||||
|
baseWidth: number;
|
||||||
|
height: number;
|
||||||
|
phi: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TowerAirflowSystem: React.FC<TowerAirflowSystemProps> = ({
|
||||||
|
baseWidth,
|
||||||
|
height,
|
||||||
|
phi,
|
||||||
|
}) => {
|
||||||
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||||
|
const theme = useCanvasTheme();
|
||||||
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
|
const COUNT = 100;
|
||||||
|
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||||
|
const colorObj = useMemo(() => new THREE.Color(), []);
|
||||||
|
|
||||||
|
const halfWidth = baseWidth / 2;
|
||||||
|
|
||||||
|
const particles = useMemo(() => {
|
||||||
|
const arr: Particle[] = [];
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
arr.push({
|
||||||
|
position: new THREE.Vector3(
|
||||||
|
(Math.random() - 0.5) * baseWidth * 3,
|
||||||
|
Math.random() * (height + 5),
|
||||||
|
-halfWidth - 10 - Math.random() * 20
|
||||||
|
),
|
||||||
|
baseSpeed: 0.075 + Math.random() * 0.05,
|
||||||
|
wobbleSpeed: 0.5 + Math.random() * 1.5,
|
||||||
|
wobbleOffset: Math.random() * Math.PI * 2,
|
||||||
|
currentCpe: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
}, [baseWidth, height]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (meshRef.current) {
|
||||||
|
for (let i = 0; i < COUNT; i++) {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
|
||||||
|
meshRef.current.setColorAt(i, colorObj);
|
||||||
|
}
|
||||||
|
meshRef.current.instanceColor!.needsUpdate = true;
|
||||||
|
}
|
||||||
|
}, [isDark, colorObj]);
|
||||||
|
|
||||||
|
useFrame((state) => {
|
||||||
|
if (!meshRef.current) return;
|
||||||
|
const time = state.clock.elapsedTime;
|
||||||
|
|
||||||
|
particles.forEach((p, i) => {
|
||||||
|
const px = p.position.x;
|
||||||
|
const py = p.position.y;
|
||||||
|
const pz = p.position.z;
|
||||||
|
|
||||||
|
let currentSpeed = p.baseSpeed;
|
||||||
|
let localCpe = 0;
|
||||||
|
let deflectX = 0;
|
||||||
|
|
||||||
|
const inX = Math.abs(px) < halfWidth + 2;
|
||||||
|
const inZ = Math.abs(pz) < halfWidth + 2;
|
||||||
|
|
||||||
|
if (inX && inZ && py < height) {
|
||||||
|
localCpe = 0.8;
|
||||||
|
// Se a torre é densa (phi alto), o ar tenta desviar pelas laterais
|
||||||
|
if (pz < 0) {
|
||||||
|
const dist = halfWidth - Math.abs(px);
|
||||||
|
if (dist > 0) {
|
||||||
|
deflectX += (px > 0 ? 1 : -1) * currentSpeed * phi * 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.currentCpe += (localCpe - p.currentCpe) * 0.1;
|
||||||
|
p.position.z += currentSpeed;
|
||||||
|
p.position.x += deflectX;
|
||||||
|
p.position.y += Math.sin(time * p.wobbleSpeed * 10 + p.wobbleOffset) * 0.01;
|
||||||
|
|
||||||
|
if (p.position.z > halfWidth + 10) {
|
||||||
|
p.position.z = -halfWidth - 10 - Math.random() * 5;
|
||||||
|
p.position.y = Math.random() * (height + 5);
|
||||||
|
p.position.x = (Math.random() - 0.5) * baseWidth * 3;
|
||||||
|
p.currentCpe = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dummy.position.copy(p.position);
|
||||||
|
dummy.rotation.set(0, 0, 0);
|
||||||
|
dummy.rotateX(Math.PI / 2);
|
||||||
|
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
|
||||||
|
dummy.updateMatrix();
|
||||||
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||||
|
|
||||||
|
if (p.currentCpe > 0) {
|
||||||
|
colorObj.set(isDark ? '#f87171' : '#ef4444');
|
||||||
|
} else if (p.currentCpe < 0) {
|
||||||
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity);
|
||||||
|
} else {
|
||||||
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6');
|
||||||
|
}
|
||||||
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
|
});
|
||||||
|
|
||||||
|
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||||
|
meshRef.current.instanceColor!.needsUpdate = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<instancedMesh ref={meshRef} args={[undefined, undefined, COUNT]}>
|
||||||
|
<cylinderGeometry args={[0.04, 0.04, 1.0, 4]} />
|
||||||
|
<meshBasicMaterial transparent opacity={0.6} depthWrite={false} />
|
||||||
|
</instancedMesh>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -175,6 +175,30 @@ function VaultModel({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDire
|
|||||||
<meshStandardMaterial color={isDark ? "#334155" : "#cbd5e1"} opacity={isAirflow ? 0.25 : 0.7} transparent side={THREE.DoubleSide} roughness={0.5} />
|
<meshStandardMaterial color={isDark ? "#334155" : "#cbd5e1"} opacity={isAirflow ? 0.25 : 0.7} transparent side={THREE.DoubleSide} roughness={0.5} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
|
||||||
|
{/* Seta indicativa de direção do vento */}
|
||||||
|
<group position={[-span / 2 - 2.5, rise / 2, length / 2]} rotation={[0, 0, -Math.PI / 2]}>
|
||||||
|
<mesh castShadow>
|
||||||
|
<coneGeometry args={[0.3, 0.8, 16]} />
|
||||||
|
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
||||||
|
</mesh>
|
||||||
|
<mesh position={[0, -0.6, 0]} castShadow>
|
||||||
|
<cylinderGeometry args={[0.1, 0.1, 1.2, 16]} />
|
||||||
|
<meshStandardMaterial color="#3b82f6" roughness={0.3} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
|
||||||
|
{/* Texto Vento */}
|
||||||
|
<Text
|
||||||
|
position={[-span / 2 - 2.5, rise / 2 + 1.2, length / 2]}
|
||||||
|
rotation={[0, 0, 0]}
|
||||||
|
fontSize={1.2}
|
||||||
|
color="#3b82f6"
|
||||||
|
anchorX="center"
|
||||||
|
anchorY="bottom"
|
||||||
|
>
|
||||||
|
Vento
|
||||||
|
</Text>
|
||||||
|
|
||||||
{/* Rótulo de dimensões */}
|
{/* Rótulo de dimensões */}
|
||||||
<Text
|
<Text
|
||||||
position={[0, rise + 1.5, length / 2]}
|
position={[0, rise + 1.5, length / 2]}
|
||||||
|
|||||||
@@ -103,6 +103,8 @@ export function VaultAirflowSystem({ span, length, rise, viewDirection = 'perpen
|
|||||||
p.currentCpe = 0.7; // Compressão a barlavento
|
p.currentCpe = 0.7; // Compressão a barlavento
|
||||||
} else if (px >= -span * 0.15 && px <= span * 0.15) {
|
} else if (px >= -span * 0.15 && px <= span * 0.15) {
|
||||||
p.currentCpe = -0.9; // Forte sucção no topo
|
p.currentCpe = -0.9; // Forte sucção no topo
|
||||||
|
} else if (px > span / 4) {
|
||||||
|
p.currentCpe = 0.5; // Sotavento / Esteira (agora positivo para ficar vermelho)
|
||||||
} else {
|
} else {
|
||||||
p.currentCpe = -0.5; // Sucção a sotavento
|
p.currentCpe = -0.5; // Sucção a sotavento
|
||||||
}
|
}
|
||||||
@@ -170,16 +172,12 @@ export function VaultAirflowSystem({ span, length, rise, viewDirection = 'perpen
|
|||||||
|
|
||||||
// Colorir
|
// Colorir
|
||||||
if (p.currentCpe > 0) {
|
if (p.currentCpe > 0) {
|
||||||
colorObj
|
colorObj.set(isDark ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão)
|
||||||
.set(isDark ? '#ef4444' : '#dc2626')
|
|
||||||
.lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe));
|
|
||||||
} else if (p.currentCpe < 0) {
|
} else if (p.currentCpe < 0) {
|
||||||
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||||
colorObj
|
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
|
||||||
.set(isDark ? '#7dd3fc' : '#38bdf8')
|
|
||||||
.lerp(new THREE.Color(isDark ? '#1e40af' : '#1d4ed8'), intensity);
|
|
||||||
} else {
|
} else {
|
||||||
colorObj.set(isDark ? '#f1f5f9' : '#64748b');
|
colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
|
||||||
}
|
}
|
||||||
meshRef.current!.setColorAt(i, colorObj);
|
meshRef.current!.setColorAt(i, colorObj);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -102,6 +102,22 @@ export function calculateIsolatedGableRoof(input: IsolatedGableRoofInput): Isola
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cobertura isolada em V (Borboleta).
|
||||||
|
* Extrapolação baseada na inversão geométrica das duas águas.
|
||||||
|
* A NBR 6123 não possui tabela explícita, portanto applies = false.
|
||||||
|
*/
|
||||||
|
export function calculateIsolatedButterflyRoof(input: IsolatedGableRoofInput): IsolatedGableRoofResult {
|
||||||
|
const base = calculateIsolatedGableRoof(input);
|
||||||
|
return {
|
||||||
|
// Barlavento (descendo): forte sucção no bordo de ataque
|
||||||
|
cpb: { cpb1: -Math.abs(base.cpa.cpa1), cpb2: -Math.abs(base.cpa.cpa2) },
|
||||||
|
// Sotavento (subindo): atua como uma barreira, sofrendo pressão
|
||||||
|
cpa: { cpa1: Math.abs(base.cpb.cpb1), cpa2: Math.abs(base.cpb.cpb2) },
|
||||||
|
applies: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
/** Força de atrito na cobertura isolada: F = 0,05 · q · a · b (sec. 7.2.2) */
|
/** Força de atrito na cobertura isolada: F = 0,05 · q · a · b (sec. 7.2.2) */
|
||||||
export function frictionForceIsolatedRoof(q: number, a: number, b: number): number {
|
export function frictionForceIsolatedRoof(q: number, a: number, b: number): number {
|
||||||
return Number((0.05 * q * a * b).toFixed(3));
|
return Number((0.05 * q * a * b).toFixed(3));
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { Wind, Box } from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
|
|
||||||
import { useHydrationStore } from '@/store/hydrationStore';
|
import { useHydrationStore } from '@/store/hydrationStore';
|
||||||
@@ -47,6 +48,7 @@ const BridgeModule: React.FC = () => {
|
|||||||
const [fv, setFv] = React.useState(0.6);
|
const [fv, setFv] = React.useState(0.6);
|
||||||
const [vk, setVk] = React.useState(45);
|
const [vk, setVk] = React.useState(45);
|
||||||
const [alpha, setAlpha] = React.useState(0);
|
const [alpha, setAlpha] = React.useState(0);
|
||||||
|
const [viewMode, setViewMode] = useState<'solid' | 'airflow'>('solid');
|
||||||
const [vf, setVf] = React.useState(100); // Velocidade crítica de flutter da seção
|
const [vf, setVf] = React.useState(100); // Velocidade crítica de flutter da seção
|
||||||
|
|
||||||
const classification = useMemo(
|
const classification = useMemo(
|
||||||
@@ -214,6 +216,24 @@ const BridgeModule: React.FC = () => {
|
|||||||
Classe {classification.bridgeClass}
|
Classe {classification.bridgeClass}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute top-4 right-16 z-10">
|
||||||
|
<div className="flex bg-background/80 backdrop-blur-sm border border-border/80 rounded-md shadow-sm p-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('solid')}
|
||||||
|
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'solid' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
|
||||||
|
>
|
||||||
|
<Box className="w-3.5 h-3.5 inline-block mr-1.5" />
|
||||||
|
Sólido
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('airflow')}
|
||||||
|
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'airflow' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
|
||||||
|
>
|
||||||
|
<Wind className="w-3.5 h-3.5 inline-block mr-1.5" />
|
||||||
|
Fluxo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="absolute top-4 right-4 z-10">
|
<div className="absolute top-4 right-4 z-10">
|
||||||
<EducationalManual type="bridge" params={{ alpha }} />
|
<EducationalManual type="bridge" params={{ alpha }} />
|
||||||
</div>
|
</div>
|
||||||
@@ -227,6 +247,7 @@ const BridgeModule: React.FC = () => {
|
|||||||
fxPerLength={forces.fxPerLength}
|
fxPerLength={forces.fxPerLength}
|
||||||
fzPerLength={forces.fzPerLength}
|
fzPerLength={forces.fzPerLength}
|
||||||
alpha={alpha}
|
alpha={alpha}
|
||||||
|
viewMode={viewMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useMemo, useState } from 'react';
|
||||||
|
import { Box, Wind } from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Separator } from '@/components/ui/separator';
|
import { Separator } from '@/components/ui/separator';
|
||||||
import { useWindStore } from '@/store/appStore';
|
import { useWindStore } from '@/store/appStore';
|
||||||
import { calculateIsolatedShedRoof, calculateIsolatedGableRoof, frictionForceIsolatedRoof } from '@/lib/nbr-tables/table-24-25';
|
import { calculateIsolatedShedRoof, calculateIsolatedGableRoof, calculateIsolatedButterflyRoof, frictionForceIsolatedRoof } from '@/lib/nbr-tables/table-24-25';
|
||||||
import IsolatedRoof3DViewer from '@/components/three/IsolatedRoof3D';
|
import IsolatedRoof3DViewer from '@/components/three/IsolatedRoof3D';
|
||||||
import SceneCapturePanel from '../components/SceneCapturePanel';
|
import SceneCapturePanel from '../components/SceneCapturePanel';
|
||||||
import ExportMenu from '../components/ExportMenu';
|
import ExportMenu from '../components/ExportMenu';
|
||||||
@@ -17,17 +18,22 @@ import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generi
|
|||||||
|
|
||||||
const IsolatedRoofModule: React.FC = () => {
|
const IsolatedRoofModule: React.FC = () => {
|
||||||
const { q } = useWindStore();
|
const { q } = useWindStore();
|
||||||
const [type, setType] = React.useState<'shed' | 'gable'>('shed');
|
const [type, setType] = React.useState<'shed' | 'gable' | 'butterfly'>('shed');
|
||||||
const [theta, setTheta] = React.useState(15);
|
const [theta, setTheta] = React.useState(15);
|
||||||
const [height, setHeight] = React.useState(2);
|
const [height, setHeight] = React.useState(2);
|
||||||
const [width, setWidth] = React.useState(10);
|
const [width, setWidth] = useState(10);
|
||||||
const [depth, setDepth] = React.useState(10);
|
const [depth, setDepth] = useState(10);
|
||||||
|
const [viewMode, setViewMode] = useState<'solid' | 'airflow'>('solid');
|
||||||
|
|
||||||
const result = useMemo(() => {
|
const result = useMemo(() => {
|
||||||
if (type === 'shed') {
|
if (type === 'shed') {
|
||||||
const r = calculateIsolatedShedRoof({ theta, height, depth });
|
const r = calculateIsolatedShedRoof({ theta, height, depth });
|
||||||
return { ...r, cpb: { cpb1: 0, cpb2: 0 }, cpa: { cpa1: 0, cpa2: 0 } };
|
return { ...r, cpb: { cpb1: 0, cpb2: 0 }, cpa: { cpa1: 0, cpa2: 0 } };
|
||||||
}
|
}
|
||||||
|
if (type === 'butterfly') {
|
||||||
|
const b = calculateIsolatedButterflyRoof({ theta, height, depth });
|
||||||
|
return { ...b, cph1: { high: 0, low: 0 }, cph2: { high: 0, low: 0 } };
|
||||||
|
}
|
||||||
const g = calculateIsolatedGableRoof({ theta, height, depth });
|
const g = calculateIsolatedGableRoof({ theta, height, depth });
|
||||||
return { ...g, cph1: { high: 0, low: 0 }, cph2: { high: 0, low: 0 } };
|
return { ...g, cph1: { high: 0, low: 0 }, cph2: { high: 0, low: 0 } };
|
||||||
}, [type, theta, height, depth]);
|
}, [type, theta, height, depth]);
|
||||||
@@ -79,12 +85,15 @@ const IsolatedRoofModule: React.FC = () => {
|
|||||||
title: 'Geometria da Cobertura Isolada',
|
title: 'Geometria da Cobertura Isolada',
|
||||||
type: 'grid',
|
type: 'grid',
|
||||||
gridItems: [
|
gridItems: [
|
||||||
{ label: 'Tipo', value: type === 'shed' ? 'Uma água' : 'Duas águas' },
|
{ label: 'Tipo', value: type === 'shed' ? 'Uma água' : type === 'gable' ? 'Duas águas' : 'Borboleta (Extrapolação)' },
|
||||||
{ label: 'Inclinação (θ)', value: `${theta}°` },
|
{ label: 'Inclinação (θ)', value: `${theta}°` },
|
||||||
{ label: 'Largura (b)', value: `${width} m` },
|
{ label: 'Largura (b)', value: `${width} m` },
|
||||||
{ label: 'Profundidade (ℓ)', value: `${depth} m` },
|
{ label: 'Profundidade (ℓ)', value: `${depth} m` },
|
||||||
{ label: 'Altura livre (h)', value: `${height} m` },
|
{ label: 'Altura livre (h)', value: `${height} m` },
|
||||||
{ label: 'Aplicável à norma', value: result.applies ? 'Sim' : 'Não' },
|
{
|
||||||
|
label: 'Aplicável à norma',
|
||||||
|
value: result.applies ? 'Sim' : type === 'butterfly' ? 'Extrapolado (Não há tabela)' : 'Não'
|
||||||
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -116,7 +125,7 @@ const IsolatedRoofModule: React.FC = () => {
|
|||||||
{
|
{
|
||||||
title: '1. Coeficientes de Pressão Líquida (Cpn) e Carregamentos',
|
title: '1. Coeficientes de Pressão Líquida (Cpn) e Carregamentos',
|
||||||
formula: 'Cpn = Cp_superior − Cp_inferior (Tabelas 24 e 25 NBR 6123)',
|
formula: 'Cpn = Cp_superior − Cp_inferior (Tabelas 24 e 25 NBR 6123)',
|
||||||
calculation: `Para inclinação θ = ${theta}° (${type === 'shed' ? 'Uma água' : 'Duas águas'}), os coeficientes são definidos por zona e por carregamento máximo/mínimo.`,
|
calculation: `Para inclinação θ = ${theta}° (${type === 'shed' ? 'Uma água' : type === 'gable' ? 'Duas águas' : 'Borboleta'}), os coeficientes são definidos por zona e por carregamento máximo/mínimo.`,
|
||||||
result: `θ = ${theta}° (Tab. ${type === 'shed' ? '24' : '25'})`,
|
result: `θ = ${theta}° (Tab. ${type === 'shed' ? '24' : '25'})`,
|
||||||
note: 'Coberturas isoladas estão sujeitas a descolamento violento de fluxo, exigindo verificação para sucção (arrancamento) e compressão.',
|
note: 'Coberturas isoladas estão sujeitas a descolamento violento de fluxo, exigindo verificação para sucção (arrancamento) e compressão.',
|
||||||
},
|
},
|
||||||
@@ -159,11 +168,14 @@ const IsolatedRoofModule: React.FC = () => {
|
|||||||
<WindParametersSummary />
|
<WindParametersSummary />
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<label className="text-sm font-medium">Tipo de Cobertura</label>
|
<label className="text-sm font-medium">Tipo de Cobertura</label>
|
||||||
<Select value={type} onValueChange={(v) => setType(v as 'shed' | 'gable')}>
|
<Select value={type} onValueChange={(v: any) => setType(v)}>
|
||||||
<SelectTrigger><SelectValue /></SelectTrigger>
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Tipo de Cobertura" />
|
||||||
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
<SelectItem value="shed">Uma água (Tab. 24)</SelectItem>
|
<SelectItem value="shed">Uma Água</SelectItem>
|
||||||
<SelectItem value="gable">Duas águas (Tab. 25)</SelectItem>
|
<SelectItem value="gable">Duas Águas</SelectItem>
|
||||||
|
<SelectItem value="butterfly">Telhado Borboleta (Em V)</SelectItem>
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
@@ -200,7 +212,25 @@ const IsolatedRoofModule: React.FC = () => {
|
|||||||
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
<div className="flex-1 flex flex-col min-h-[500px] lg:min-h-0 bg-background rounded-xl border border-border shadow-sm overflow-hidden relative">
|
||||||
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
<div className="absolute top-4 left-4 z-10 flex gap-2">
|
||||||
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/80 backdrop-blur-sm shadow-sm">θ = {theta}°</Badge>
|
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/80 backdrop-blur-sm shadow-sm">θ = {theta}°</Badge>
|
||||||
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/80 backdrop-blur-sm shadow-sm">{type === 'shed' ? 'Uma água' : 'Duas águas'}</Badge>
|
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/80 backdrop-blur-sm shadow-sm">{type === 'shed' ? 'Uma água' : type === 'gable' ? 'Duas águas' : 'Borboleta'}</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="absolute top-4 right-16 z-10">
|
||||||
|
<div className="flex bg-background/80 backdrop-blur-sm border border-border/80 rounded-md shadow-sm p-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('solid')}
|
||||||
|
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'solid' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
|
||||||
|
>
|
||||||
|
<Box className="w-3.5 h-3.5 inline-block mr-1.5" />
|
||||||
|
Sólido
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('airflow')}
|
||||||
|
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'airflow' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
|
||||||
|
>
|
||||||
|
<Wind className="w-3.5 h-3.5 inline-block mr-1.5" />
|
||||||
|
Fluxo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-4 right-4 z-10">
|
<div className="absolute top-4 right-4 z-10">
|
||||||
<EducationalManual type="isolated-roof" params={{ theta, type }} />
|
<EducationalManual type="isolated-roof" params={{ theta, type }} />
|
||||||
@@ -216,6 +246,7 @@ const IsolatedRoofModule: React.FC = () => {
|
|||||||
cpeLeeward={cpeLeeward}
|
cpeLeeward={cpeLeeward}
|
||||||
cpeTop={cpeTop}
|
cpeTop={cpeTop}
|
||||||
forceKN={forces.magnitude}
|
forceKN={forces.magnitude}
|
||||||
|
viewMode={viewMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { EducationalManual } from '@/components/EducationalManual';
|
|||||||
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
import { WindParametersSummary } from '@/components/WindParametersSummary';
|
||||||
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
import { SaveModuleDialog } from '@/components/SaveModuleDialog';
|
||||||
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
import { exportGenericToPDF, type GenericPDFSection } from '../lib/export-generic-pdf';
|
||||||
import { Eye, Box, Layers, ArrowUpRight, CheckCircle2 } from 'lucide-react';
|
import { Eye, Box, Layers, ArrowUpRight, CheckCircle2, Wind } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
const conditionLabels: Record<ShelterCondition, string> = {
|
const conditionLabels: Record<ShelterCondition, string> = {
|
||||||
@@ -41,7 +41,7 @@ const ShelterModule: React.FC = () => {
|
|||||||
const [backRange, setBackRange] = useState<[number, number]>([0, 75]);
|
const [backRange, setBackRange] = useState<[number, number]>([0, 75]);
|
||||||
const [leftClosure, setLeftClosure] = useState(100);
|
const [leftClosure, setLeftClosure] = useState(100);
|
||||||
const [rightClosure, setRightClosure] = useState(100);
|
const [rightClosure, setRightClosure] = useState(100);
|
||||||
const [viewMode, setViewMode] = useState<'3d' | 'elevation'>('3d');
|
const [viewMode, setViewMode] = useState<'3d' | 'elevation' | 'airflow'>('3d');
|
||||||
|
|
||||||
// Determinação automática da posição da fresta/vão a partir dos cursores (base e topo)
|
// Determinação automática da posição da fresta/vão a partir dos cursores (base e topo)
|
||||||
const bottomGap = backRange[0];
|
const bottomGap = backRange[0];
|
||||||
@@ -502,6 +502,9 @@ const ShelterModule: React.FC = () => {
|
|||||||
<Button size="sm" variant={viewMode === '3d' ? 'default' : 'ghost'} className="h-6 px-2.5 text-xs rounded-full" onClick={() => setViewMode('3d')}>
|
<Button size="sm" variant={viewMode === '3d' ? 'default' : 'ghost'} className="h-6 px-2.5 text-xs rounded-full" onClick={() => setViewMode('3d')}>
|
||||||
<Box className="size-3 mr-1" /> Vista 3D
|
<Box className="size-3 mr-1" /> Vista 3D
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button size="sm" variant={viewMode === 'airflow' ? 'default' : 'ghost'} className="h-6 px-2.5 text-xs rounded-full" onClick={() => setViewMode('airflow')}>
|
||||||
|
<Wind className="size-3 mr-1" /> Fluxo
|
||||||
|
</Button>
|
||||||
<Button size="sm" variant={viewMode === 'elevation' ? 'default' : 'ghost'} className="h-6 px-2.5 text-xs rounded-full" onClick={() => setViewMode('elevation')}>
|
<Button size="sm" variant={viewMode === 'elevation' ? 'default' : 'ghost'} className="h-6 px-2.5 text-xs rounded-full" onClick={() => setViewMode('elevation')}>
|
||||||
<Eye className="size-3 mr-1" /> Corte A-A
|
<Eye className="size-3 mr-1" /> Corte A-A
|
||||||
</Button>
|
</Button>
|
||||||
@@ -512,7 +515,7 @@ const ShelterModule: React.FC = () => {
|
|||||||
<EducationalManual type="shelter" params={{ condition, backClosure, openingPos, theta, windAngle }} />
|
<EducationalManual type="shelter" params={{ condition, backClosure, openingPos, theta, windAngle }} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{viewMode === '3d' ? (
|
{viewMode === '3d' || viewMode === 'airflow' ? (
|
||||||
<Shelter3D
|
<Shelter3D
|
||||||
condition={condition}
|
condition={condition}
|
||||||
depth={depth}
|
depth={depth}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import React, { useMemo } from 'react';
|
import React, { useState, useMemo } from 'react';
|
||||||
|
import { Wind, Box } from 'lucide-react';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
|
||||||
import { Slider } from '@/components/ui/slider';
|
import { Slider } from '@/components/ui/slider';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
@@ -19,9 +20,10 @@ const TowerModule: React.FC = () => {
|
|||||||
const [barType, setBarType] = React.useState<TowerBarType>('flat');
|
const [barType, setBarType] = React.useState<TowerBarType>('flat');
|
||||||
const [baseWidth, setBaseWidth] = React.useState(3);
|
const [baseWidth, setBaseWidth] = React.useState(3);
|
||||||
const [height, setHeight] = React.useState(30);
|
const [height, setHeight] = React.useState(30);
|
||||||
const [panels, setPanels] = React.useState(6);
|
const [panels, setPanels] = React.useState(5);
|
||||||
const [phi, setPhi] = React.useState(0.3);
|
const [phi, setPhi] = React.useState(0.2);
|
||||||
const [alphaWind, setAlphaWind] = React.useState<0 | 45 | 90>(0);
|
const [alphaWind, setAlphaWind] = React.useState<0 | 45 | 90>(0);
|
||||||
|
const [viewMode, setViewMode] = useState<'solid' | 'airflow'>('solid');
|
||||||
|
|
||||||
const result = useMemo(() => {
|
const result = useMemo(() => {
|
||||||
const aFace = baseWidth * height;
|
const aFace = baseWidth * height;
|
||||||
@@ -178,6 +180,24 @@ const TowerModule: React.FC = () => {
|
|||||||
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">{section === 'square' ? 'Quadrada' : 'Triangular'}</Badge>
|
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">{section === 'square' ? 'Quadrada' : 'Triangular'}</Badge>
|
||||||
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">α = {alphaWind}°</Badge>
|
<Badge variant="secondary" className="bg-background/80 text-foreground border border-border/50 backdrop-blur-sm shadow-sm">α = {alphaWind}°</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="absolute top-4 right-16 z-10">
|
||||||
|
<div className="flex bg-background/80 backdrop-blur-sm border border-border/80 rounded-md shadow-sm p-1">
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('solid')}
|
||||||
|
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'solid' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
|
||||||
|
>
|
||||||
|
<Box className="w-3.5 h-3.5 inline-block mr-1.5" />
|
||||||
|
Sólido
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode('airflow')}
|
||||||
|
className={`px-3 py-1 text-xs font-medium rounded-sm transition-colors ${viewMode === 'airflow' ? 'bg-primary/20 text-primary' : 'text-muted-foreground hover:bg-muted/50'}`}
|
||||||
|
>
|
||||||
|
<Wind className="w-3.5 h-3.5 inline-block mr-1.5" />
|
||||||
|
Fluxo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="absolute top-4 right-4 z-10">
|
<div className="absolute top-4 right-4 z-10">
|
||||||
<EducationalManual type="tower" params={{ section, alphaWind }} />
|
<EducationalManual type="tower" params={{ section, alphaWind }} />
|
||||||
</div>
|
</div>
|
||||||
@@ -190,6 +210,7 @@ const TowerModule: React.FC = () => {
|
|||||||
phi={phi}
|
phi={phi}
|
||||||
alphaWind={alphaWind}
|
alphaWind={alphaWind}
|
||||||
forceKN={result.forceKN}
|
forceKN={result.forceKN}
|
||||||
|
viewMode={viewMode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user