From f7911e7147e3badf6f374fa4b794340b2be0979c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=A5=D0=B0=D0=BB=D0=B8=D0=BC=D0=BE=D0=B2=20=D0=A0=D1=83?= =?UTF-8?q?=D1=81=D1=82=D0=B0=D0=BC?= Date: Sat, 10 Jan 2026 16:57:20 +0300 Subject: [PATCH] Try fix layout step --- src/App.tsx | 13 ++++++-- src/components/LayoutStep.tsx | 51 +++++++++++++++---------------- src/components/PreviewStep.tsx | 49 +++++++++++++++++++++-------- src/services/geometryGenerator.ts | 37 +++++----------------- src/types.ts | 11 +++---- 5 files changed, 86 insertions(+), 75 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 8384063..cff9c35 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -17,7 +17,7 @@ const App = () => { cornerRadius: 4, }); - // ВАЖНО: Инициализируем partitions + // ИНИЦИАЛИЗАЦИЯ: partitions обязательно присутствует const [splits, setSplits] = useState({ x: [], y: [], @@ -28,6 +28,7 @@ const App = () => { const sharedData = parseShareUrl(); if (sharedData) { setConfig(sharedData.config); + // Защита: если в ссылке старый формат, подставляем пустые partitions setSplits({ x: sharedData.splits.x || [], y: sharedData.splits.y || [], @@ -68,7 +69,15 @@ const App = () => {
{step === 1 &&
} - {step === 2 &&
} + + {/* ШАГ 2 */} + {step === 2 && ( +
+ {/* Передаем key, чтобы React пересоздал компонент при смене шага (сброс ошибок) */} + +
+ )} + {step === 3 &&
}
diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx index 21db1d4..8b6cc6c 100644 --- a/src/components/LayoutStep.tsx +++ b/src/components/LayoutStep.tsx @@ -1,6 +1,7 @@ import React, { useRef, useState, useMemo } from 'react'; import { AppConfig, LayoutSplits, Partition } from '../types'; -import { Grid, MousePointer2, Trash2, RotateCcw, LayoutGrid, X, Plus, Settings2, Sliders } from 'lucide-react'; +// Используем только безопасные, стандартные иконки +import { Grid, MousePointer2, Trash2, RotateCcw, X, Plus, Move, Ban } from 'lucide-react'; interface Props { config: AppConfig; @@ -15,18 +16,16 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const svgRef = useRef(null); const [mode, setMode] = useState('lines'); - - // States for Lines const [phantomAxis, setPhantomAxis] = useState(null); const [mousePos, setMousePos] = useState({ x: 0, y: 0 }); const [hoveredSplit, setHoveredSplit] = useState<{ axis: Axis; index: number } | null>(null); const [dragging, setDragging] = useState<{ axis: Axis; index: number } | null>(null); const [isButtonHovered, setIsButtonHovered] = useState(false); - - // State for Cell Editor + + // Редактор ячеек const [editingCell, setEditingCell] = useState<{ i: number, j: number } | null>(null); - // Safeties + // --- SAFETY FIRST --- const safeX = splits?.x || []; const safeY = splits?.y || []; const safePartitions = splits?.partitions || {}; @@ -38,7 +37,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const sortedX = useMemo(() => [0, ...safeX, 1].sort((a, b) => a - b), [safeX]); const sortedY = useMemo(() => [0, ...safeY, 1].sort((a, b) => a - b), [safeY]); - // --- PARTITION LOGIC --- + // --- Логика перегородок --- const getCurrentPartitions = () => { if (!editingCell) return []; const key = `${editingCell.i}-${editingCell.j}`; @@ -53,7 +52,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { id: Date.now().toString(), axis, offset: 0.5, - height: config.drawer.height, // по умолчанию полная высота + height: config.drawer.height, rounded: false }; @@ -85,7 +84,6 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { }); }; - // --- MOUSE HANDLERS (Global) --- const handleGlobalMouseMove = (e: React.MouseEvent) => { if (!svgRef.current) return; const rect = svgRef.current.getBoundingClientRect(); @@ -155,10 +153,10 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
@@ -173,21 +171,21 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { {/* Instructions */}
- {mode === 'lines' ? 'Режим: Границы' : 'Режим: Ячейки'} + + {mode === 'lines' ? 'Режим: Границы' : 'Режим: Ячейки'}
{mode === 'lines' ? (
    -
  • Клик: Новая граница
  • +
  • Клик: Новая линия
  • Драг: Двигать
) : (
    -
  • Клик по ячейке: Настройка стенок
  • +
  • Клик по ячейке: Настройка
)}
- {/* Rulers */}
0 {config.drawer.width} мм @@ -217,7 +215,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { - {/* --- Cells & Partitions --- */} + {/* --- Ячейки и перегородки --- */} {sortedX.slice(0, -1).map((x1, i) => { const x2 = sortedX[i + 1]; return sortedY.slice(0, -1).map((y1, j) => { @@ -236,14 +234,14 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { className={mode === 'cells' ? "cursor-pointer hover:fill-white/5 transition-all" : ""} onClick={(e) => { if (mode === 'cells') { e.stopPropagation(); setEditingCell({ i, j }); } }} /> - {/* Render Partitions */} + {/* Рисуем внутренние стенки */} {parts.map(p => { if (p.axis === 'x') { const px = cellX + (cellW * p.offset); - return ; + return ; } else { const py = cellY + (cellH * p.offset); - return ; + return ; } })} @@ -251,7 +249,7 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { }); })} - {/* --- Main Grid Lines --- */} + {/* --- Основные линии сетки (Границы) --- */} {safeX.map((x, i) => ( { if (mode === 'lines' && !dragging) { e.stopPropagation(); setHoveredSplit({ axis: 'x', index: i }); setPhantomAxis(null); } }}> @@ -284,22 +282,22 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
- {/* --- MODAL EDITOR FOR CELLS --- */} + {/* --- ПАНЕЛЬ РЕДАКТОРА (Справа) --- */} {mode === 'cells' && editingCell && ( -
+

- Редактор ячейки + Редактор ячейки

@@ -345,7 +343,8 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => {
))} {getCurrentPartitions().length === 0 && ( -
+
+ Нет перегородок
)} diff --git a/src/components/PreviewStep.tsx b/src/components/PreviewStep.tsx index 3faaeaf..8334b76 100644 --- a/src/components/PreviewStep.tsx +++ b/src/components/PreviewStep.tsx @@ -8,7 +8,7 @@ import { createBinGeometry, generateSTL, exportSTL } from '../services/geometryG import { Download, Package, Info, Loader2, Share2, Check, Ruler } from 'lucide-react'; import { generateShareUrl } from '../utils/share'; -// --- DrawerFrame (Каркас) --- +// --- DrawerFrame (Каркас ящика) --- const DrawerFrame = ({ config }: { config: AppConfig }) => { const { width, depth, height } = config.drawer; const offset = 0.5; @@ -32,32 +32,37 @@ interface BinMeshProps { } const BinMesh: React.FC = ({ part, thickness, cornerRadius, isSelected, onClick }) => { + // 1. Создаем геометрию, учитывая ВНУТРЕННИЕ ПЕРЕГОРОДКИ const geometry = useMemo(() => { return createBinGeometry( part.width, part.depth, part.height, thickness, - cornerRadius, - part.internalPartitions // <--- ВАЖНО: передаем перегородки + cornerRadius, + part.internalPartitions // <--- ВАЖНО: передаем перегородки в генератор ); -}, [part, thickness, cornerRadius]); + }, [part, thickness, cornerRadius]); + // 2. Создаем контур выделения (EdgesGeometry) + // Threshold 20 градусов скрывает линии на плавных скруглениях const edgesGeometry = useMemo(() => { return new THREE.EdgesGeometry(geometry, 20); }, [geometry]); return ( + {/* Сама модель */} { e.stopPropagation(); onClick(); }}> + {/* Белая подсветка при выборе */} {isSelected && ( @@ -67,7 +72,7 @@ const BinMesh: React.FC = ({ part, thickness, cornerRadius, isSele ); }; -// --- PreviewStep (Основной) --- +// --- PreviewStep (Основной компонент) --- interface Props { parts: GeneratedPart[]; config: AppConfig; @@ -80,25 +85,42 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { const [shareUrlCopied, setShareUrlCopied] = 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, config.cornerRadius); + const geometry = createBinGeometry( + part.width, + part.depth, + part.height, + config.wallThickness, + config.cornerRadius, + part.internalPartitions // <--- ВАЖНО для STL + ); 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 { const zip = new JSZip(); parts.forEach(part => { - const geometry = createBinGeometry(part.width, part.depth, part.height, config.wallThickness, config.cornerRadius); + const geometry = createBinGeometry( + part.width, + part.depth, + part.height, + config.wallThickness, + config.cornerRadius, + part.internalPartitions // <--- ВАЖНО для STL + ); const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial()); const stlData = generateSTL(mesh); zip.file(`${part.name.replace(/\s+/g, '_')}.stl`, stlData); @@ -117,6 +139,7 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { } }; + // Поделиться ссылкой const handleShare = async () => { const url = generateShareUrl(config, splits); let success = false; @@ -150,8 +173,9 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { return (
- {/* Верхняя панель */} + {/* Верхняя панель: Размеры + Поделиться */}
+
@@ -173,6 +197,7 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { мм
+