Merge pull request 'test' (#2) from test into main
Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
13
src/App.tsx
13
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<AppConfig>({
|
||||
drawer: { width: 300, depth: 400, height: 80 },
|
||||
wallThickness: 1.2,
|
||||
printerTolerance: 0.5,
|
||||
printerTolerance: 0.5,
|
||||
cornerRadius: 4, // <--- Дефолтное скругление (4мм)
|
||||
});
|
||||
|
||||
const [splits, setSplits] = useState<LayoutSplits>({
|
||||
@@ -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 && (
|
||||
<div className="h-[calc(100vh-200px)] min-h-[600px] animate-fade-in">
|
||||
{/* Передаем splits, чтобы кнопка Share могла их использовать */}
|
||||
<PreviewStep parts={parts} config={config} splits={splits} />
|
||||
</div>
|
||||
)}
|
||||
@@ -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"
|
||||
|
||||
@@ -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<Props> = ({ config, onChange }) => {
|
||||
<Layers className="text-accent" size={18} /> Параметры печати
|
||||
</h3>
|
||||
|
||||
{/* Wall Thickness */}
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="text-sm font-medium text-gray-300">Толщина стенок</label>
|
||||
@@ -80,7 +81,6 @@ export const ConfigStep: React.FC<Props> = ({ config, onChange }) => {
|
||||
{config.wallThickness.toFixed(1)} мм
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-xs text-gray-500 font-mono">0.4</span>
|
||||
<input
|
||||
@@ -96,6 +96,32 @@ export const ConfigStep: React.FC<Props> = ({ config, onChange }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Corner Radius (NEW) */}
|
||||
<div className="mb-6">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="text-sm font-medium text-gray-300 flex items-center gap-1">
|
||||
<CircleDashed size={14} className="text-gray-400"/> Радиус скругления
|
||||
</label>
|
||||
<span className="text-purple-400 font-bold bg-purple-400/10 px-2 py-1 rounded text-sm border border-purple-400/20">
|
||||
{config.cornerRadius?.toFixed(0) || 0} мм
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-xs text-gray-500 font-mono">0</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0"
|
||||
max="20"
|
||||
step="1"
|
||||
value={config.cornerRadius || 0}
|
||||
onChange={(e) => 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"
|
||||
/>
|
||||
<span className="text-xs text-gray-500 font-mono">20</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Printer Tolerance */}
|
||||
<div className="mb-4">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<label className="text-sm font-medium text-gray-300 flex items-center gap-1">
|
||||
@@ -105,7 +131,6 @@ export const ConfigStep: React.FC<Props> = ({ config, onChange }) => {
|
||||
{config.printerTolerance.toFixed(1)} мм
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-xs text-gray-500 font-mono">0.0</span>
|
||||
<input
|
||||
@@ -121,11 +146,6 @@ export const ConfigStep: React.FC<Props> = ({ config, onChange }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-slate-900/50 p-3 rounded border border-slate-700/50">
|
||||
<p className="text-xs text-gray-400 leading-relaxed">
|
||||
<span className="text-accent font-semibold">Зазор</span> уменьшает размер каждой ячейки, чтобы они легко вставлялись в ящик и друг в друга. Рекомендуется 0.5 мм.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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';
|
||||
@@ -26,23 +26,35 @@ const DrawerFrame = ({ config }: { config: AppConfig }) => {
|
||||
interface BinMeshProps {
|
||||
part: GeneratedPart;
|
||||
thickness: number;
|
||||
cornerRadius: number;
|
||||
isSelected: boolean;
|
||||
onClick: () => void;
|
||||
}
|
||||
|
||||
const BinMesh: React.FC<BinMeshProps> = ({ part, thickness, isSelected, onClick }) => {
|
||||
const geometry = React.useMemo(() => {
|
||||
return createBinGeometry(part.width, part.depth, part.height, thickness);
|
||||
}, [part, thickness]);
|
||||
const BinMesh: React.FC<BinMeshProps> = ({ part, thickness, cornerRadius, isSelected, onClick }) => {
|
||||
const geometry = useMemo(() => {
|
||||
return createBinGeometry(part.width, part.depth, part.height, thickness, cornerRadius);
|
||||
}, [part, thickness, cornerRadius]);
|
||||
|
||||
const edgesGeometry = useMemo(() => {
|
||||
return new THREE.EdgesGeometry(geometry, 20);
|
||||
}, [geometry]);
|
||||
|
||||
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}/>
|
||||
<meshStandardMaterial
|
||||
color={isSelected ? '#f59e0b' : part.color}
|
||||
roughness={0.5}
|
||||
metalness={0.1}
|
||||
side={THREE.DoubleSide} // <--- ИСПРАВЛЕНИЕ: Рисуем обе стороны стенки
|
||||
/>
|
||||
</mesh>
|
||||
|
||||
{/* Белая обводка */}
|
||||
{isSelected && (
|
||||
<lineSegments position={[0, part.height/2, 0]}>
|
||||
<edgesGeometry args={[new THREE.BoxGeometry(part.width, part.height, part.depth)]} />
|
||||
<lineSegments geometry={edgesGeometry}>
|
||||
<lineBasicMaterial color="white" linewidth={2} />
|
||||
</lineSegments>
|
||||
)}
|
||||
@@ -70,7 +82,7 @@ export const PreviewStep: React.FC<Props> = ({ parts, config, splits }) => {
|
||||
}, [selectedId]);
|
||||
|
||||
const handleDownload = (part: GeneratedPart) => {
|
||||
const geometry = createBinGeometry(part.width, part.depth, part.height, config.wallThickness);
|
||||
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 +93,7 @@ export const PreviewStep: React.FC<Props> = ({ parts, config, splits }) => {
|
||||
try {
|
||||
const zip = new JSZip();
|
||||
parts.forEach(part => {
|
||||
const geometry = createBinGeometry(part.width, part.depth, part.height, config.wallThickness);
|
||||
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 +112,14 @@ export const PreviewStep: React.FC<Props> = ({ 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 +133,8 @@ export const PreviewStep: React.FC<Props> = ({ 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 +145,12 @@ export const PreviewStep: React.FC<Props> = ({ parts, config, splits }) => {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
{/* Верхняя панель: Размеры + Поделиться */}
|
||||
<div className="flex flex-col xl:flex-row justify-between items-center bg-slate-800/80 p-4 rounded-xl border border-slate-700 mb-4 gap-4 backdrop-blur-sm shadow-lg">
|
||||
|
||||
<div className="flex flex-wrap items-center gap-6 justify-center md:justify-start">
|
||||
<div className="hidden md:flex items-center gap-2 text-gray-300 mr-2">
|
||||
<Ruler className="text-primary" size={20} />
|
||||
<span className="font-medium text-sm uppercase tracking-wide opacity-70">Размеры ящика:</span>
|
||||
</div>
|
||||
|
||||
{/* --- ОБНОВЛЕННЫЙ БЛОК РАЗМЕРОВ --- */}
|
||||
<div className="flex flex-wrap gap-6 font-mono text-white items-baseline">
|
||||
<div className="flex items-baseline gap-2">
|
||||
<span className="text-slate-500 text-sm font-bold uppercase tracking-wider">Ширина:</span>
|
||||
@@ -166,7 +167,6 @@ export const PreviewStep: React.FC<Props> = ({ parts, config, splits }) => {
|
||||
<span className="text-sm text-slate-500 font-bold self-baseline">мм</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleShare}
|
||||
className={`
|
||||
@@ -183,7 +183,6 @@ export const PreviewStep: React.FC<Props> = ({ parts, config, splits }) => {
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col lg:flex-row h-full gap-6 relative flex-1 min-h-0">
|
||||
{/* 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">
|
||||
@@ -216,7 +215,10 @@ export const PreviewStep: React.FC<Props> = ({ parts, config, splits }) => {
|
||||
<DrawerFrame config={config} />
|
||||
{parts.map(part => (
|
||||
<BinMesh
|
||||
key={part.id} part={part} thickness={config.wallThickness}
|
||||
key={part.id}
|
||||
part={part}
|
||||
thickness={config.wallThickness}
|
||||
cornerRadius={config.cornerRadius || 0}
|
||||
isSelected={selectedId === part.id}
|
||||
onClick={() => setSelectedId(part.id)}
|
||||
/>
|
||||
@@ -228,7 +230,6 @@ export const PreviewStep: React.FC<Props> = ({ parts, config, splits }) => {
|
||||
</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">
|
||||
|
||||
@@ -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,12 @@ 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 +25,14 @@ 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.
|
||||
// Применяем зазор (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 +54,116 @@ 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.
|
||||
* Создает 2D форму прямоугольника со скругленными краями
|
||||
*/
|
||||
const createRoundedRectShape = (width: number, height: number, radius: number): THREE.Shape => {
|
||||
const shape = new THREE.Shape();
|
||||
const x = -width / 2;
|
||||
const y = -height / 2;
|
||||
|
||||
// Ограничиваем радиус, чтобы он не сломал геометрию (не больше половины стороны)
|
||||
const r = Math.min(radius, width / 2, height / 2);
|
||||
|
||||
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 {
|
||||
// Прямоугольник со скруглениями
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Генерирует 3D геометрию ящика
|
||||
*/
|
||||
export const createBinGeometry = (
|
||||
width: number,
|
||||
depth: number,
|
||||
height: number,
|
||||
thickness: number
|
||||
thickness: number,
|
||||
radius: number = 0
|
||||
): 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. ГЕОМЕТРИЯ ДНА (Сплошная)
|
||||
const floorShape = createRoundedRectShape(width, depth, radius);
|
||||
|
||||
const floorGeo = new THREE.ExtrudeGeometry(floorShape, {
|
||||
depth: thickness, // Выдавливаем на толщину дна
|
||||
bevelEnabled: false,
|
||||
curveSegments: 16 // Количество сегментов на скруглениях
|
||||
});
|
||||
|
||||
// Extrude выдавливает по оси Z. Нам нужно повернуть, чтобы "глубина" стала "высотой" (Y).
|
||||
// Поворот на -90 градусов вокруг X кладет Z на Y.
|
||||
floorGeo.rotateX(-Math.PI / 2);
|
||||
// Теперь дно занимает пространство от Y=0 до Y=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. ГЕОМЕТРИЯ СТЕНОК (С дыркой)
|
||||
const outerShape = createRoundedRectShape(width, depth, radius);
|
||||
|
||||
// Вырезаем внутреннюю часть
|
||||
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: 16
|
||||
});
|
||||
|
||||
// Поворачиваем стенки так же, как дно
|
||||
wallGeo.rotateX(-Math.PI / 2);
|
||||
|
||||
// Сейчас стенки тоже начинаются с Y=0.
|
||||
// Нам нужно поднять их НАД дном.
|
||||
wallGeo.translate(0, thickness, 0);
|
||||
|
||||
// Теперь стенки занимают пространство от Y=thickness до Y=height.
|
||||
|
||||
// 3. ОБЪЕДИНЕНИЕ
|
||||
// Сливаем две геометрии в одну. Слайсеры поймут это как единый объект,
|
||||
// так как поверхности идеально соприкасаются.
|
||||
const merged = mergeBufferGeometries([floorGeo, wallGeo]);
|
||||
|
||||
// Центрирование не нужно, так как createRoundedRectShape строит форму вокруг (0,0) по X и Z.
|
||||
// А по Y мы выстроили от 0 вверх.
|
||||
// Pivot point (опорная точка) осталась внизу в центре (0,0,0), что идеально для позиционирования.
|
||||
|
||||
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' });
|
||||
|
||||
28
src/types.ts
Normal file
28
src/types.ts
Normal file
@@ -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;
|
||||
}
|
||||
29
types.ts
29
types.ts
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user