🚀 Auto-deploy: BrainWind atualizado em 12/07/2026 16:42:11
This commit is contained in:
@@ -12,6 +12,8 @@ export interface Vault3DInput {
|
|||||||
rise: number;
|
rise: number;
|
||||||
cpi: number;
|
cpi: number;
|
||||||
cpeProfile: Record<string, number>;
|
cpeProfile: Record<string, number>;
|
||||||
|
cpeParallel?: Record<string, number>;
|
||||||
|
viewDirection?: 'perpendicular' | 'parallel';
|
||||||
}
|
}
|
||||||
|
|
||||||
function vaultColor(cpe: number, cpi: number, isDark: boolean): THREE.Color {
|
function vaultColor(cpe: number, cpi: number, isDark: boolean): THREE.Color {
|
||||||
@@ -24,10 +26,11 @@ function vaultColor(cpe: number, cpi: number, isDark: boolean): THREE.Color {
|
|||||||
return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, l + 5)}%)`);
|
return new THREE.Color(`hsl(0, ${70 + intensity * 25}%, ${Math.max(40, l + 5)}%)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function VaultModel({ span, length, rise, cpi, cpeProfile }: Vault3DInput) {
|
function VaultModel({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDirection = 'perpendicular' }: Vault3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
const segments = 64;
|
const segments = 64;
|
||||||
|
|
||||||
const points = useMemo(() => {
|
const points = useMemo(() => {
|
||||||
const pts: THREE.Vector3[] = [];
|
const pts: THREE.Vector3[] = [];
|
||||||
for (let i = 0; i <= segments; i++) {
|
for (let i = 0; i <= segments; i++) {
|
||||||
@@ -39,20 +42,6 @@ function VaultModel({ span, length, rise, cpi, cpeProfile }: Vault3DInput) {
|
|||||||
return pts;
|
return pts;
|
||||||
}, [span, rise, segments]);
|
}, [span, rise, segments]);
|
||||||
|
|
||||||
// Divide em zonas (1, 2, 3, 4, 5, 6)
|
|
||||||
const zones = useMemo(() => {
|
|
||||||
const arr: { cpe: number; startIdx: number; endIdx: number }[] = [];
|
|
||||||
const zoneSize = segments / 6;
|
|
||||||
for (let z = 0; z < 6; z++) {
|
|
||||||
const startIdx = Math.floor(z * zoneSize);
|
|
||||||
const endIdx = Math.floor((z + 1) * zoneSize);
|
|
||||||
const key = `zone${z + 1}`;
|
|
||||||
arr.push({ cpe: cpeProfile[key] ?? -0.5, startIdx, endIdx });
|
|
||||||
}
|
|
||||||
return arr;
|
|
||||||
}, [cpeProfile, segments]);
|
|
||||||
|
|
||||||
// Shape para fechar os tímpanos (paredes frontais/traseiras em arco)
|
|
||||||
const archShape = useMemo(() => {
|
const archShape = useMemo(() => {
|
||||||
const s = new THREE.Shape();
|
const s = new THREE.Shape();
|
||||||
s.moveTo(-span / 2, 0);
|
s.moveTo(-span / 2, 0);
|
||||||
@@ -67,10 +56,17 @@ function VaultModel({ span, length, rise, cpi, cpeProfile }: Vault3DInput) {
|
|||||||
return s;
|
return s;
|
||||||
}, [span, rise, segments]);
|
}, [span, rise, segments]);
|
||||||
|
|
||||||
return (
|
const renderPerpendicular = () => {
|
||||||
<group>
|
const zones = [];
|
||||||
{/* Casca da Abóbada */}
|
const zoneSize = segments / 6;
|
||||||
{zones.map((zone, idx) => {
|
for (let z = 0; z < 6; z++) {
|
||||||
|
const startIdx = Math.floor(z * zoneSize);
|
||||||
|
const endIdx = Math.floor((z + 1) * zoneSize);
|
||||||
|
const key = `zone${z + 1}`;
|
||||||
|
zones.push({ cpe: cpeProfile[key] ?? -0.5, startIdx, endIdx, label: `${z + 1}` });
|
||||||
|
}
|
||||||
|
|
||||||
|
return zones.map((zone, idx) => {
|
||||||
const color = vaultColor(zone.cpe, cpi, isDark);
|
const color = vaultColor(zone.cpe, cpi, isDark);
|
||||||
const verts: number[] = [];
|
const verts: number[] = [];
|
||||||
for (let i = zone.startIdx; i <= zone.endIdx; i++) {
|
for (let i = zone.startIdx; i <= zone.endIdx; i++) {
|
||||||
@@ -85,16 +81,84 @@ function VaultModel({ span, length, rise, cpi, cpeProfile }: Vault3DInput) {
|
|||||||
const d = i * 2 + 3;
|
const d = i * 2 + 3;
|
||||||
indices.push(a, b, c, b, d, c);
|
indices.push(a, b, c, b, d, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const midIdx = Math.floor((zone.startIdx + zone.endIdx) / 2);
|
||||||
|
const midPt = points[midIdx];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<mesh key={idx} castShadow receiveShadow>
|
<group key={idx}>
|
||||||
|
<mesh castShadow receiveShadow>
|
||||||
<bufferGeometry>
|
<bufferGeometry>
|
||||||
<bufferAttribute attach="attributes-position" args={[new Float32Array(verts), 3]} />
|
<bufferAttribute attach="attributes-position" args={[new Float32Array(verts), 3]} />
|
||||||
<bufferAttribute attach="index" args={[new Uint16Array(indices), 1]} />
|
<bufferAttribute attach="index" args={[new Uint16Array(indices), 1]} />
|
||||||
</bufferGeometry>
|
</bufferGeometry>
|
||||||
<meshStandardMaterial color={color} opacity={0.92} transparent side={THREE.DoubleSide} roughness={0.4} />
|
<meshStandardMaterial color={color} opacity={0.92} transparent side={THREE.DoubleSide} roughness={0.4} />
|
||||||
</mesh>
|
</mesh>
|
||||||
|
<Text
|
||||||
|
position={[midPt.x, midPt.y + 0.5, length / 2]}
|
||||||
|
fontSize={Math.max(0.6, span / 15)}
|
||||||
|
color={isDark ? "#f8fafc" : "#0f172a"}
|
||||||
|
anchorX="center"
|
||||||
|
anchorY="middle"
|
||||||
|
>
|
||||||
|
{zone.label}
|
||||||
|
</Text>
|
||||||
|
</group>
|
||||||
);
|
);
|
||||||
})}
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderParallel = () => {
|
||||||
|
const zones = ['A', 'B', 'C', 'D'];
|
||||||
|
const zStep = length / 4;
|
||||||
|
|
||||||
|
return zones.map((label, idx) => {
|
||||||
|
const zStart = idx * zStep;
|
||||||
|
const zEnd = (idx + 1) * zStep;
|
||||||
|
const cpe = cpeParallel?.[label] ?? -0.5;
|
||||||
|
const color = vaultColor(cpe, cpi, isDark);
|
||||||
|
|
||||||
|
const verts: number[] = [];
|
||||||
|
for (let i = 0; i <= segments; i++) {
|
||||||
|
verts.push(points[i].x, points[i].y, zStart);
|
||||||
|
verts.push(points[i].x, points[i].y, zEnd);
|
||||||
|
}
|
||||||
|
const indices: number[] = [];
|
||||||
|
for (let i = 0; i < segments; i++) {
|
||||||
|
const a = i * 2;
|
||||||
|
const b = i * 2 + 1;
|
||||||
|
const c = i * 2 + 2;
|
||||||
|
const d = i * 2 + 3;
|
||||||
|
indices.push(a, b, c, b, d, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group key={idx}>
|
||||||
|
<mesh castShadow receiveShadow>
|
||||||
|
<bufferGeometry>
|
||||||
|
<bufferAttribute attach="attributes-position" args={[new Float32Array(verts), 3]} />
|
||||||
|
<bufferAttribute attach="index" args={[new Uint16Array(indices), 1]} />
|
||||||
|
</bufferGeometry>
|
||||||
|
<meshStandardMaterial color={color} opacity={0.92} transparent side={THREE.DoubleSide} roughness={0.4} />
|
||||||
|
</mesh>
|
||||||
|
<Text
|
||||||
|
position={[0, rise + 0.5, (zStart + zEnd) / 2]}
|
||||||
|
fontSize={Math.max(0.6, span / 15)}
|
||||||
|
color={isDark ? "#f8fafc" : "#0f172a"}
|
||||||
|
anchorX="center"
|
||||||
|
anchorY="middle"
|
||||||
|
rotation={[-Math.PI / 2, 0, 0]}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group>
|
||||||
|
{viewDirection === 'perpendicular' ? renderPerpendicular() : renderParallel()}
|
||||||
|
|
||||||
{/* Tímpano Traseiro (Z = 0) */}
|
{/* Tímpano Traseiro (Z = 0) */}
|
||||||
<mesh position={[0, 0, 0]} castShadow receiveShadow>
|
<mesh position={[0, 0, 0]} castShadow receiveShadow>
|
||||||
@@ -122,7 +186,7 @@ function VaultModel({ span, length, rise, cpi, cpeProfile }: Vault3DInput) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Vault3DViewer({ span, length, rise, cpi, cpeProfile }: Vault3DInput) {
|
export default function Vault3DViewer({ span, length, rise, cpi, cpeProfile, cpeParallel, viewDirection = 'perpendicular' }: Vault3DInput) {
|
||||||
const theme = useCanvasTheme();
|
const theme = useCanvasTheme();
|
||||||
const isDark = theme === 'dark';
|
const isDark = theme === 'dark';
|
||||||
|
|
||||||
@@ -150,7 +214,7 @@ export default function Vault3DViewer({ span, length, rise, cpi, cpeProfile }: V
|
|||||||
shadow-mapSize-width={1024}
|
shadow-mapSize-width={1024}
|
||||||
shadow-mapSize-height={1024}
|
shadow-mapSize-height={1024}
|
||||||
/>
|
/>
|
||||||
<VaultModel span={span} length={length} rise={rise} cpi={cpi} cpeProfile={cpeProfile} />
|
<VaultModel span={span} length={length} rise={rise} cpi={cpi} cpeProfile={cpeProfile} cpeParallel={cpeParallel} viewDirection={viewDirection} />
|
||||||
<Grid
|
<Grid
|
||||||
infiniteGrid
|
infiniteGrid
|
||||||
fadeDistance={maxDimension * 5}
|
fadeDistance={maxDimension * 5}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ 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 { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { useWindStore } from '@/store/appStore';
|
import { useWindStore } from '@/store/appStore';
|
||||||
import { calculateVault, type VaultRegime } from '@/lib/modules/vault';
|
import { calculateVault, type VaultRegime } from '@/lib/modules/vault';
|
||||||
import Vault3DViewer from '@/components/three/Vault3D';
|
import Vault3DViewer from '@/components/three/Vault3D';
|
||||||
@@ -20,6 +21,7 @@ const VaultModule: React.FC = () => {
|
|||||||
const [rise, setRise] = React.useState(5);
|
const [rise, setRise] = React.useState(5);
|
||||||
const [regime, setRegime] = React.useState<VaultRegime>('laminar-rough');
|
const [regime, setRegime] = React.useState<VaultRegime>('laminar-rough');
|
||||||
const [localCpi, setLocalCpi] = React.useState<number>(0);
|
const [localCpi, setLocalCpi] = React.useState<number>(0);
|
||||||
|
const [viewDirection, setViewDirection] = React.useState<'perpendicular' | 'parallel'>('perpendicular');
|
||||||
|
|
||||||
const result = useMemo(() => calculateVault({ f: rise, l: span, b: length, vk, regime, cpi: localCpi }), [span, length, rise, vk, regime, localCpi]);
|
const result = useMemo(() => calculateVault({ f: rise, l: span, b: length, vk, regime, cpi: localCpi }), [span, length, rise, vk, regime, localCpi]);
|
||||||
|
|
||||||
@@ -128,12 +130,26 @@ const VaultModule: React.FC = () => {
|
|||||||
Cpi = {localCpi > 0 ? `+${localCpi.toFixed(2)}` : localCpi.toFixed(2)}
|
Cpi = {localCpi > 0 ? `+${localCpi.toFixed(2)}` : localCpi.toFixed(2)}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="absolute top-4 right-4 z-10">
|
<div className="absolute top-4 right-4 z-10 flex gap-2">
|
||||||
|
<Tabs value={viewDirection} onValueChange={(v) => setViewDirection(v as 'perpendicular' | 'parallel')} className="bg-background/80 backdrop-blur-sm rounded-md shadow-sm border border-border">
|
||||||
|
<TabsList className="h-8 p-1">
|
||||||
|
<TabsTrigger value="perpendicular" className="text-xs px-3">Vento ⊥</TabsTrigger>
|
||||||
|
<TabsTrigger value="parallel" className="text-xs px-3">Vento ∥</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
<EducationalManual type="vault" params={{ rise, span, regime, localCpi }} />
|
<EducationalManual type="vault" params={{ rise, span, regime, localCpi }} />
|
||||||
</div>
|
</div>
|
||||||
<PressureLegend />
|
<PressureLegend />
|
||||||
<div className="w-full h-full bg-muted/20">
|
<div className="w-full h-full bg-muted/20">
|
||||||
<Vault3DViewer span={span} length={length} rise={rise} cpi={localCpi} cpeProfile={{ ...result.windPerpendicular }} />
|
<Vault3DViewer
|
||||||
|
span={span}
|
||||||
|
length={length}
|
||||||
|
rise={rise}
|
||||||
|
cpi={localCpi}
|
||||||
|
cpeProfile={{ ...result.windPerpendicular }}
|
||||||
|
cpeParallel={{ ...result.windParallel }}
|
||||||
|
viewDirection={viewDirection}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user