Files
BoxGenerator/components/PreviewStep.tsx
Халимов Рустам d318519cd7 V1
2025-12-27 17:46:19 +03:00

259 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React, { useMemo, Suspense, useEffect, useRef, useState } from 'react';
import { Canvas } from '@react-three/fiber';
import { OrbitControls, Center, Environment } from '@react-three/drei';
import * as THREE from 'three';
import JSZip from 'jszip';
import { AppConfig, GeneratedPart } from '../types';
import { createBinGeometry, exportSTL, generateSTL } from '../services/geometryGenerator';
import { Download, Package, Info, Loader2 } from 'lucide-react';
// --- 3D Helper Components ---
// Каркас ящика (только ребра, без диагоналей)
const DrawerFrame = ({ config }: { config: AppConfig }) => {
const { width, depth, height } = config.drawer;
const offset = 0.5; // Небольшой отступ наружу
return (
<group position={[width / 2, height / 2, depth / 2]}>
<lineSegments>
<edgesGeometry args={[new THREE.BoxGeometry(width + offset, height + offset, depth + offset)]} />
<lineBasicMaterial color="#475569" />
</lineSegments>
</group>
)
}
// --- Bin Component ---
interface BinMeshProps {
part: GeneratedPart;
thickness: number;
isSelected: boolean;
onClick: () => void;
}
const BinMesh: React.FC<BinMeshProps> = ({ part, thickness, isSelected, onClick }) => {
// REMOVED ARTIFICIAL GAP: The part dimensions now include the printer tolerance physically.
// The gap will be visible naturally because part.width is smaller than grid size.
// Генерируем реальную геометрию, как для STL
const geometry = useMemo(() => {
return createBinGeometry(part.width, part.depth, part.height, thickness);
}, [part, thickness]);
return (
<group position={[part.x + part.width/2, 0, part.y + part.depth/2]}>
{/* Основной меш ячейки */}
<mesh
geometry={geometry}
onClick={(e) => { e.stopPropagation(); onClick(); }}
>
<meshStandardMaterial
color={isSelected ? '#f59e0b' : part.color}
roughness={0.5}
metalness={0.1}
/>
</mesh>
{/* Подсветка выделения (Bounding Box) */}
{isSelected && (
<lineSegments position={[0, part.height/2, 0]}>
<edgesGeometry args={[new THREE.BoxGeometry(part.width, part.height, part.depth)]} />
<lineBasicMaterial color="white" linewidth={2} />
</lineSegments>
)}
</group>
);
};
interface Props {
parts: GeneratedPart[];
config: AppConfig;
}
export const PreviewStep: React.FC<Props> = ({ parts, config }) => {
const [selectedId, setSelectedId] = useState<string | null>(null);
const [isZipping, setIsZipping] = useState(false);
const itemRefs = useRef<{ [key: string]: HTMLDivElement | null }>({});
// Автопрокрутка к выбранному элементу
useEffect(() => {
if (selectedId && itemRefs.current[selectedId]) {
itemRefs.current[selectedId]?.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}, [selectedId]);
const handleDownload = (part: GeneratedPart) => {
const geometry = createBinGeometry(part.width, part.depth, part.height, config.wallThickness);
const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial());
exportSTL(mesh, `${part.name.replace(/\s+/g, '_')}.stl`);
};
const handleDownloadAll = async () => {
if (isZipping) return;
setIsZipping(true);
try {
console.log("Starting ZIP generation...");
// Ensure JSZip is available
if (typeof JSZip === 'undefined' && !JSZip) {
throw new Error("Библиотека JSZip не загружена.");
}
const zip = new JSZip();
// Генерация STL для каждой части и добавление в архив
parts.forEach(part => {
const geometry = createBinGeometry(part.width, part.depth, part.height, config.wallThickness);
const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial());
const stlData = generateSTL(mesh);
// stlData is Uint8Array or string here, which is supported
zip.file(`${part.name.replace(/\s+/g, '_')}.stl`, stlData);
});
console.log("Files added to ZIP. Generating blob...");
// Генерация самого ZIP файла
const content = await zip.generateAsync({ type: "blob" });
console.log("ZIP blob generated. Size:", content.size);
// Скачивание
const link = document.createElement('a');
link.href = URL.createObjectURL(content);
link.download = "PrintFit_Project.zip";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
} catch (e: any) {
console.error("Failed to create zip archive", e);
alert(`Ошибка при создании архива: ${e.message || 'Неизвестная ошибка'}`);
} finally {
setIsZipping(false);
}
};
return (
<div className="flex flex-col lg:flex-row h-full gap-6">
{/* 3D Viewer */}
<div className="flex-1 bg-slate-900 rounded-xl overflow-hidden shadow-2xl border border-slate-800 relative min-h-[400px]">
<div className="absolute top-4 right-4 z-10 bg-black/60 p-3 rounded-lg text-xs text-gray-300 backdrop-blur pointer-events-none border border-slate-700">
<div className="flex items-center gap-2 mb-1 text-primary font-bold">
<Info size={14} /> Управление
</div>
<ul className="space-y-1">
<li> ЛКМ: Вращение</li>
<li> ПКМ: Перемещение</li>
<li> Скролл: Масштаб</li>
<li className="text-accent mt-2 font-semibold"> Клик по детали для выбора</li>
</ul>
</div>
<Canvas
shadows
dpr={[1, 2]}
camera={{
position: [config.drawer.width * 1.5, config.drawer.height * 3, config.drawer.depth * 1.5],
fov: 45,
near: 1,
far: 20000
}}
>
<Suspense fallback={null}>
<color attach="background" args={['#0f172a']} />
<ambientLight intensity={0.7} />
<directionalLight position={[100, 200, 50]} intensity={1.2} />
<directionalLight position={[-100, 100, -50]} intensity={0.5} />
<Environment preset="city" />
<Center>
<group>
<DrawerFrame config={config} />
{parts.map(part => (
<BinMesh
key={part.id}
part={part}
thickness={config.wallThickness}
isSelected={selectedId === part.id}
onClick={() => setSelectedId(part.id)}
/>
))}
</group>
</Center>
<OrbitControls makeDefault minDistance={10} maxDistance={10000} />
</Suspense>
</Canvas>
</div>
{/* Sidebar List */}
<div className="w-full lg:w-96 bg-slate-900 p-6 rounded-xl border border-slate-800 flex flex-col h-full shadow-xl">
<div className="flex justify-between items-center mb-6 shrink-0">
<h2 className="text-xl font-bold flex items-center gap-2 text-primary">
<Package size={24} /> Детали ({parts.length})
</h2>
<button
onClick={handleDownloadAll}
disabled={isZipping || parts.length === 0}
className={`bg-green-600 hover:bg-green-700 text-white px-3 py-1.5 rounded-md text-sm font-medium flex items-center gap-1 transition-all active:scale-95 disabled:opacity-50 disabled:scale-100 disabled:cursor-not-allowed`}
>
{isZipping ? (
<>
<Loader2 size={16} className="animate-spin" /> ZIP...
</>
) : (
<>
<Download size={16} /> Скачать все
</>
)}
</button>
</div>
<div className="flex-1 overflow-y-auto pr-2 space-y-3 custom-scrollbar">
{parts.map(part => (
<div
key={part.id}
ref={(el) => { itemRefs.current[part.id] = el }}
className={`p-4 rounded-lg border transition-all cursor-pointer group ${selectedId === part.id ? 'bg-slate-800 border-accent shadow-md shadow-accent/10 ring-1 ring-accent' : 'bg-slate-800/50 border-slate-700 hover:border-slate-500 hover:bg-slate-800'}`}
onClick={() => setSelectedId(part.id)}
>
<div className="flex justify-between items-start mb-2">
<span className="font-semibold text-gray-200 group-hover:text-white transition-colors">{part.name}</span>
<div
className="w-3 h-3 rounded-full border border-white/10"
style={{ backgroundColor: part.color }}
/>
</div>
<div className="grid grid-cols-3 gap-2 text-xs text-gray-400 mb-3">
<div className="bg-slate-900/50 p-1.5 rounded">
<span className="block text-gray-500 uppercase text-[9px] mb-0.5">Ширина</span>
{part.width.toFixed(1)}
</div>
<div className="bg-slate-900/50 p-1.5 rounded">
<span className="block text-gray-500 uppercase text-[9px] mb-0.5">Глубина</span>
{part.depth.toFixed(1)}
</div>
<div className="bg-slate-900/50 p-1.5 rounded">
<span className="block text-gray-500 uppercase text-[9px] mb-0.5">Высота</span>
{part.height.toFixed(1)}
</div>
</div>
<button
onClick={(e) => { e.stopPropagation(); handleDownload(part); }}
className="w-full py-2 bg-slate-700 hover:bg-primary hover:text-white text-gray-300 rounded text-xs flex items-center justify-center gap-2 transition-colors font-medium"
>
<Download size={14} /> Скачать STL
</button>
</div>
))}
</div>
</div>
</div>
);
};