From 4af73b00ebc8cad6ad460af941877ac9b70e8196 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:43:20 +0300 Subject: [PATCH] Add split --- src/App.tsx | 86 ++---- src/components/LayoutStep.tsx | 432 ++++++++++++++++++------------ src/components/PreviewStep.tsx | 11 +- src/services/geometryGenerator.ts | 115 +++++--- src/types.ts | 13 + 5 files changed, 379 insertions(+), 278 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 225b87d..8384063 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -9,9 +9,7 @@ import { ChevronRight, ChevronLeft, Box } from 'lucide-react'; const App = () => { const [step, setStep] = useState(1); - const [isLoadedFromUrl, setIsLoadedFromUrl] = useState(false); - // State const [config, setConfig] = useState({ drawer: { width: 300, depth: 400, height: 80 }, wallThickness: 1.2, @@ -19,36 +17,33 @@ const App = () => { cornerRadius: 4, }); - // Чистый стейт без subdivisions + // ВАЖНО: Инициализируем partitions const [splits, setSplits] = useState({ x: [], - y: [] + y: [], + partitions: {} }); - // --- ЛОГИКА ВОССТАНОВЛЕНИЯ ИЗ ССЫЛКИ --- useEffect(() => { const sharedData = parseShareUrl(); if (sharedData) { setConfig(sharedData.config); - // Принудительно чистим объект от старых полей, если они были в ссылке setSplits({ x: sharedData.splits.x || [], - y: sharedData.splits.y || [] + y: sharedData.splits.y || [], + partitions: sharedData.splits.partitions || {} }); setStep(3); - setIsLoadedFromUrl(true); window.history.replaceState({}, '', window.location.pathname); } }, []); - // Derived State: Parts const parts: GeneratedPart[] = useMemo(() => { return calculateParts(config, splits); }, [config, splits]); return (
- {/* Header */}
@@ -60,80 +55,31 @@ const App = () => {

Генератор органайзеров

- - {/* Progress Stepper */}
{[1, 2, 3].map((num) => ( - -
- - {num} - - - {num === 1 ? 'Настройки' : num === 2 ? 'Макет' : 'Экспорт'} - -
- {num < 3 &&
} - +
+ {num} + {num === 1 ? 'Настройки' : num === 2 ? 'Макет' : 'Экспорт'} +
))}
- {/* Main Content */}
- {step === 1 && ( -
- -
- )} - - {step === 2 && ( -
- -
- )} - - {step === 3 && ( -
- -
- )} + {step === 1 &&
} + {step === 2 &&
} + {step === 3 &&
}
- {/* Footer Navigation */}
- - -
- {step === 2 && Ячеек: {parts.length}} -
- + +
{step === 2 && Ячеек: {parts.length}}
{step < 3 ? ( - + ) : ( - + )}
diff --git a/src/components/LayoutStep.tsx b/src/components/LayoutStep.tsx index 3f82dd6..21db1d4 100644 --- a/src/components/LayoutStep.tsx +++ b/src/components/LayoutStep.tsx @@ -1,6 +1,6 @@ import React, { useRef, useState, useMemo } from 'react'; -import { AppConfig, LayoutSplits } from '../types'; -import { Grid, MousePointer2, Trash2, RotateCcw } from 'lucide-react'; +import { AppConfig, LayoutSplits, Partition } from '../types'; +import { Grid, MousePointer2, Trash2, RotateCcw, LayoutGrid, X, Plus, Settings2, Sliders } from 'lucide-react'; interface Props { config: AppConfig; @@ -8,31 +8,84 @@ interface Props { onChange: (splits: LayoutSplits) => void; } +type EditMode = 'lines' | 'cells'; type Axis = 'x' | 'y'; export const LayoutStep: React.FC = ({ config, splits, onChange }) => { const svgRef = useRef(null); - // State + 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); - - // FIX: Кнопка удаления не пропадает под курсором const [isButtonHovered, setIsButtonHovered] = useState(false); - // SAFE ACCESS: Защита от undefined - const safeX = splits.x || []; - const safeY = splits.y || []; + // State for Cell Editor + const [editingCell, setEditingCell] = useState<{ i: number, j: number } | null>(null); + + // Safeties + const safeX = splits?.x || []; + const safeY = splits?.y || []; + const safePartitions = splits?.partitions || {}; const viewBoxW = 1000; - const aspectRatio = config.drawer.depth / config.drawer.width; + const aspectRatio = (config.drawer.depth || 1) / (config.drawer.width || 1); const viewBoxH = viewBoxW * aspectRatio; 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}`; + return safePartitions[key] || []; + }; + + const addPartition = (axis: 'x' | 'y') => { + if (!editingCell) return; + const key = `${editingCell.i}-${editingCell.j}`; + const current = safePartitions[key] || []; + const newPart: Partition = { + id: Date.now().toString(), + axis, + offset: 0.5, + height: config.drawer.height, // по умолчанию полная высота + rounded: false + }; + + onChange({ + ...splits, + partitions: { ...safePartitions, [key]: [...current, newPart] } + }); + }; + + const updatePartition = (id: string, updates: Partial) => { + if (!editingCell) return; + const key = `${editingCell.i}-${editingCell.j}`; + const current = safePartitions[key] || []; + const updated = current.map(p => p.id === id ? { ...p, ...updates } : p); + + onChange({ + ...splits, + partitions: { ...safePartitions, [key]: updated } + }); + }; + + const removePartition = (id: string) => { + if (!editingCell) return; + const key = `${editingCell.i}-${editingCell.j}`; + const current = safePartitions[key] || []; + onChange({ + ...splits, + partitions: { ...safePartitions, [key]: current.filter(p => p.id !== id) } + }); + }; + + // --- MOUSE HANDLERS (Global) --- const handleGlobalMouseMove = (e: React.MouseEvent) => { if (!svgRef.current) return; const rect = svgRef.current.getBoundingClientRect(); @@ -41,56 +94,51 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { setMousePos({ x: nx, y: ny }); - if (dragging) { - const newSplits = { x: [...safeX], y: [...safeY] }; - const val = dragging.axis === 'x' ? nx : ny; - newSplits[dragging.axis][dragging.index] = val; - onChange(newSplits); - return; + if (mode === 'lines') { + if (dragging) { + const newSplits = { ...splits, x: [...safeX], y: [...safeY] }; + const val = dragging.axis === 'x' ? nx : ny; + newSplits[dragging.axis][dragging.index] = val; + onChange(newSplits); + return; + } + if (isButtonHovered) return; + setHoveredSplit(null); + + const SNAP = 0.02; + const closeToX = safeX.some(val => Math.abs(nx - val) < SNAP); + const closeToY = safeY.some(val => Math.abs(ny - val) < SNAP); + const closeToEdgeX = nx < SNAP || nx > (1 - SNAP); + const closeToEdgeY = ny < SNAP || ny > (1 - SNAP); + + if (!closeToX && !closeToY && !closeToEdgeX && !closeToEdgeY) { + const distRight = 1 - nx; const distBottom = 1 - ny; + setPhantomAxis(Math.min(nx, distRight) < Math.min(ny, distBottom) ? 'y' : 'x'); + } else { + setPhantomAxis(null); + } } - - if (isButtonHovered) return; - - setHoveredSplit(null); - - const SNAP = 0.02; - const closeToX = safeX.some(val => Math.abs(nx - val) < SNAP); - const closeToY = safeY.some(val => Math.abs(ny - val) < SNAP); - const closeToEdgeX = nx < SNAP || nx > (1 - SNAP); - const closeToEdgeY = ny < SNAP || ny > (1 - SNAP); - - if (!closeToX && !closeToY && !closeToEdgeX && !closeToEdgeY) { - const distRight = 1 - nx; const distBottom = 1 - ny; - const minXDist = Math.min(nx, distRight); - const minYDist = Math.min(ny, distBottom); - setPhantomAxis(minXDist < minYDist ? 'y' : 'x'); - } else { - setPhantomAxis(null); - } - }; - - const handleSplitHover = (e: React.MouseEvent, axis: Axis, index: number) => { - if (dragging) return; - e.stopPropagation(); - setHoveredSplit({ axis, index }); - setPhantomAxis(null); }; const handleMouseDown = (e: React.MouseEvent) => { - if (hoveredSplit && !isButtonHovered) { - if (e.button === 0) setDragging(hoveredSplit); - else if (e.button === 2) removeSplit(hoveredSplit.axis, hoveredSplit.index); - } else if (phantomAxis && !isButtonHovered) { - const val = phantomAxis === 'x' ? mousePos.x : mousePos.y; - const newSplits = { x: [...safeX], y: [...safeY] }; - newSplits[phantomAxis] = [...newSplits[phantomAxis], val]; - onChange(newSplits); - setDragging({ axis: phantomAxis, index: newSplits[phantomAxis].length - 1 }); + if (mode === 'lines') { + if (hoveredSplit && !isButtonHovered) { + if (e.button === 0) setDragging(hoveredSplit); + else if (e.button === 2) removeMainSplit(hoveredSplit.axis, hoveredSplit.index); + } else if (phantomAxis && !isButtonHovered) { + const val = phantomAxis === 'x' ? mousePos.x : mousePos.y; + const newSplits = { ...splits, x: [...safeX], y: [...safeY] }; + newSplits[phantomAxis] = [...newSplits[phantomAxis], val]; + onChange(newSplits); + setDragging({ axis: phantomAxis, index: newSplits[phantomAxis].length - 1 }); + } + } else { + if (e.target === svgRef.current) setEditingCell(null); } }; - const removeSplit = (axis: Axis, index: number) => { - const newSplits = { x: [...safeX], y: [...safeY] }; + const removeMainSplit = (axis: Axis, index: number) => { + const newSplits = { ...splits, x: [...safeX], y: [...safeY] }; newSplits[axis] = newSplits[axis].filter((_, i) => i !== index); onChange(newSplits); setHoveredSplit(null); @@ -99,62 +147,68 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { }; return ( -
-
+
+

2. Редактор макета

- + +
+ +
-
+
-
+ {/* Instructions */} +
- Инструкция + {mode === 'lines' ? 'Режим: Границы' : 'Режим: Ячейки'}
-
    -
  • Клик у края: Новая линия
  • -
  • Перетаскивание: Изменить размер
  • -
  • Двойной клик/ПКМ: Удалить
  • -
+ {mode === 'lines' ? ( +
    +
  • Клик: Новая граница
  • +
  • Драг: Двигать
  • +
+ ) : ( +
    +
  • Клик по ячейке: Настройка стенок
  • +
+ )}
+ {/* Rulers */}
0 {config.drawer.width} мм
-
+
0 {config.drawer.depth} мм
-
- setDragging(null)} - onContextMenu={(e) => e.preventDefault()} + setDragging(null)} onContextMenu={(e) => e.preventDefault()} > @@ -163,113 +217,141 @@ export const LayoutStep: React.FC = ({ config, splits, onChange }) => { - {/* --- Labels --- */} + {/* --- Cells & Partitions --- */} {sortedX.slice(0, -1).map((x1, i) => { const x2 = sortedX[i + 1]; return sortedY.slice(0, -1).map((y1, j) => { - const y2 = sortedY[j + 1]; - const width = (x2 - x1) * config.drawer.width; - const depth = (y2 - y1) * config.drawer.depth; - const centerX = ((x1 + x2) / 2) * viewBoxW; - const centerY = ((y1 + y2) / 2) * viewBoxH; - - const cellWidthSVG = (x2 - x1) * viewBoxW; - const cellHeightSVG = (y2 - y1) * viewBoxH; - - let fontSize = Math.min(36, cellHeightSVG * 0.6); - fontSize = Math.min(fontSize, cellWidthSVG * 0.25); - - if (fontSize < 10) return null; + const cellX = x1 * viewBoxW; + const cellY = y1 * viewBoxH; + const cellW = (x2 - x1) * viewBoxW; + const cellH = (y2 - y1) * viewBoxH; + const isSelected = editingCell?.i === i && editingCell?.j === j; + const parts = safePartitions[`${i}-${j}`] || []; return ( - - {width.toFixed(0)} × {depth.toFixed(0)} - + + { if (mode === 'cells') { e.stopPropagation(); setEditingCell({ i, j }); } }} + /> + {/* Render Partitions */} + {parts.map(p => { + if (p.axis === 'x') { + const px = cellX + (cellW * p.offset); + return ; + } else { + const py = cellY + (cellH * p.offset); + return ; + } + })} + ); }); })} - - {/* --- X Lines (Vertical) --- */} - {safeX.map((x, i) => { - const isHovered = hoveredSplit?.axis === 'x' && hoveredSplit.index === i; - const isDragging = dragging?.axis === 'x' && dragging.index === i; - const color = isHovered || isDragging ? '#f59e0b' : '#64748b'; - const width = isHovered || isDragging ? 8 : 4; - return ( - removeSplit('x', i)} - onMouseMove={(e) => handleSplitHover(e, 'x', i)} - > - - - {(isHovered || isDragging) && ( - { e.stopPropagation(); removeSplit('x', i); }} - onMouseEnter={() => setIsButtonHovered(true)} onMouseLeave={() => setIsButtonHovered(false)} - > - - - - )} - - ); - })} + {/* --- Main Grid Lines --- */} + {safeX.map((x, i) => ( + { if (mode === 'lines' && !dragging) { e.stopPropagation(); setHoveredSplit({ axis: 'x', index: i }); setPhantomAxis(null); } }}> + + + {mode === 'lines' && hoveredSplit?.axis === 'x' && hoveredSplit.index === i && ( + { e.stopPropagation(); removeMainSplit('x', i); }} onMouseEnter={() => setIsButtonHovered(true)} onMouseLeave={() => setIsButtonHovered(false)}> + + + + )} + + ))} - {/* --- Y Lines (Horizontal) --- */} - {safeY.map((y, i) => { - const isHovered = hoveredSplit?.axis === 'y' && hoveredSplit.index === i; - const isDragging = dragging?.axis === 'y' && dragging.index === i; - const color = isHovered || isDragging ? '#f59e0b' : '#64748b'; - const width = isHovered || isDragging ? 8 : 4; + {safeY.map((y, i) => ( + { if (mode === 'lines' && !dragging) { e.stopPropagation(); setHoveredSplit({ axis: 'y', index: i }); setPhantomAxis(null); } }}> + + + {mode === 'lines' && hoveredSplit?.axis === 'y' && hoveredSplit.index === i && ( + { e.stopPropagation(); removeMainSplit('y', i); }} onMouseEnter={() => setIsButtonHovered(true)} onMouseLeave={() => setIsButtonHovered(false)}> + + + + )} + + ))} - return ( - removeSplit('y', i)} - onMouseMove={(e) => handleSplitHover(e, 'y', i)} - > - - - {(isHovered || isDragging) && ( - { e.stopPropagation(); removeSplit('y', i); }} - onMouseEnter={() => setIsButtonHovered(true)} onMouseLeave={() => setIsButtonHovered(false)} - > - - - - )} - - ); - })} - - {/* --- Phantom Lines --- */} - {!hoveredSplit && !dragging && !isButtonHovered && phantomAxis === 'x' && ( - - - - - )} - {!hoveredSplit && !dragging && !isButtonHovered && phantomAxis === 'y' && ( - - - - - )} + {mode === 'lines' && !hoveredSplit && !dragging && phantomAxis === 'x' && } + {mode === 'lines' && !hoveredSplit && !dragging && phantomAxis === 'y' && }
+ + {/* --- MODAL EDITOR FOR CELLS --- */} + {mode === 'cells' && editingCell && ( +
+
+

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

+ +
+ +
+ + +
+ +
+ {getCurrentPartitions().map((p, idx) => ( +
+
+ + #{idx+1} {p.axis === 'x' ? 'Вертикальная' : 'Горизонтальная'} + + +
+ +
+
+
+ Позиция {(p.offset * 100).toFixed(0)}% +
+ updatePartition(p.id, { offset: parseFloat(e.target.value) })} + className="w-full h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-purple-500" + /> +
+ +
+
+ Высота {p.height} мм +
+ updatePartition(p.id, { height: parseFloat(e.target.value) })} + className="w-full h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-blue-500" + /> +
+ +
+ updatePartition(p.id, { rounded: e.target.checked })} + className="rounded bg-slate-700 border-slate-600 text-purple-500 focus:ring-0" + /> + +
+
+
+ ))} + {getCurrentPartitions().length === 0 && ( +
+ Нет перегородок +
+ )} +
+
+ )}
diff --git a/src/components/PreviewStep.tsx b/src/components/PreviewStep.tsx index c9f9aca..3faaeaf 100644 --- a/src/components/PreviewStep.tsx +++ b/src/components/PreviewStep.tsx @@ -33,8 +33,15 @@ interface BinMeshProps { const BinMesh: React.FC = ({ part, thickness, cornerRadius, isSelected, onClick }) => { const geometry = useMemo(() => { - return createBinGeometry(part.width, part.depth, part.height, thickness, cornerRadius); - }, [part, thickness, cornerRadius]); + return createBinGeometry( + part.width, + part.depth, + part.height, + thickness, + cornerRadius, + part.internalPartitions // <--- ВАЖНО: передаем перегородки + ); +}, [part, thickness, cornerRadius]); const edgesGeometry = useMemo(() => { return new THREE.EdgesGeometry(geometry, 20); diff --git a/src/services/geometryGenerator.ts b/src/services/geometryGenerator.ts index 8ad4d65..7d1c021 100644 --- a/src/services/geometryGenerator.ts +++ b/src/services/geometryGenerator.ts @@ -1,16 +1,12 @@ import * as THREE from 'three'; import { STLExporter, mergeBufferGeometries } from 'three-stdlib'; -import { AppConfig, LayoutSplits, GeneratedPart } from '../types'; +import { AppConfig, LayoutSplits, GeneratedPart, Partition } from '../types'; -export const calculateParts = ( - config: AppConfig, - splits: LayoutSplits -): GeneratedPart[] => { +export const calculateParts = (config: AppConfig, splits: LayoutSplits): GeneratedPart[] => { const parts: GeneratedPart[] = []; - - // Safe Access const safeX = splits.x || []; const safeY = splits.y || []; + const safeParts = splits.partitions || {}; const xPoints = [0, ...[...safeX].sort((a, b) => a - b), 1]; const yPoints = [0, ...[...safeY].sort((a, b) => a - b), 1]; @@ -19,20 +15,21 @@ export const calculateParts = ( for (let i = 0; i < xPoints.length - 1; i++) { for (let j = 0; j < yPoints.length - 1; j++) { - - const segmentX = xPoints[i] * config.drawer.width; - const segmentY = yPoints[j] * config.drawer.depth; - const segmentW = (xPoints[i + 1] - xPoints[i]) * config.drawer.width; - const segmentD = (yPoints[j + 1] - yPoints[j]) * config.drawer.depth; + const rawX = xPoints[i] * config.drawer.width; + const rawY = yPoints[j] * config.drawer.depth; + const rawW = (xPoints[i + 1] - xPoints[i]) * config.drawer.width; + const rawD = (yPoints[j + 1] - yPoints[j]) * config.drawer.depth; - const realWidth = segmentW - config.printerTolerance; - const realDepth = segmentD - config.printerTolerance; - const realX = segmentX + (config.printerTolerance / 2); - const realY = segmentY + (config.printerTolerance / 2); + // Получаем перегородки для этой ячейки + const internalPartitions = safeParts[`${i}-${j}`] || []; - if (realWidth < 5 || realDepth < 5) { - continue; - } + // Рассчитываем реальные размеры с учетом допуска принтера + const realWidth = rawW - config.printerTolerance; + const realDepth = rawD - config.printerTolerance; + const realX = rawX + (config.printerTolerance / 2); + const realY = rawY + (config.printerTolerance / 2); + + if (realWidth < 5 || realDepth < 5) continue; parts.push({ id: `part-${partCounter}`, @@ -42,15 +39,18 @@ export const calculateParts = ( height: config.drawer.height, x: realX, y: realY, - color: `hsl(${Math.random() * 360}, 70%, 50%)` + color: `hsl(${Math.random() * 360}, 70%, 50%)`, + internalPartitions: internalPartitions }); partCounter++; } } - return parts; }; +// --- GEOMETRY GENERATION --- + +// Создает форму скругленного прямоугольника (или обычного) const createRoundedRectShape = (width: number, height: number, radius: number): THREE.Shape => { const shape = new THREE.Shape(); const x = -width / 2; @@ -75,20 +75,25 @@ const createRoundedRectShape = (width: number, height: number, radius: number): shape.quadraticCurveTo(x, y, x, y + r); } return shape; -} +}; +// Генерирует геометрию ячейки С ПЕРЕГОРОДКАМИ export const createBinGeometry = ( width: number, depth: number, height: number, thickness: number, - radius: number = 0 + radius: number = 0, + partitions: Partition[] = [] ): THREE.BufferGeometry => { + + const geometries: THREE.BufferGeometry[] = []; + + // 1. ОСНОВНАЯ КОРОБКА (Дно + Стенки) const floorShape = createRoundedRectShape(width, depth, radius); - const floorGeo = new THREE.ExtrudeGeometry(floorShape, { - depth: thickness, bevelEnabled: false, curveSegments: 16 - }); + const floorGeo = new THREE.ExtrudeGeometry(floorShape, { depth: thickness, bevelEnabled: false, curveSegments: 12 }); floorGeo.rotateX(-Math.PI / 2); + geometries.push(floorGeo); const outerShape = createRoundedRectShape(width, depth, radius); const innerRadius = Math.max(0, radius - thickness); @@ -101,19 +106,67 @@ export const createBinGeometry = ( } const wallHeight = height - thickness; - const wallGeo = new THREE.ExtrudeGeometry(outerShape, { - depth: wallHeight, bevelEnabled: false, curveSegments: 16 - }); - + const wallGeo = new THREE.ExtrudeGeometry(outerShape, { depth: wallHeight, bevelEnabled: false, curveSegments: 12 }); wallGeo.rotateX(-Math.PI / 2); wallGeo.translate(0, thickness, 0); + geometries.push(wallGeo); - const merged = mergeBufferGeometries([floorGeo, wallGeo]); + // 2. ВНУТРЕННИЕ ПЕРЕГОРОДКИ + // Мы создаем их внутри внутреннего пространства (innerWidth/innerDepth) + partitions.forEach(p => { + // Размеры перегородки + let pWidth = 0; + let pDepth = 0; + + // Позиция центра перегородки относительно центра ящика + let pX = 0; + let pY = 0; // (это Z в 3D) + + if (p.axis === 'x') { + // Вертикальная палка (делит ширину) + pWidth = thickness; + // Длина палки равна внутренней глубине ящика + pDepth = innerDepth; + + // Смещение: p.offset (0..1) переводим в координаты. + // innerLeft = -innerWidth/2. Position = innerLeft + (innerWidth * offset) + pX = (-innerWidth / 2) + (innerWidth * p.offset); + pY = 0; // По центру глубины + } else { + // Горизонтальная палка (делит глубину) + pWidth = innerWidth; + pDepth = thickness; + + pX = 0; // По центру ширины + pY = (-innerDepth / 2) + (innerDepth * p.offset); + } + + // Форма перегородки (скругленная или нет) + // Если скругленная, радиус берем такой же как у основной стенки, но не больше половины толщины + const pRadius = p.rounded ? Math.min(radius, thickness / 1.5) : 0; + + const partShape = createRoundedRectShape(pWidth, pDepth, pRadius); + const partGeo = new THREE.ExtrudeGeometry(partShape, { + depth: p.height, // Высота перегородки (может отличаться от основной) + bevelEnabled: false, + curveSegments: 8 + }); + + partGeo.rotateX(-Math.PI / 2); + // Поднимаем на толщину дна + partGeo.translate(pX, thickness, pY); + + geometries.push(partGeo); + }); + + // 3. СЛИЯНИЕ + const merged = mergeBufferGeometries(geometries); if (merged) merged.computeVertexNormals(); return merged || new THREE.BoxGeometry(1, 1, 1); }; +// ... Остальной код экспорта (без изменений) ... export const generateSTL = (mesh: THREE.Object3D): Uint8Array | string => { const exporter = new STLExporter(); const result = exporter.parse(mesh, { binary: true }); diff --git a/src/types.ts b/src/types.ts index 1bdb5ff..92c517a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -11,9 +11,20 @@ export interface AppConfig { cornerRadius: number; } +// Описание одной внутренней перегородки +export interface Partition { + id: string; + axis: 'x' | 'y'; // x - вертикальная палка, y - горизонтальная + offset: number; // позиция от 0 до 100% (0.5 = центр) + height: number; // высота стенки в мм + rounded: boolean; // скруглять ли края этой стенки +} + export interface LayoutSplits { x: number[]; y: number[]; + // Ключ: индекс ячейки "i-j", Значение: массив перегородок + partitions: Record; } export interface GeneratedPart { @@ -25,4 +36,6 @@ export interface GeneratedPart { x: number; y: number; color: string; + // Передаем перегородки в генератор + internalPartitions: Partition[]; } \ No newline at end of file