Try add perforation

This commit is contained in:
Халимов Рустам
2026-01-12 00:03:24 +03:00
parent 96bf976b64
commit e7d153ce6b
3 changed files with 412 additions and 228 deletions

View File

@@ -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>
);
};