Add perforation

This commit is contained in:
Халимов Рустам
2026-01-12 17:32:41 +03:00
parent 356873fb0e
commit 531b5dc0b1
5 changed files with 995 additions and 564 deletions

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { AppConfig } from '../types';
import { Ruler, Box, Layers, Minimize2, CircleDashed } from 'lucide-react';
import { AppConfig, PerforationShape } from '../types';
import { Ruler, Box, Layers, Minimize2, CircleDashed, Grid, Circle, Hexagon, Triangle } from 'lucide-react';
interface Props {
config: AppConfig;
@@ -14,16 +14,16 @@ const InputGroup: React.FC<{ label: string; children: React.ReactNode }> = ({ la
</div>
);
const NumberInput = ({
label,
value,
onChange,
max
}: {
label: string;
value: number;
onChange: (val: number) => void;
max?: number
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>
@@ -39,113 +39,280 @@ const NumberInput = ({
</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">
<h2 className="text-xl font-bold mb-6 flex items-center gap-2 text-primary">
<Box size={24} /> 1. Размеры
</h2>
<div className="bg-slate-900 p-6 rounded-xl shadow-lg border border-slate-800 animate-fade-in space-y-8">
<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>
{/* 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>
{/* 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">
<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"
</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)})}
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>
/>
<span className="text-xs text-gray-500 font-mono">3.2</span>
</div>
</div>
{/* Corner Radius (NEW) */}
<div className="mb-6">
<div className="flex justify-between items-center mb-2">
{/* 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"/> Радиус скругления
<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"
</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)})}
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>
/>
<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">
{/* 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)
<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"
</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)})}
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>
/>
<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>