Files
BoxGenerator/src/components/ConfigStep.tsx
Халимов Рустам 531b5dc0b1 Add perforation
2026-01-12 17:32:41 +03:00

320 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import React from 'react';
import { AppConfig, PerforationShape } from '../types';
import { Ruler, Box, Layers, Minimize2, CircleDashed, Grid, Circle, Hexagon, Triangle } from 'lucide-react';
interface Props {
config: AppConfig;
onChange: (newConfig: 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>
);
const PerforationPreview = ({ config }: { config: AppConfig['perforation'] }) => {
if (!config.enabled) return (
<div className="w-full h-48 bg-slate-950 rounded-lg border border-slate-800 flex items-center justify-center text-gray-600">
<span className="text-sm">Перфорация отключена</span>
</div>
);
// Simple canvas-like SVG generation
const width = 200;
const height = 120;
const size = config.size * 2; // Scale up mostly for visibility
const gap = config.gap * 2;
const step = size + gap;
const elements = [];
if (config.shape === 'circle') {
for (let y = 0; y < height; y += step) {
for (let x = 0; x < width; x += step) {
elements.push(<circle key={`${x}-${y}`} cx={x + step / 2} cy={y + step / 2} r={size / 2} fill="#3b82f6" />);
}
}
} else if (config.shape === 'honeycomb') {
const hStep = size * 0.866; // height of equilateral triangle
for (let y = 0; y < height; y += (size + gap) * 0.85) {
const row = Math.floor(y / ((size + gap) * 0.85));
const xOffset = row % 2 === 0 ? 0 : (size + gap) / 2;
for (let x = xOffset - step; x < width; x += step) {
// Hexagon points
const r = size / 2;
const cx = x + step / 2;
const cy = y + step / 2;
// Points for flat-topped hexagon
const points = [];
for (let i = 0; i < 6; i++) {
const angle_deg = 60 * i + 30;
const angle_rad = Math.PI / 180 * angle_deg;
points.push(`${cx + r * Math.cos(angle_rad)},${cy + r * Math.sin(angle_rad)}`);
}
elements.push(<polygon key={`${x}-${y}`} points={points.join(" ")} fill="#3b82f6" />);
}
}
} else if (config.shape === 'triangle') {
for (let y = 0; y < height; y += step * 0.866) { // Staggered rows
const row = Math.floor(y / (step * 0.866));
const xOffset = row % 2 === 0 ? 0 : step / 2;
for (let x = -step + xOffset; x < width; x += step) {
const cx = x + step / 2;
const cy = y + step / 2;
const r = size / 2;
// Upright triangle
const points = [
`${cx},${cy - r}`,
`${cx + r * 0.866},${cy + r * 0.5}`,
`${cx - r * 0.866},${cy + r * 0.5}`
];
elements.push(<polygon key={`${x}-${y}`} points={points.join(" ")} fill="#3b82f6" />);
}
}
}
return (
<div className="w-full h-48 bg-slate-950 rounded-lg border border-slate-800 overflow-hidden relative">
<svg width="100%" height="100%" viewBox={`0 0 ${width} ${height}`} preserveAspectRatio="xMidYMid slice">
{elements}
</svg>
<div className="absolute top-2 right-2 text-xs text-gray-500">:: Масштаб условен</div>
</div>
);
}
export const ConfigStep: React.FC<Props> = ({ config, onChange }) => {
const updateDrawer = (key: keyof AppConfig['drawer'], val: number) => {
onChange({ ...config, drawer: { ...config.drawer, [key]: val } });
};
const updatePerforation = (key: keyof AppConfig['perforation'], val: any) => {
onChange({ ...config, perforation: { ...config.perforation, [key]: val } })
}
return (
<div className="bg-slate-900 p-6 rounded-xl shadow-lg border border-slate-800 animate-fade-in space-y-8">
{/* SECTION 1: Dimensions & Settings */}
<div>
<h2 className="text-xl font-bold mb-6 flex items-center gap-2 text-primary">
<Box size={24} /> 1. Размеры
</h2>
<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>
</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>
{/* Corner Radius */}
<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>
</div>
</div>
{/* SECTION 2: Perforation */}
<div className="bg-slate-800/30 p-6 rounded-lg border border-slate-700/50">
<div className="flex items-center justify-between mb-6">
<h3 className="text-lg font-semibold flex items-center gap-2 text-blue-400">
<Grid size={20} /> 2. Перфорация (узоры)
</h3>
<div className="flex items-center gap-3">
<span className="text-sm font-medium text-gray-400 uppercase tracking-wider">Включено</span>
<button
onClick={() => updatePerforation('enabled', !config.perforation.enabled)}
className={`w-12 h-6 rounded-full relative transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 ${config.perforation.enabled ? 'bg-blue-600' : 'bg-slate-600'}`}
>
<span className={`block w-4 h-4 rounded-full bg-white shadow transform transition-transform duration-200 ease-in-out absolute top-1 ${config.perforation.enabled ? 'translate-x-7' : 'translate-x-1'}`} />
</button>
</div>
</div>
<div className={`grid grid-cols-1 md:grid-cols-2 gap-8 transition-all duration-300 ${config.perforation.enabled ? 'opacity-100 pointer-events-auto' : 'opacity-40 pointer-events-none filter blur-[1px]'}`}>
<div>
<label className="text-xs font-bold text-gray-500 uppercase mb-3 block">Тип узора</label>
<div className="flex gap-4 mb-8">
{[
{ id: 'circle', icon: Circle, label: 'Круг' },
{ id: 'honeycomb', icon: Hexagon, label: 'Соты' },
{ id: 'triangle', icon: Triangle, label: 'Треуг.' }
].map((item) => (
<button
key={item.id}
onClick={() => updatePerforation('shape', item.id as PerforationShape)}
className={`flex-1 flex flex-col items-center justify-center gap-2 py-4 px-2 rounded-lg border transition-all ${config.perforation.shape === item.id
? 'bg-slate-700/80 border-blue-500 text-blue-400 shadow-lg shadow-blue-500/10'
: 'bg-slate-800 border-slate-700 text-gray-400 hover:bg-slate-750 hover:border-slate-600'
}`}
>
<item.icon size={24} />
<span className="text-sm font-medium">{item.label}</span>
</button>
))}
</div>
{/* Resize Controls */}
<div className="space-y-6">
<div>
<div className="flex justify-between items-center mb-2">
<label className="text-sm font-medium text-gray-300">Диаметр отверстий</label>
<span className="text-sm font-bold text-blue-400">{config.perforation.size} мм</span>
</div>
<div className="flex items-center gap-4">
<span className="text-xs text-gray-500">2 мм</span>
<input
type="range" min="2" max="25" step="1"
value={config.perforation.size}
onChange={(e) => updatePerforation('size', parseFloat(e.target.value))}
className="flex-1 h-1.5 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
/>
<span className="text-xs text-gray-500">25 мм</span>
</div>
</div>
<div>
<div className="flex justify-between items-center mb-2">
<label className="text-sm font-medium text-gray-300">Зазор (между отверстиями)</label>
<span className="text-sm font-bold text-blue-400">{config.perforation.gap} мм</span>
</div>
<div className="flex items-center gap-4">
<span className="text-xs text-gray-500">1 мм</span>
<input
type="range" min="1" max="10" step="0.5"
value={config.perforation.gap}
onChange={(e) => updatePerforation('gap', parseFloat(e.target.value))}
className="flex-1 h-1.5 bg-slate-700 rounded-lg appearance-none cursor-pointer accent-blue-500"
/>
<span className="text-xs text-gray-500">10 мм</span>
</div>
</div>
</div>
</div>
{/* Preview */}
<div>
<label className="text-xs font-bold text-gray-500 uppercase mb-3 block">Предпросмотр</label>
<PerforationPreview config={config.perforation} />
</div>
</div>
</div>
</div>
);
};