Try add perforation
This commit is contained in:
@@ -1,153 +1,171 @@
|
||||
import React from 'react';
|
||||
import { AppConfig } from '../types';
|
||||
import { Ruler, Box, Layers, Minimize2, CircleDashed } from 'lucide-react';
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { AppConfig, PerforationPattern } from '../types';
|
||||
import { Settings2, Grid, Circle, Triangle, Hexagon, LayoutGrid } from 'lucide-react';
|
||||
|
||||
interface Props {
|
||||
config: AppConfig;
|
||||
onChange: (newConfig: AppConfig) => void;
|
||||
onChange: (config: AppConfig) => void;
|
||||
}
|
||||
|
||||
const InputGroup: React.FC<{ label: string; children: React.ReactNode }> = ({ label, children }) => (
|
||||
<div className="flex flex-col gap-2 mb-4">
|
||||
<label className="text-sm font-medium text-gray-300">{label}</label>
|
||||
<div className="flex gap-4">{children}</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const NumberInput = ({
|
||||
label,
|
||||
value,
|
||||
onChange,
|
||||
max
|
||||
}: {
|
||||
label: string;
|
||||
value: number;
|
||||
onChange: (val: number) => void;
|
||||
max?: number
|
||||
}) => (
|
||||
<div className="flex-1 relative">
|
||||
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-500 text-xs">{label}</span>
|
||||
<input
|
||||
type="number"
|
||||
min="1"
|
||||
max={max}
|
||||
value={value}
|
||||
onChange={(e) => onChange(parseFloat(e.target.value) || 0)}
|
||||
className="w-full bg-slate-800 border border-slate-700 rounded p-2 pl-8 text-white focus:ring-2 focus:ring-primary outline-none"
|
||||
/>
|
||||
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-gray-500 text-xs">мм</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
export const ConfigStep: React.FC<Props> = ({ config, onChange }) => {
|
||||
const updateDrawer = (key: keyof AppConfig['drawer'], val: number) => {
|
||||
onChange({ ...config, drawer: { ...config.drawer, [key]: val } });
|
||||
|
||||
// Инициализация дефолтных значений, если их нет
|
||||
useEffect(() => {
|
||||
if (!config.perforation) {
|
||||
onChange({
|
||||
...config,
|
||||
perforation: { enabled: false, pattern: 'hexagon', diameter: 8, spacing: 4 }
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
const perf = config.perforation || { enabled: false, pattern: 'hexagon', diameter: 8, spacing: 4 };
|
||||
|
||||
const updatePerf = (updates: Partial<typeof perf>) => {
|
||||
onChange({ ...config, perforation: { ...perf, ...updates } });
|
||||
};
|
||||
|
||||
const updateDrawer = (key: keyof typeof config.drawer, value: number) => {
|
||||
onChange({ ...config, drawer: { ...config.drawer, [key]: value } });
|
||||
};
|
||||
|
||||
// --- RENDER PREVIEW (SVG) ---
|
||||
const renderPreview = () => {
|
||||
if (!perf.enabled) return <div className="text-gray-500 text-xs flex items-center justify-center h-full">Перфорация выключена</div>;
|
||||
|
||||
const size = perf.diameter;
|
||||
const gap = Math.max(2, perf.spacing); // Минимальный зазор 2мм
|
||||
const step = size + gap;
|
||||
const W = 140;
|
||||
const H = 100;
|
||||
|
||||
const elements = [];
|
||||
const rows = Math.floor(H / (step * 0.866)); // 0.866 для сот (sin 60)
|
||||
const cols = Math.floor(W / step);
|
||||
|
||||
for(let j=0; j<rows; j++) {
|
||||
const isOdd = j % 2 !== 0;
|
||||
const y = 10 + j * (perf.pattern === 'circle' ? step : step * 0.866);
|
||||
|
||||
for(let i=0; i<cols; i++) {
|
||||
let x = 10 + i * step;
|
||||
if ((perf.pattern === 'hexagon' || perf.pattern === 'triangle') && isOdd) x += step / 2;
|
||||
|
||||
if (x > W - 10 || y > H - 10) continue;
|
||||
|
||||
if (perf.pattern === 'circle') {
|
||||
elements.push(<circle key={`${i}-${j}`} cx={x} cy={y} r={size/2} fill="#3b82f6" />);
|
||||
} else if (perf.pattern === 'hexagon') {
|
||||
// Рисуем шестиугольник
|
||||
const r = size / 2;
|
||||
const points = [];
|
||||
for (let k = 0; k < 6; k++) {
|
||||
const angle = (k * 60 + 30) * Math.PI / 180;
|
||||
points.push(`${x + r * Math.cos(angle)},${y + r * Math.sin(angle)}`);
|
||||
}
|
||||
elements.push(<polygon key={`${i}-${j}`} points={points.join(' ')} fill="#3b82f6" />);
|
||||
} else if (perf.pattern === 'triangle') {
|
||||
const r = size / 2;
|
||||
const angleOffset = isOdd ? 180 : 0;
|
||||
const points = [];
|
||||
for (let k = 0; k < 3; k++) {
|
||||
const angle = (k * 120 - 90 + angleOffset) * Math.PI / 180;
|
||||
points.push(`${x + r * Math.cos(angle)},${y + r * Math.sin(angle)}`);
|
||||
}
|
||||
elements.push(<polygon key={`${i}-${j}`} points={points.join(' ')} fill="#3b82f6" />);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-full bg-slate-900 border border-slate-700 rounded">
|
||||
{elements}
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-slate-900 p-6 rounded-xl shadow-lg border border-slate-800 animate-fade-in">
|
||||
<h2 className="text-xl font-bold mb-6 flex items-center gap-2 text-primary">
|
||||
<Box size={24} /> 1. Размеры
|
||||
</h2>
|
||||
<div className="space-y-6 pb-20">
|
||||
{/* 1. ГАБАРИТЫ */}
|
||||
<section className="bg-slate-900 p-5 rounded-xl border border-slate-800 shadow-lg">
|
||||
<h2 className="text-lg font-bold mb-4 flex items-center gap-2 text-primary"><Settings2 size={20}/> 1. Размеры ящика</h2>
|
||||
<div className="grid grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 font-bold ml-1">Ширина (X)</label>
|
||||
<input type="number" value={config.drawer.width} onChange={(e) => updateDrawer('width', parseFloat(e.target.value))} className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary outline-none mt-1"/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 font-bold ml-1">Глубина (Y)</label>
|
||||
<input type="number" value={config.drawer.depth} onChange={(e) => updateDrawer('depth', parseFloat(e.target.value))} className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary outline-none mt-1"/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 font-bold ml-1">Высота (Z)</label>
|
||||
<input type="number" value={config.drawer.height} onChange={(e) => updateDrawer('height', parseFloat(e.target.value))} className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white focus:ring-2 focus:ring-primary outline-none mt-1"/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-8">
|
||||
{/* Drawer Dimensions */}
|
||||
<div className="bg-slate-800/50 p-4 rounded-lg border border-slate-700">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2 text-gray-200">
|
||||
<Ruler className="text-accent" size={18} /> Внутренние размеры ящика
|
||||
</h3>
|
||||
<InputGroup label="Ширина (X)">
|
||||
<NumberInput label="Ш" value={config.drawer.width} onChange={(v) => updateDrawer('width', v)} />
|
||||
</InputGroup>
|
||||
<InputGroup label="Глубина (Y)">
|
||||
<NumberInput label="Г" value={config.drawer.depth} onChange={(v) => updateDrawer('depth', v)} />
|
||||
</InputGroup>
|
||||
<InputGroup label="Высота (Z)">
|
||||
<NumberInput label="В" value={config.drawer.height} onChange={(v) => updateDrawer('height', v)} />
|
||||
</InputGroup>
|
||||
{/* 2. ПЕРФОРАЦИЯ */}
|
||||
<section className="bg-slate-900 p-5 rounded-xl border border-slate-800 shadow-lg">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<h2 className="text-lg font-bold flex items-center gap-2 text-primary"><LayoutGrid size={20}/> 2. Перфорация (узоры)</h2>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-400">{perf.enabled ? 'Включено' : 'Выключено'}</span>
|
||||
<button
|
||||
onClick={() => updatePerf({ enabled: !perf.enabled })}
|
||||
className={`w-12 h-6 rounded-full p-1 transition-colors ${perf.enabled ? 'bg-primary' : 'bg-slate-700'}`}
|
||||
>
|
||||
<div className={`w-4 h-4 bg-white rounded-full shadow-md transform transition-transform ${perf.enabled ? 'translate-x-6' : 'translate-x-0'}`}/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Settings */}
|
||||
<div className="bg-slate-800/50 p-4 rounded-lg border border-slate-700 flex flex-col justify-center">
|
||||
<h3 className="text-lg font-semibold mb-4 flex items-center gap-2 text-gray-200">
|
||||
<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>
|
||||
<span className="text-primary font-bold bg-primary/10 px-2 py-1 rounded text-sm border border-primary/20">
|
||||
{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
|
||||
type="range"
|
||||
min="0.4"
|
||||
max="3.2"
|
||||
step="0.1"
|
||||
value={config.wallThickness}
|
||||
onChange={(e) => onChange({...config, wallThickness: parseFloat(e.target.value)})}
|
||||
className="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-primary hover:accent-blue-400 transition-all"
|
||||
/>
|
||||
<span className="text-xs text-gray-500 font-mono">3.2</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className={`grid grid-cols-1 md:grid-cols-2 gap-6 transition-opacity ${perf.enabled ? 'opacity-100' : 'opacity-50 pointer-events-none'}`}>
|
||||
{/* Controls */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 font-bold ml-1 mb-2 block">Тип узора</label>
|
||||
<div className="flex gap-2">
|
||||
<button onClick={() => updatePerf({ pattern: 'circle' })} className={`flex-1 py-2 rounded border flex items-center justify-center gap-2 ${perf.pattern === 'circle' ? 'bg-primary/20 border-primary text-primary' : 'bg-slate-800 border-slate-700 text-gray-400'}`}><Circle size={16}/> Круг</button>
|
||||
<button onClick={() => updatePerf({ pattern: 'hexagon' })} className={`flex-1 py-2 rounded border flex items-center justify-center gap-2 ${perf.pattern === 'hexagon' ? 'bg-primary/20 border-primary text-primary' : 'bg-slate-800 border-slate-700 text-gray-400'}`}><Hexagon size={16}/> Соты</button>
|
||||
<button onClick={() => updatePerf({ pattern: 'triangle' })} className={`flex-1 py-2 rounded border flex items-center justify-center gap-2 ${perf.pattern === 'triangle' ? 'bg-primary/20 border-primary text-primary' : 'bg-slate-800 border-slate-700 text-gray-400'}`}><Triangle size={16}/> Треуг.</button>
|
||||
</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">
|
||||
<Minimize2 size={14} className="text-gray-400"/> Зазор (Tolerance)
|
||||
</label>
|
||||
<span className="text-accent font-bold bg-accent/10 px-2 py-1 rounded text-sm border border-accent/20">
|
||||
{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
|
||||
type="range"
|
||||
min="0.0"
|
||||
max="2.0"
|
||||
step="0.1"
|
||||
value={config.printerTolerance}
|
||||
onChange={(e) => onChange({...config, printerTolerance: parseFloat(e.target.value)})}
|
||||
className="w-full h-2 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-accent hover:accent-amber-400 transition-all"
|
||||
/>
|
||||
<span className="text-xs text-gray-500 font-mono">2.0</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 font-bold ml-1">Диаметр (мм)</label>
|
||||
<input type="number" min="2" max="12" step="0.5" value={perf.diameter} onChange={(e) => updatePerf({ diameter: Math.min(12, parseFloat(e.target.value)) })} className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white mt-1"/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-400 font-bold ml-1">Зазор (мм)</label>
|
||||
<input type="number" min="2" value={perf.spacing} onChange={(e) => updatePerf({ spacing: Math.max(2, parseFloat(e.target.value)) })} className="w-full bg-slate-800 border border-slate-700 rounded-lg px-3 py-2 text-white mt-1"/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="h-32 md:h-auto flex flex-col">
|
||||
<span className="text-xs text-gray-400 mb-2 font-bold ml-1">Предпросмотр</span>
|
||||
{renderPreview()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* 3. ПРОЧЕЕ */}
|
||||
<section className="bg-slate-900 p-5 rounded-xl border border-slate-800 shadow-lg opacity-80 hover:opacity-100 transition-opacity">
|
||||
<h2 className="text-sm font-bold mb-4 text-gray-400">Дополнительно</h2>
|
||||
<div className="flex gap-4">
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 block">Толщина стенок</label>
|
||||
<input type="number" step="0.1" value={config.wallThickness} onChange={(e) => onChange({...config, wallThickness: parseFloat(e.target.value)})} className="bg-slate-800 border border-slate-700 rounded px-2 py-1 text-sm w-20 text-gray-300"/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 block">Скругление углов</label>
|
||||
<input type="number" value={config.cornerRadius} onChange={(e) => onChange({...config, cornerRadius: parseFloat(e.target.value)})} className="bg-slate-800 border border-slate-700 rounded px-2 py-1 text-sm w-20 text-gray-300"/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user