From f43c92bb443ab5774f5ae6eef210002834053e25 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, 27 Dec 2025 20:44:50 +0300 Subject: [PATCH 1/4] Add corner --- src/App.tsx | 13 +-- src/components/ConfigStep.tsx | 36 ++++++-- src/components/PreviewStep.tsx | 42 ++++----- src/services/geometryGenerator.ts | 147 +++++++++++++++++++----------- src/types.ts | 28 ++++++ types.ts | 29 ------ 6 files changed, 169 insertions(+), 126 deletions(-) create mode 100644 src/types.ts delete mode 100644 types.ts diff --git a/src/App.tsx b/src/App.tsx index 93dc741..0120272 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,11 +1,10 @@ import React, { useState, useMemo, useEffect } from 'react'; import { AppConfig, LayoutSplits, GeneratedPart } from './types'; -// Импорт должен быть правильным, проверь путь import { calculateParts } from './services/geometryGenerator'; import { ConfigStep } from './components/ConfigStep'; import { LayoutStep } from './components/LayoutStep'; import { PreviewStep } from './components/PreviewStep'; -import { parseShareUrl } from './utils/share'; // <--- Новый импорт +import { parseShareUrl } from './utils/share'; import { ChevronRight, ChevronLeft, Box } from 'lucide-react'; const App = () => { @@ -16,7 +15,8 @@ const App = () => { const [config, setConfig] = useState({ drawer: { width: 300, depth: 400, height: 80 }, wallThickness: 1.2, - printerTolerance: 0.5, + printerTolerance: 0.5, + cornerRadius: 4, // <--- Дефолтное скругление (4мм) }); const [splits, setSplits] = useState({ @@ -30,14 +30,11 @@ const App = () => { if (sharedData) { setConfig(sharedData.config); setSplits(sharedData.splits); - setStep(3); // Сразу прыгаем на превью + setStep(3); setIsLoadedFromUrl(true); - - // Очищаем URL, чтобы он не мозолил глаза (опционально) window.history.replaceState({}, '', window.location.pathname); } }, []); - // --------------------------------------- // Derived State: Parts const parts: GeneratedPart[] = useMemo(() => { @@ -94,7 +91,6 @@ const App = () => { {step === 3 && (
- {/* Передаем splits, чтобы кнопка Share могла их использовать */}
)} @@ -127,7 +123,6 @@ const App = () => { onClick={() => { setStep(1); setSplits({x: [], y: []}); - // Сбрасываем URL если он был window.history.replaceState({}, '', window.location.pathname); }} className="flex items-center gap-2 px-6 py-3 rounded-lg font-semibold text-gray-400 hover:text-white transition-colors border border-transparent hover:border-slate-700" diff --git a/src/components/ConfigStep.tsx b/src/components/ConfigStep.tsx index 088f867..37c856e 100644 --- a/src/components/ConfigStep.tsx +++ b/src/components/ConfigStep.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { AppConfig } from '../types'; -import { Ruler, Box, Layers, Minimize2 } from 'lucide-react'; +import { Ruler, Box, Layers, Minimize2, CircleDashed } from 'lucide-react'; interface Props { config: AppConfig; @@ -73,6 +73,7 @@ export const ConfigStep: React.FC = ({ config, onChange }) => { Параметры печати + {/* Wall Thickness */}
@@ -80,7 +81,6 @@ export const ConfigStep: React.FC = ({ config, onChange }) => { {config.wallThickness.toFixed(1)} мм
-
0.4 = ({ config, onChange }) => {
+ {/* Corner Radius (NEW) */} +
+
+ + + {config.cornerRadius?.toFixed(0) || 0} мм + +
+
+ 0 + onChange({...config, cornerRadius: parseFloat(e.target.value)})} + className="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-purple-500 hover:accent-purple-400 transition-all" + /> + 20 +
+
+ + {/* Printer Tolerance */}
-
0.0 = ({ config, onChange }) => {
-
-

- Зазор уменьшает размер каждой ячейки, чтобы они легко вставлялись в ящик и друг в друга. Рекомендуется 0.5 мм. -

-
diff --git a/src/components/PreviewStep.tsx b/src/components/PreviewStep.tsx index 132e489..a637cf7 100644 --- a/src/components/PreviewStep.tsx +++ b/src/components/PreviewStep.tsx @@ -8,7 +8,6 @@ import { createBinGeometry, generateSTL, exportSTL } from '../services/geometryG import { Download, Package, Info, Loader2, Share2, Check, Ruler } from 'lucide-react'; import { generateShareUrl } from '../utils/share'; -// --- DrawerFrame (Каркас) --- const DrawerFrame = ({ config }: { config: AppConfig }) => { const { width, depth, height } = config.drawer; const offset = 0.5; @@ -22,18 +21,19 @@ const DrawerFrame = ({ config }: { config: AppConfig }) => { ) } -// --- BinMesh (Ячейка) --- interface BinMeshProps { part: GeneratedPart; thickness: number; + cornerRadius: number; // <--- ADDED PROP isSelected: boolean; onClick: () => void; } -const BinMesh: React.FC = ({ part, thickness, isSelected, onClick }) => { +const BinMesh: React.FC = ({ part, thickness, cornerRadius, isSelected, onClick }) => { const geometry = React.useMemo(() => { - return createBinGeometry(part.width, part.depth, part.height, thickness); - }, [part, thickness]); + // PASS cornerRadius HERE + return createBinGeometry(part.width, part.depth, part.height, thickness, cornerRadius); + }, [part, thickness, cornerRadius]); return ( @@ -42,6 +42,7 @@ const BinMesh: React.FC = ({ part, thickness, isSelected, onClick {isSelected && ( + {/* Для подсветки оставляем обычный бокс, т.к. edgesGeometry на extrude выглядит грязно */} @@ -50,7 +51,6 @@ const BinMesh: React.FC = ({ part, thickness, isSelected, onClick ); }; -// --- PreviewStep (Основной) --- interface Props { parts: GeneratedPart[]; config: AppConfig; @@ -70,7 +70,8 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { }, [selectedId]); const handleDownload = (part: GeneratedPart) => { - const geometry = createBinGeometry(part.width, part.depth, part.height, config.wallThickness); + // PASS config.cornerRadius HERE + const geometry = createBinGeometry(part.width, part.depth, part.height, config.wallThickness, config.cornerRadius); const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial()); exportSTL(mesh, `${part.name.replace(/\s+/g, '_')}.stl`); }; @@ -81,7 +82,8 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { try { const zip = new JSZip(); parts.forEach(part => { - const geometry = createBinGeometry(part.width, part.depth, part.height, config.wallThickness); + // PASS config.cornerRadius HERE + const geometry = createBinGeometry(part.width, part.depth, part.height, config.wallThickness, config.cornerRadius); const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial()); const stlData = generateSTL(mesh); zip.file(`${part.name.replace(/\s+/g, '_')}.stl`, stlData); @@ -100,18 +102,14 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { } }; - // --- Функция копирования --- const handleShare = async () => { const url = generateShareUrl(config, splits); let success = false; - try { if (navigator.clipboard && navigator.clipboard.writeText) { await navigator.clipboard.writeText(url); success = true; - } else { - throw new Error('Clipboard API unavailable'); - } + } else { throw new Error('Clipboard API unavailable'); } } catch (err) { try { const textArea = document.createElement("textarea"); @@ -125,11 +123,8 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { const result = document.execCommand('copy'); document.body.removeChild(textArea); if (result) success = true; - } catch (e) { - console.error("Copy failed", e); - } + } catch (e) { console.error("Copy failed", e); } } - if (success) { setShareUrlCopied(true); setTimeout(() => setShareUrlCopied(false), 3000); @@ -140,16 +135,12 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { return (
- {/* Верхняя панель: Размеры + Поделиться */}
-
Размеры ящика:
- - {/* --- ОБНОВЛЕННЫЙ БЛОК РАЗМЕРОВ --- */}
Ширина: @@ -166,7 +157,6 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { мм
-
- {/* 3D Viewer */}
@@ -195,7 +184,6 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => {
  • • Скролл: Масштаб
  • - = ({ parts, config, splits }) => { {parts.map(part => ( setSelectedId(part.id)} /> @@ -228,7 +219,6 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => {
    - {/* Sidebar List */}

    diff --git a/src/services/geometryGenerator.ts b/src/services/geometryGenerator.ts index b55cf78..b83a9db 100644 --- a/src/services/geometryGenerator.ts +++ b/src/services/geometryGenerator.ts @@ -4,7 +4,6 @@ import { AppConfig, LayoutSplits, GeneratedPart } from '../types'; /** * Calculates the final list of bins based on layout. - * Removes printer bed constraints logic. */ export const calculateParts = ( config: AppConfig, @@ -12,14 +11,11 @@ export const calculateParts = ( ): GeneratedPart[] => { const parts: GeneratedPart[] = []; - // 1. Sort splits to create segments - // CRITICAL FIX: Create a copy of the array before sorting to avoid mutating React state const xPoints = [0, ...[...splits.x].sort((a, b) => a - b), 1]; const yPoints = [0, ...[...splits.y].sort((a, b) => a - b), 1]; let partCounter = 1; - // 2. Iterate through the grid defined by the user for (let i = 0; i < xPoints.length - 1; i++) { for (let j = 0; j < yPoints.length - 1; j++) { @@ -28,16 +24,13 @@ export const calculateParts = ( const segmentW = (xPoints[i + 1] - xPoints[i]) * config.drawer.width; const segmentD = (yPoints[j + 1] - yPoints[j]) * config.drawer.depth; - // Apply Printer Tolerance (Gap) - // We subtract the tolerance from the width/depth and shift the position - // so the gap is evenly distributed around the part center. + // Apply Printer Tolerance const realWidth = segmentW - config.printerTolerance; const realDepth = segmentD - config.printerTolerance; const realX = segmentX + (config.printerTolerance / 2); const realY = segmentY + (config.printerTolerance / 2); - // Filter out extremely small parts (likely errors/double lines or consumed by tolerance) - if (realWidth < 1 || realDepth < 1) { + if (realWidth < 5 || realDepth < 5) { continue; } @@ -59,74 +52,120 @@ export const calculateParts = ( }; /** - * Generates a Three.js Geometry for a hollow bin. - * The floor is at y=0..thickness. - * The walls sit on top of the floor, y=thickness..height. + * Helper to create a rounded rectangle Shape + */ +const createRoundedRectShape = (width: number, height: number, radius: number): THREE.Shape => { + const shape = new THREE.Shape(); + const x = -width / 2; + const y = -height / 2; + + // Clamp radius to not exceed half of width or height + const r = Math.min(radius, width / 2, height / 2); + + if (r <= 0) { + // Regular rectangle + shape.moveTo(x, y); + shape.lineTo(x + width, y); + shape.lineTo(x + width, y + height); + shape.lineTo(x, y + height); + shape.lineTo(x, y); + } else { + // Rounded rectangle + shape.moveTo(x, y + r); + shape.lineTo(x, y + height - r); + shape.quadraticCurveTo(x, y + height, x + r, y + height); + shape.lineTo(x + width - r, y + height); + shape.quadraticCurveTo(x + width, y + height, x + width, y + height - r); + shape.lineTo(x + width, y + r); + shape.quadraticCurveTo(x + width, y, x + width - r, y); + shape.lineTo(x + r, y); + shape.quadraticCurveTo(x, y, x, y + r); + } + + return shape; +} + +/** + * Generates a Three.js Geometry for a hollow bin with fillets (rounded corners). */ export const createBinGeometry = ( width: number, depth: number, height: number, - thickness: number + thickness: number, + radius: number = 0 // Default radius ): THREE.BufferGeometry => { - const geometries: THREE.BufferGeometry[] = []; - // 1. Floor (Base) - // Positioned at the bottom (0 to thickness) - const floorGeo = new THREE.BoxGeometry(width, thickness, depth); - floorGeo.translate(0, thickness / 2, 0); - geometries.push(floorGeo); + // 1. Создаем форму ДНА (Floor) + // Это сплошной прямоугольник со скруглениями + const floorShape = createRoundedRectShape(width, depth, radius); + + const floorGeo = new THREE.ExtrudeGeometry(floorShape, { + depth: thickness, // Толщина дна + bevelEnabled: false, + curveSegments: 12 // Гладкость скругления + }); + + // ExtrudeGeometry создает объект "лежа" на XY, нам нужно повернуть его, чтобы он стал дном (XZ) + floorGeo.rotateX(Math.PI / 2); + // Сдвигаем на высоту thickness, так как extrude идет в +Z (после поворота это +Y, но перевернуто... проще подобрать) + // По умолчанию extrude создает от 0 до Z. После поворота X(90deg): + // Z становится -Y. То есть дно уходит вниз от 0. + // Нам нужно, чтобы дно было от 0 до thickness по Y. + // Возвращаем поворот в -90 (стандарт для топологии) + floorGeo.rotateX(-Math.PI); + floorGeo.translate(0, thickness, 0); // Поднимаем, чтобы верх дна был на уровне thickness - // Wall dimensions - // Walls sit ON TOP of the floor to avoid overlap/z-fighting - const wallHeight = height - thickness; - const wallCenterY = thickness + (wallHeight / 2); - if (wallHeight > 0) { - // 2. Left & Right Walls (Full depth) - const wallLRGeo = new THREE.BoxGeometry(thickness, wallHeight, depth); - - const leftWall = wallLRGeo.clone(); - leftWall.translate(-(width/2) + (thickness/2), wallCenterY, 0); - geometries.push(leftWall); - - const rightWall = wallLRGeo.clone(); - rightWall.translate((width/2) - (thickness/2), wallCenterY, 0); - geometries.push(rightWall); - - // 3. Front & Back Walls (Width minus LR thickness to fit between) - // Width is reduced by 2*thickness - const wallFBWidth = Math.max(0, width - (2 * thickness)); - const wallFBGeo = new THREE.BoxGeometry(wallFBWidth, wallHeight, thickness); - - const frontWall = wallFBGeo.clone(); - frontWall.translate(0, wallCenterY, (depth/2) - (thickness/2)); - geometries.push(frontWall); - - const backWall = wallFBGeo.clone(); - backWall.translate(0, wallCenterY, -(depth/2) + (thickness/2)); - geometries.push(backWall); + // 2. Создаем форму СТЕНОК (Walls) + // Это внешняя форма МИНУС внутренняя форма (дырка) + const outerShape = createRoundedRectShape(width, depth, radius); + + // Внутренний радиус должен быть меньше внешнего на толщину стенки, но не меньше 0 + const innerRadius = Math.max(0, radius - thickness); + const innerWidth = width - (2 * thickness); + const innerDepth = depth - (2 * thickness); + + if (innerWidth > 0 && innerDepth > 0) { + const innerHole = createRoundedRectShape(innerWidth, innerDepth, innerRadius); + outerShape.holes.push(innerHole); } - // Merge geometries into a single mesh for clean STL export - const merged = mergeBufferGeometries(geometries); + const wallHeight = height - thickness; + + const wallGeo = new THREE.ExtrudeGeometry(outerShape, { + depth: wallHeight, + bevelEnabled: false, + curveSegments: 12 + }); + + // Поворачиваем стенки так же, как дно + wallGeo.rotateX(Math.PI / 2); + wallGeo.rotateX(-Math.PI); + // Стенки начинаются ОТ дна (y = thickness) + wallGeo.translate(0, thickness + wallHeight, 0); + + // Объединяем геометрии + const merged = mergeBufferGeometries([floorGeo, wallGeo]); + + // Центрируем геометрию, чтобы Pivot был в центре дна (как раньше в BinMesh) + // Ранее мы использовали BoxGeometry который центрирован. + // Extrude создает форму вокруг (0,0), так что по X/Z она уже центрирована. + // По Y она сейчас от 0 до height. + // Вернем как было: Pivot внизу. + return merged || new THREE.BoxGeometry(1, 1, 1); }; -// Generates STL binary data without triggering download export const generateSTL = (mesh: THREE.Object3D): Uint8Array | string => { const exporter = new STLExporter(); const result = exporter.parse(mesh, { binary: true }); - - // JSZip requires Uint8Array or ArrayBuffer, not DataView if (result instanceof DataView) { return new Uint8Array(result.buffer, result.byteOffset, result.byteLength); } - return result as string; }; -// Triggers download immediately export const exportSTL = (mesh: THREE.Object3D, filename: string) => { const result = generateSTL(mesh); const blob = new Blob([result], { type: 'application/octet-stream' }); diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..d3c2012 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,28 @@ +export interface DrawerDimensions { + width: number; + depth: number; + height: number; +} + +export interface AppConfig { + drawer: DrawerDimensions; + wallThickness: number; + printerTolerance: number; + cornerRadius: number; // <--- Новое свойство +} + +export interface LayoutSplits { + x: number[]; + y: number[]; +} + +export interface GeneratedPart { + id: string; + name: string; + width: number; + depth: number; + height: number; + x: number; + y: number; + color: string; +} \ No newline at end of file diff --git a/types.ts b/types.ts deleted file mode 100644 index 5a43f7a..0000000 --- a/types.ts +++ /dev/null @@ -1,29 +0,0 @@ -export interface Dimensions { - width: number; - depth: number; - height: number; -} - -export interface AppConfig { - drawer: Dimensions; - wallThickness: number; - printerTolerance: number; // Gap between parts for fit -} - -// Represents a vertical or horizontal divider position (0 to 1) -export interface LayoutSplits { - x: number[]; // Vertical dividers (along width) - y: number[]; // Horizontal dividers (along depth) -} - -// A single generated bin part -export interface GeneratedPart { - id: string; - name: string; - width: number; - depth: number; - height: number; - x: number; // Position in drawer - y: number; // Position in drawer - color: string; -} \ No newline at end of file From 16b92ca364a8c30e8ed89ce9086f35afee3f8056 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, 27 Dec 2025 21:04:08 +0300 Subject: [PATCH 2/4] Fix create --- src/services/geometryGenerator.ts | 76 +++++++++++++++---------------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/src/services/geometryGenerator.ts b/src/services/geometryGenerator.ts index b83a9db..7dd5e51 100644 --- a/src/services/geometryGenerator.ts +++ b/src/services/geometryGenerator.ts @@ -11,6 +11,7 @@ export const calculateParts = ( ): GeneratedPart[] => { const parts: GeneratedPart[] = []; + // Сортируем линии разреза const xPoints = [0, ...[...splits.x].sort((a, b) => a - b), 1]; const yPoints = [0, ...[...splits.y].sort((a, b) => a - b), 1]; @@ -24,12 +25,13 @@ export const calculateParts = ( const segmentW = (xPoints[i + 1] - xPoints[i]) * config.drawer.width; const segmentD = (yPoints[j + 1] - yPoints[j]) * config.drawer.depth; - // Apply Printer Tolerance + // Применяем зазор (Tolerance) const realWidth = segmentW - config.printerTolerance; const realDepth = segmentD - config.printerTolerance; const realX = segmentX + (config.printerTolerance / 2); const realY = segmentY + (config.printerTolerance / 2); + // Игнорируем слишком мелкие детали if (realWidth < 5 || realDepth < 5) { continue; } @@ -52,25 +54,25 @@ export const calculateParts = ( }; /** - * Helper to create a rounded rectangle Shape + * Создает 2D форму прямоугольника со скругленными краями */ const createRoundedRectShape = (width: number, height: number, radius: number): THREE.Shape => { const shape = new THREE.Shape(); const x = -width / 2; const y = -height / 2; - // Clamp radius to not exceed half of width or height + // Ограничиваем радиус, чтобы он не сломал геометрию (не больше половины стороны) const r = Math.min(radius, width / 2, height / 2); - if (r <= 0) { - // Regular rectangle + if (r <= 0.1) { + // Обычный прямоугольник (если радиус 0) shape.moveTo(x, y); shape.lineTo(x + width, y); shape.lineTo(x + width, y + height); shape.lineTo(x, y + height); shape.lineTo(x, y); } else { - // Rounded rectangle + // Прямоугольник со скруглениями shape.moveTo(x, y + r); shape.lineTo(x, y + height - r); shape.quadraticCurveTo(x, y + height, x + r, y + height); @@ -86,42 +88,34 @@ const createRoundedRectShape = (width: number, height: number, radius: number): } /** - * Generates a Three.js Geometry for a hollow bin with fillets (rounded corners). + * Генерирует 3D геометрию ящика */ export const createBinGeometry = ( width: number, depth: number, height: number, thickness: number, - radius: number = 0 // Default radius + radius: number = 0 ): THREE.BufferGeometry => { - // 1. Создаем форму ДНА (Floor) - // Это сплошной прямоугольник со скруглениями + // 1. ГЕОМЕТРИЯ ДНА (Сплошная) const floorShape = createRoundedRectShape(width, depth, radius); const floorGeo = new THREE.ExtrudeGeometry(floorShape, { - depth: thickness, // Толщина дна + depth: thickness, // Выдавливаем на толщину дна bevelEnabled: false, - curveSegments: 12 // Гладкость скругления + curveSegments: 16 // Количество сегментов на скруглениях }); - // ExtrudeGeometry создает объект "лежа" на XY, нам нужно повернуть его, чтобы он стал дном (XZ) - floorGeo.rotateX(Math.PI / 2); - // Сдвигаем на высоту thickness, так как extrude идет в +Z (после поворота это +Y, но перевернуто... проще подобрать) - // По умолчанию extrude создает от 0 до Z. После поворота X(90deg): - // Z становится -Y. То есть дно уходит вниз от 0. - // Нам нужно, чтобы дно было от 0 до thickness по Y. - // Возвращаем поворот в -90 (стандарт для топологии) - floorGeo.rotateX(-Math.PI); - floorGeo.translate(0, thickness, 0); // Поднимаем, чтобы верх дна был на уровне thickness + // Extrude выдавливает по оси Z. Нам нужно повернуть, чтобы "глубина" стала "высотой" (Y). + // Поворот на -90 градусов вокруг X кладет Z на Y. + floorGeo.rotateX(-Math.PI / 2); + // Теперь дно занимает пространство от Y=0 до Y=thickness. - - // 2. Создаем форму СТЕНОК (Walls) - // Это внешняя форма МИНУС внутренняя форма (дырка) + // 2. ГЕОМЕТРИЯ СТЕНОК (С дыркой) const outerShape = createRoundedRectShape(width, depth, radius); - // Внутренний радиус должен быть меньше внешнего на толщину стенки, но не меньше 0 + // Вырезаем внутреннюю часть const innerRadius = Math.max(0, radius - thickness); const innerWidth = width - (2 * thickness); const innerDepth = depth - (2 * thickness); @@ -131,29 +125,33 @@ export const createBinGeometry = ( outerShape.holes.push(innerHole); } + // Высота стенок = общая высота минус толщина дна const wallHeight = height - thickness; const wallGeo = new THREE.ExtrudeGeometry(outerShape, { depth: wallHeight, bevelEnabled: false, - curveSegments: 12 + curveSegments: 16 }); - - // Поворачиваем стенки так же, как дно - wallGeo.rotateX(Math.PI / 2); - wallGeo.rotateX(-Math.PI); - // Стенки начинаются ОТ дна (y = thickness) - wallGeo.translate(0, thickness + wallHeight, 0); - // Объединяем геометрии + // Поворачиваем стенки так же, как дно + wallGeo.rotateX(-Math.PI / 2); + + // Сейчас стенки тоже начинаются с Y=0. + // Нам нужно поднять их НАД дном. + wallGeo.translate(0, thickness, 0); + + // Теперь стенки занимают пространство от Y=thickness до Y=height. + + // 3. ОБЪЕДИНЕНИЕ + // Сливаем две геометрии в одну. Слайсеры поймут это как единый объект, + // так как поверхности идеально соприкасаются. const merged = mergeBufferGeometries([floorGeo, wallGeo]); - // Центрируем геометрию, чтобы Pivot был в центре дна (как раньше в BinMesh) - // Ранее мы использовали BoxGeometry который центрирован. - // Extrude создает форму вокруг (0,0), так что по X/Z она уже центрирована. - // По Y она сейчас от 0 до height. - // Вернем как было: Pivot внизу. - + // Центрирование не нужно, так как createRoundedRectShape строит форму вокруг (0,0) по X и Z. + // А по Y мы выстроили от 0 вверх. + // Pivot point (опорная точка) осталась внизу в центре (0,0,0), что идеально для позиционирования. + return merged || new THREE.BoxGeometry(1, 1, 1); }; From 83cbe04c13f09600268b7bf7235f63b33ceaa5b5 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, 27 Dec 2025 21:18:44 +0300 Subject: [PATCH 3/4] Fix view --- src/components/PreviewStep.tsx | 43 +++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/src/components/PreviewStep.tsx b/src/components/PreviewStep.tsx index a637cf7..2b2dc6e 100644 --- a/src/components/PreviewStep.tsx +++ b/src/components/PreviewStep.tsx @@ -1,4 +1,4 @@ -import React, { Suspense, useEffect, useRef, useState } from 'react'; +import React, { Suspense, useEffect, useRef, useState, useMemo } from 'react'; import { Canvas } from '@react-three/fiber'; import { OrbitControls, Center, Environment } from '@react-three/drei'; import * as THREE from 'three'; @@ -8,6 +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 (Каркас ящика) --- const DrawerFrame = ({ config }: { config: AppConfig }) => { const { width, depth, height } = config.drawer; const offset = 0.5; @@ -21,29 +22,43 @@ const DrawerFrame = ({ config }: { config: AppConfig }) => { ) } +// --- BinMesh (Ячейка) --- interface BinMeshProps { part: GeneratedPart; thickness: number; - cornerRadius: number; // <--- ADDED PROP + cornerRadius: number; // Важный проп для радиуса isSelected: boolean; onClick: () => void; } const BinMesh: React.FC = ({ part, thickness, cornerRadius, isSelected, onClick }) => { - const geometry = React.useMemo(() => { - // PASS cornerRadius HERE + // 1. Создаем основную геометрию ячейки + const geometry = useMemo(() => { return createBinGeometry(part.width, part.depth, part.height, thickness, cornerRadius); }, [part, thickness, cornerRadius]); + // 2. Создаем геометрию для рамки выделения + // Используем EdgesGeometry с порогом 20 градусов. + // Это скроет линии на плавных изгибах (где угол между полигонами ~5-6 градусов), + // но оставит четкие грани сверху, снизу и по углам. + const edgesGeometry = useMemo(() => { + return new THREE.EdgesGeometry(geometry, 20); + }, [geometry]); + return ( + {/* Сама модель */} { e.stopPropagation(); onClick(); }}> - + + + {/* Подсветка выделения (теперь повторяет форму скругления) */} {isSelected && ( - - {/* Для подсветки оставляем обычный бокс, т.к. edgesGeometry на extrude выглядит грязно */} - + )} @@ -51,6 +66,7 @@ const BinMesh: React.FC = ({ part, thickness, cornerRadius, isSele ); }; +// --- PreviewStep (Основной компонент) --- interface Props { parts: GeneratedPart[]; config: AppConfig; @@ -70,7 +86,6 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { }, [selectedId]); const handleDownload = (part: GeneratedPart) => { - // PASS config.cornerRadius HERE const geometry = createBinGeometry(part.width, part.depth, part.height, config.wallThickness, config.cornerRadius); const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial()); exportSTL(mesh, `${part.name.replace(/\s+/g, '_')}.stl`); @@ -82,7 +97,6 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { try { const zip = new JSZip(); parts.forEach(part => { - // PASS config.cornerRadius HERE const geometry = createBinGeometry(part.width, part.depth, part.height, config.wallThickness, config.cornerRadius); const mesh = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial()); const stlData = generateSTL(mesh); @@ -135,12 +149,15 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { return (
    + {/* Верхняя панель: Размеры + Поделиться */}
    +
    Размеры ящика:
    +
    Ширина: @@ -157,6 +174,7 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { мм
    +
    + {/* 3D Viewer */}
    @@ -184,6 +203,7 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => {
  • • Скролл: Масштаб
  • + = ({ parts, config, splits }) => { key={part.id} part={part} thickness={config.wallThickness} - cornerRadius={config.cornerRadius || 0} // PASS PROP + cornerRadius={config.cornerRadius || 0} // <-- ИСПРАВЛЕНО: Передаем радиус isSelected={selectedId === part.id} onClick={() => setSelectedId(part.id)} /> @@ -219,6 +239,7 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => {
    + {/* Sidebar List */}

    From ff3c8e2156cfd27ec769608093f164e565f6fd72 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, 27 Dec 2025 21:39:32 +0300 Subject: [PATCH 4/4] 3D view --- src/components/PreviewStep.tsx | 24 +++++++----------------- 1 file changed, 7 insertions(+), 17 deletions(-) diff --git a/src/components/PreviewStep.tsx b/src/components/PreviewStep.tsx index 2b2dc6e..b18b6d8 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; @@ -26,37 +26,33 @@ const DrawerFrame = ({ config }: { config: AppConfig }) => { interface BinMeshProps { part: GeneratedPart; thickness: number; - cornerRadius: number; // Важный проп для радиуса + cornerRadius: number; isSelected: boolean; onClick: () => void; } const BinMesh: React.FC = ({ part, thickness, cornerRadius, isSelected, onClick }) => { - // 1. Создаем основную геометрию ячейки const geometry = useMemo(() => { return createBinGeometry(part.width, part.depth, part.height, thickness, cornerRadius); }, [part, thickness, cornerRadius]); - // 2. Создаем геометрию для рамки выделения - // Используем EdgesGeometry с порогом 20 градусов. - // Это скроет линии на плавных изгибах (где угол между полигонами ~5-6 градусов), - // но оставит четкие грани сверху, снизу и по углам. const edgesGeometry = useMemo(() => { return new THREE.EdgesGeometry(geometry, 20); }, [geometry]); return ( - {/* Сама модель */} + {/* Модель */} { e.stopPropagation(); onClick(); }}> - {/* Подсветка выделения (теперь повторяет форму скругления) */} + {/* Белая обводка */} {isSelected && ( @@ -66,7 +62,7 @@ const BinMesh: React.FC = ({ part, thickness, cornerRadius, isSele ); }; -// --- PreviewStep (Основной компонент) --- +// --- PreviewStep (Основной) --- interface Props { parts: GeneratedPart[]; config: AppConfig; @@ -149,15 +145,12 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { return (
    - {/* Верхняя панель: Размеры + Поделиться */}
    -
    Размеры ящика:
    -
    Ширина: @@ -174,7 +167,6 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { мм
    -
    - {/* 3D Viewer */}
    @@ -227,7 +218,7 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => { key={part.id} part={part} thickness={config.wallThickness} - cornerRadius={config.cornerRadius || 0} // <-- ИСПРАВЛЕНО: Передаем радиус + cornerRadius={config.cornerRadius || 0} isSelected={selectedId === part.id} onClick={() => setSelectedId(part.id)} /> @@ -239,7 +230,6 @@ export const PreviewStep: React.FC = ({ parts, config, splits }) => {
    - {/* Sidebar List */}