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 ( ) } // --- Bin Component --- interface BinMeshProps { part: GeneratedPart; thickness: number; isSelected: boolean; onClick: () => void; } const BinMesh: React.FC = ({ 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 ( {/* Основной меш ячейки */} { e.stopPropagation(); onClick(); }} > {/* Подсветка выделения (Bounding Box) */} {isSelected && ( )} ); }; interface Props { parts: GeneratedPart[]; config: AppConfig; } export const PreviewStep: React.FC = ({ parts, config }) => { const [selectedId, setSelectedId] = useState(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 (
{/* 3D Viewer */}
Управление
  • • ЛКМ: Вращение
  • • ПКМ: Перемещение
  • • Скролл: Масштаб
  • • Клик по детали для выбора
{parts.map(part => ( setSelectedId(part.id)} /> ))}
{/* Sidebar List */}

Детали ({parts.length})

{parts.map(part => (
{ 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)} >
{part.name}
Ширина {part.width.toFixed(1)}
Глубина {part.depth.toFixed(1)}
Высота {part.height.toFixed(1)}
))}
); };