@@ -1,6 +1,6 @@
import React , { useRef , useState , useMemo } from 'react' ;
import { AppConfig , LayoutSplits , Partition } from '../types' ;
import { Grid , MousePointer2 , Trash2 , RotateCcw , X , Plus } from 'lucide-react' ;
import { Grid , MousePointer2 , RotateCcw , X , Move } from 'lucide-react' ;
interface Props {
config : AppConfig ;
@@ -19,17 +19,18 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
const svgRef = useRef < SVGSVGElement > ( null ) ;
const [ mode , setMode ] = useState < EditMode > ( 'lines' ) ;
const [ mousePos , setMousePos ] = useState ( { x : 0 , y : 0 } ) ;
const [ isButtonHovered , setIsButtonHovered ] = useState ( false ) ;
const [ mousePos , setMousePos ] = useState ( { x : 0 , y : 0 } ) ;
const [ dragging , setDragging ] = useState < DragTarget | null > ( null ) ;
const [ isButtonHovered , setIsButtonHovered ] = useState ( false ) ;
// Main Grid
const [ phantomMainAxis , setPhantomMainAxis ] = useState < Axis | null > ( null ) ;
const [ hoveredMainSplit , setHoveredMainSplit ] = useState < { axis : Axis ; index : number } | null > ( null ) ;
// Partitions
const [ hoveredCell , setHoveredCell ] = useState < { i : number ; j : number } | null > ( null ) ;
const [ hoveredPartition , setHoveredPartition ] = useState < { id : string ; cellKey : string } | null > ( null ) ;
const [ phantomPartition , setPhantomPartition ] = useState < { axis : Axis ; offset : number } | null > ( null ) ;
const [ phantomPartition , setPhantomPartition ] = useState < { axis : Axis ; offset : number ; min : number ; max : number } | null > ( null ) ;
const [ selectedPartitionId , setSelectedPartitionId ] = useState < string | null > ( null ) ;
// --- Safe Data ---
@@ -49,15 +50,42 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
// --- Helpers ---
const getSelectedPartition = ( ) = > {
if ( ! selectedPartitionId ) return null ;
for ( const key in safePartitions ) {
const part = safePartitions [ key ] . find ( p = > p . id === selectedPartitionId ) ;
if ( part ) return { key , part } ;
}
return null ;
if ( ! selectedPartitionId ) return null ;
for ( const key in safePartitions ) {
const part = safePartitions [ key ] . find ( p = > p . id === selectedPartitionId ) ;
if ( part ) return { key , part } ;
}
return null ;
} ;
const selectedData = getSelectedPartition ( ) ;
// --- Logic: Find Sub-Area Bounds ---
// Определяет границы прямоугольника под мышкой внутри ячейки, учитывая существующие стенки
const getHoveredBoundaries = ( lx : number , ly : number , parts : Partition [ ] ) = > {
let minX = 0 , maxX = 1 ;
let minY = 0 , maxY = 1 ;
// Сужаем границы на основе существующих стенок
parts . forEach ( p = > {
if ( p . axis === 'x' ) {
// Вертикальная стенка. Проверяем, пересекаемся ли мы с ней по Y
// (то есть мышь находится в диапазоне min/max этой стенки)
if ( ly >= p . min && ly <= p . max ) {
if ( p . offset < lx ) minX = Math . max ( minX , p . offset ) ;
if ( p . offset > lx ) maxX = Math . min ( maxX , p . offset ) ;
}
} else {
// Горизонтальная стенка
if ( lx >= p . min && lx <= p . max ) {
if ( p . offset < ly ) minY = Math . max ( minY , p . offset ) ;
if ( p . offset > ly ) maxY = Math . min ( maxY , p . offset ) ;
}
}
} ) ;
return { minX , maxX , minY , maxY } ;
} ;
// --- Actions ---
const removeMainSplit = ( axis : Axis , index : number ) = > {
const newSplits = { . . . splits , x : [ . . . safeX ] , y : [ . . . safeY ] } ;
@@ -68,28 +96,33 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
setIsButtonHovered ( false ) ;
} ;
const createPartition = ( i : number , j : number , axis : Axis , offset : number ) = > {
const key = ` ${ i } - ${ j } ` ;
const current = safePartitions [ key ] || [ ] ;
const newPart : Partition = {
id : Math.random ( ) . toString ( 36 ) . substr ( 2 , 9 ) ,
axis , offset , height : config.drawer.height || 80 , rounded : false
} ;
onChange ( { . . . splits , partitions : { . . . safePartitions , [ key ] : [ . . . current , newPart ] } } ) ;
setSelectedPartitionId ( newPart . id ) ;
const createPartition = ( i : number , j : number , axis : Axis , offset : number , min : number , max : number ) = > {
const key = ` ${ i } - ${ j } ` ;
const current = safePartitions [ key ] || [ ] ;
const newPart : Partition = {
id : Math.random ( ) . toString ( 36 ) . substr ( 2 , 9 ) ,
axis ,
offset ,
min ,
max ,
height : config.drawer.height || 80 ,
rounded : false
} ;
onChange ( { . . . splits , partitions : { . . . safePartitions , [ key ] : [ . . . current , newPart ] } } ) ;
setSelectedPartitionId ( newPart . id ) ;
} ;
const updatePartition = ( key : string , id : string , updates : Partial < Partition > ) = > {
const current = safePartitions [ key ] || [ ] ;
const updated = current . map ( p = > p . id === id ? { . . . p , . . . updates } : p ) ;
onChange ( { . . . splits , partitions : { . . . safePartitions , [ key ] : updated } } ) ;
const current = safePartitions [ key ] || [ ] ;
const updated = current . map ( p = > p . id === id ? { . . . p , . . . updates } : p ) ;
onChange ( { . . . splits , partitions : { . . . safePartitions , [ key ] : updated } } ) ;
} ;
const removePartition = ( key : string , id : string ) = > {
const current = safePartitions [ key ] || [ ] ;
onChange ( { . . . splits , partitions : { . . . safePartitions , [ key ] : current . filter ( p = > p . id !== id ) } } ) ;
if ( selectedPartitionId === id ) setSelectedPartitionId ( null ) ;
setHoveredPartition ( null ) ;
const current = safePartitions [ key ] || [ ] ;
onChange ( { . . . splits , partitions : { . . . safePartitions , [ key ] : current . filter ( p = > p . id !== id ) } } ) ;
if ( selectedPartitionId === id ) setSelectedPartitionId ( null ) ;
setHoveredPartition ( null ) ;
} ;
// --- Handlers ---
@@ -103,94 +136,120 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
setMousePos ( { x : nx , y : ny } ) ;
if ( dragging ) {
if ( dragging . type === 'main' ) {
const newSplits = { . . . splits , x : [ . . . safeX ] , y : [ . . . safeY ] } ;
const val = dragging . axis === 'x' ? nx : ny ;
newSplits [ dragging . axis ] [ dragging . index ] = val ;
onChange ( newSplits ) ;
if ( dragging . type === 'main' ) {
const newSplits = { . . . splits , x : [ . . . safeX ] , y : [ . . . safeY ] } ;
const val = dragging . axis === 'x' ? nx : ny ;
newSplits [ dragging . axis ] [ dragging . index ] = val ;
onChange ( newSplits ) ;
} else {
// Drag Partition
const [ iStr , jStr ] = dragging . cellKey . split ( '-' ) ;
const i = parseInt ( iStr ) ; const j = parseInt ( jStr ) ;
const cellX1 = sortedX [ i ] ; const cellX2 = sortedX [ i + 1 ] ;
const cellY1 = sortedY [ j ] ; const cellY2 = sortedY [ j + 1 ] ;
let newOffset = 0 ;
if ( dragging . axis === 'x' ) {
newOffset = ( nx - cellX1 ) / ( cellX2 - cellX1 ) ;
} else {
const [ iStr , jStr ] = dragging . cellKey . split ( '-' ) ;
const i = parseInt ( iStr ) ; const j = parseInt ( jStr ) ;
const cellX1 = sortedX [ i ] ; const cellX2 = sortedX [ i + 1 ] ;
const cellY1 = sortedY [ j ] ; const cellY2 = sortedY [ j + 1 ] ;
let newOffset = 0 ;
if ( dragging . axis === 'x' ) {
newOffset = ( nx - cellX1 ) / ( cellX2 - cellX1 ) ;
} else {
newOffset = ( ny - cellY1 ) / ( cellY2 - cellY1 ) ;
}
newOffset = Math . max ( 0.05 , Math . min ( 0.95 , newOffset ) ) ;
if ( ! isNaN ( newOffset ) ) {
updatePartition ( dragging . cellKey , dragging . id , { offset : newOffset } ) ;
}
newOffset = ( ny - cellY1 ) / ( cellY2 - cellY1 ) ;
}
return ;
// Ограничиваем перетаскивание пределами "родительской" зоны (чтобы не наехать на соседей)
// Для простоты пока ограничиваем 0.05-0.95 всей ячейки, но в идеале нужно проверять bounds
newOffset = Math . max ( 0.02 , Math . min ( 0.98 , newOffset ) ) ;
if ( ! isNaN ( newOffset ) ) {
updatePartition ( dragging . cellKey , dragging . id , { offset : newOffset } ) ;
}
}
return ;
}
if ( isButtonHovered ) return ;
if ( mode === 'lines' ) {
setHoveredMainSplit ( null ) ;
const SNAP = 0.015 ;
if ( nx > SNAP && nx < 1 - SNAP && ny > SNAP && ny < 1 - SNAP ) {
const closeToX = safeX . some ( val = > Math . abs ( nx - val ) < SNAP ) ;
const closeToY = safeY . some ( val = > Math . abs ( ny - val ) < SNAP ) ;
if ( ! closeToX && ! closeToY ) {
const distRight = 1 - nx ; const distBottom = 1 - ny ;
setPhantomMainAxis ( Math . min ( nx , distRight ) < Math . min ( ny , distBottom ) ? 'y' : 'x' ) ;
} else { setPhantomMainAxis ( null ) ; }
}
} else if ( mode === 'cells' ) {
setHoveredPartition ( null ) ;
setPhantom Partition ( null ) ;
setHoveredCell ( null ) ;
setHoveredMainSplit ( null ) ;
const SNAP = 0.015 ;
if ( nx > SNAP && nx < 1 - SNAP && ny > SNAP && ny < 1 - SNAP ) {
const closeToX = safeX . some ( val = > Math . abs ( nx - val ) < SNAP ) ;
const closeToY = safeY . some ( val = > Math . abs ( ny - val ) < SNAP ) ;
if ( ! closeToX && ! closeToY ) {
const distRight = 1 - nx ; const distBottom = 1 - ny ;
setPhantomMainAxis ( Math . min ( nx , distRight ) < Math . min ( ny , distBottom ) ? 'y' : 'x' ) ;
} else { setPhantomMainAxis ( null ) ; }
}
} else {
// Cells Mode
setHovered Partition ( null ) ;
setPhantomPartition ( null ) ;
setHoveredCell ( null ) ;
let cellIdx = null ;
for ( let i = 0 ; i < sortedX.length - 1 ; i + + ) {
if (nx > = sortedX [ i ] && nx <= sortedX [ i + 1 ] ) {
for ( let j = 0 ; j < sortedY.length - 1 ; j + + ) {
if (ny > = sortedY [ j ] && ny <= sortedY [ j + 1 ] ) {
cellIdx = { i , j } ;
break ;
}
}
// 1. Находим ячейку
let cellIdx = null ;
for (let i = 0 ; i < sortedX . length - 1 ; i ++ ) {
if ( nx >= sortedX [ i ] && nx <= sortedX [ i + 1 ] ) {
for (let j = 0 ; j < sortedY . length - 1 ; j ++ ) {
if ( ny >= sortedY [ j ] && ny <= sortedY [ j + 1 ] ) {
cellIdx = { i , j } ;
break ;
}
}
}
}
if ( cellIdx ) {
setHoveredCell ( cellIdx ) ;
const key = ` ${ cellIdx . i } - ${ cellIdx . j } ` ;
const parts = safePartitions [ key ] || [ ] ;
const cx1 = sortedX [ cellIdx . i ] ; const cx2 = sortedX [ cellIdx . i + 1 ] ;
const cy1 = sortedY [ cellIdx . j ] ; const cy2 = sortedY [ cellIdx . j + 1 ] ;
const cw = cx2 - cx1 ; const ch = cy2 - cy1 ;
const lx = ( nx - cx1 ) / cw ;
const ly = ( ny - cy1 ) / ch ;
// 2. Проверяем наведение на существующие (для удаления/выделения)
let found = null ;
const SNAP = 0.05 ;
for ( const p of parts ) {
// Учитываем min/max при наведении
if ( p . axis === 'x' ) {
if ( ly >= p . min && ly <= p . max && Math . abs ( lx - p . offset ) < SNAP * ( aspectRatio > 1 ? 1 : aspectRatio ) ) found = p ;
} else {
if ( lx >= p . min && lx <= p . max && Math . abs ( ly - p . offset ) < SNAP * ( aspectRatio < 1 ? 1 : 1 / aspectRatio ) ) found = p ;
}
}
if ( cellIdx ) {
setHoveredCell ( cellIdx ) ;
const key = ` ${ cellIdx . i } - ${ cellIdx . j } ` ;
const parts = safePartitions [ key ] || [ ] ;
const cx1 = sortedX [ cellIdx . i ] ; const cx2 = sortedX [ cellIdx . i + 1 ] ;
const cy1 = sortedY [ cellIdx . j ] ; const cy2 = sortedY [ cellIdx . j + 1 ] ;
const cw = cx2 - cx1 ; const ch = cy2 - cy1 ;
const lx = ( nx - cx1 ) / cw ;
const ly = ( ny - cy1 ) / ch ;
let found = null ;
const SNAP = 0.05 ;
for ( const p of parts ) {
if ( p . axis === 'x' ) {
if ( Math . abs ( lx - p . offset ) < SNAP * ( aspectRatio > 1 ? 1 : aspectRatio ) ) found = p ;
} else {
if ( Math . abs ( ly - p . offset ) < SNAP * ( aspectRatio < 1 ? 1 : 1 / aspectRatio ) ) f ound = p ;
}
}
if ( found ) {
setHoveredPartition ( { id : found.id , cellKey : key } ) ;
} else {
const distLeft = lx ; const distRight = 1 - lx ;
const distTop = ly ; const distBottom = 1 - ly ;
const minX = Math . min ( distLeft , distRight ) ;
const minY = Math . min ( distTop , distBottom ) ;
const axis = minX < minY ? 'y' : 'x' ;
if ( lx > 0.05 && lx < 0.95 && ly > 0.05 && ly < 0.95 ) {
setPhantomPartition ( { axis , offset : axis === 'x' ? lx : ly } ) ;
}
}
if ( found ) {
setHoveredPartition ( { id : found.id , cellKey : key } ) ;
} else {
// 3. Вычисляем границы для новой фантомной стенки
const bounds = getHoveredBoundaries ( lx , ly , parts ) ;
const distL = lx - bounds . minX ;
const distR = bounds . maxX - lx ;
const distT = ly - bounds . minY ;
const distB = bounds . maxY - ly ;
const minX = Math . min ( distL , distR ) ;
const minY = Math . min ( distT , distB ) ;
const axis = minX < minY ? 'y' : 'x' ; // Перпендикулярно ближайшему краю
// Проверяем, чтобы было место
const width = bounds . maxX - b ounds . minX ;
const height = bounds . maxY - bounds . minY ;
if ( ( axis === 'y' && height > 0.1 ) || ( axis === 'x' && width > 0.1 ) ) {
const offset = axis === 'x' ? lx : ly ;
const min = axis === 'x' ? bounds.minY : bounds.minX ;
const max = axis === 'x' ? bounds.maxY : bounds.maxX ;
setPhantomPartition ( { axis , offset , min , max } ) ;
}
}
}
}
} ;
@@ -198,55 +257,58 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
if ( isButtonHovered ) return ;
if ( mode === 'lines' ) {
if ( hoveredMainSplit ) {
if ( e . button === 0 ) setDragging ( { type : 'main' , . . . hoveredMainSplit } ) ;
else if ( e . button === 2 ) removeMainSplit ( hoveredMainSplit . axis , hoveredMainSplit . index ) ;
} else if ( phantomMainAxis ) {
const val = phantomMainAxis === 'x' ? mousePos.x : mousePos.y ;
const newSplits = { . . . splits , x : [ . . . safeX ] , y : [ . . . safeY ] } ;
newSplits [ phantomMainAxis ] = [ . . . newSplits [ phantomMainAxis ] , val ] ;
onChange ( newSplits ) ;
setDragging ( { type : 'main' , axis : phantomMainAxis , index : newSplits [ phantomMainAxis ] . length - 1 } ) ;
if ( hoveredMainSplit ) {
if ( e . button === 0 ) setDragging ( { type : 'main' , . . . hoveredMainSplit } ) ;
else if ( e . button === 2 ) removeMainSplit ( hoveredMainSplit . axis , hoveredMainSplit . index ) ;
} else if ( phantomMainAxis ) {
const val = phantomMainAxis === 'x' ? mousePos.x : mousePos.y ;
const newSplits = { . . . splits , x : [ . . . safeX ] , y : [ . . . safeY ] } ;
newSplits [ phantomMainAxis ] = [ . . . newSplits [ phantomMainAxis ] , val ] ;
onChange ( newSplits ) ;
setDragging ( { type : 'main' , axis : phantomMainAxis , index : newSplits [ phantomMainAxis ] . length - 1 } ) ;
}
} else {
if ( hoveredPartition ) {
const key = hoveredPartition . cellKey ;
const parts = safePartitions [ key ] || [ ] ;
const part = parts . find ( p = > p . id === hoveredPartition . id ) ;
if ( part ) {
if ( e . button === 0 ) {
setDragging ( { type : 'partition' , cellKey : key , id : part.id , axis : part.axis } ) ;
setSelectedPartitionId ( part . id ) ;
} else if ( e . button === 2 ) {
removePartition ( key , part . id ) ;
}
}
} else if ( mode === 'cells' ) {
if ( hoveredPartition ) {
const key = hoveredPartition . cellKey ;
const parts = safePartitions [ key ] || [ ] ;
const part = parts . find ( p = > p . id === hoveredPartition . id ) ;
if ( part ) {
if ( e . button === 0 ) {
setDragging ( { type : 'p artition' , cellKey : key , id : part.id , axis : part.axis } ) ;
setSelectedPartitionId ( part . id ) ;
} else if ( e . button === 2 ) {
removePartition ( key , part . id ) ;
}
}
} else if ( hoveredCell && phantomPartition ) {
if ( e . button === 0 ) {
createPartition ( hoveredCell . i , hoveredCell . j , phantomPartition . axis , phantomPartition . offset ) ;
}
} else {
setSelectedPartitionId ( null ) ;
} else if ( hoveredCell && phantomPartition ) {
if ( e . button === 0 ) {
createPartition (
hoveredCell . i ,
hoveredCell . j ,
phantomPartition . axis ,
phantomPartition . offset ,
phantomP artition. min ,
phantomPartition . max
) ;
}
} else {
setSelectedPartitionId ( null ) ;
}
}
} ;
// --- RENDER HELPERS ---
// --- Render ---
const renderCells = ( ) = > {
const elements = [ ] ;
for ( let i = 0 ; i < sortedX . length - 1 ; i ++ ) {
for ( let j = 0 ; j < sortedY . length - 1 ; j ++ ) {
const x1 = sortedX [ i ] ;
const x 2 = sortedX [ i + 1 ] ;
const y1 = sortedY [ j ] ;
const y2 = sortedY [ j + 1 ] ;
const x1 = sortedX [ i ] ; const x2 = sortedX [ i + 1 ] ;
const y1 = sortedY [ j ] ; const y 2 = sortedY [ j + 1 ] ;
if ( y2 === undefined || x2 === undefined ) continue ;
const cellX = x1 * viewBoxW ;
const cellY = y1 * viewBoxH ;
const cellW = ( x2 - x1 ) * viewBoxW ;
const cellH = ( y2 - y1 ) * viewBoxH ;
const cellX = x1 * viewBoxW ; const cellY = y1 * viewBoxH ;
const cellW = ( x2 - x1 ) * viewBoxW ; const cellH = ( y2 - y1 ) * viewBoxH ;
const key = ` ${ i } - ${ j } ` ;
const isHovered = hoveredCell ? . i === i && hoveredCell ? . j === j && mode === 'cells' ;
@@ -258,18 +320,28 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
{ isHovered && (
< rect x = { cellX } y = { cellY } width = { cellW } height = { cellH } fill = "rgba(16, 185, 129, 0.05)" stroke = "#10b981" strokeWidth = "2" strokeDasharray = "4,4" className = "pointer-events-none" / >
) }
{ isSelected && mode === 'cells' && (
< rect x = { cellX } y = { cellY } width = { cellW } height = { cellH } fill = "transparent" stroke = "#a855f7" strokeWidth = "2" className = "pointer-events-none opacity-50" / >
) }
{ parts . map ( p = > {
// Рендер с учетом min/max
const pMin = p . min ? ? 0 ;
const pMax = p . max ? ? 1 ;
let lx1 , ly1 , lx2 , ly2 ;
if ( p . axis === 'x' ) {
const px = cellX + ( cellW * p . offset ) ;
lx1 = px ; ly1 = cellY ; lx2 = px ; ly2 = cellY + cellH ;
lx1 = px ;
lx2 = px ;
ly1 = cellY + ( cellH * pMin ) ;
ly2 = cellY + ( cellH * pMax ) ;
} else {
const py = cellY + ( cellH * p . offset ) ;
lx1 = cellX ; ly1 = py ; lx2 = cellX + cellW ; ly2 = py ;
ly1 = py ;
ly2 = py ;
lx1 = cellX + ( cellW * pMin ) ;
lx2 = cellX + ( cellW * pMax ) ;
}
const isPSelected = selectedPartitionId === p . id ;
const isPHovered = hoveredPartition ? . id === p . id ;
const stroke = isPSelected ? "#a855f7" : ( isPHovered ? "#d8b4fe" : "#7e22ce" ) ;
@@ -281,13 +353,28 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
< / g >
) ;
} ) }
{ isHovered && phantomPartition && ! hoveredPartition && ! dragging && (
< g className = "pointer-events-none opacity-60" >
{ phantomPartition . axis === 'x' ? (
< line x1 = { cellX + ( cellW * phantomPartition . offset ) } y1 = { cellY } x2 = { cellX + ( cellW * phantomPartition . offset ) } y2 = { cellY + cellH } stroke = "#34d399" strokeWidth = "3" strokeDasharray = "6,6" / >
) : (
< line x1 = { cellX } y1 = { cellY + ( cellH * phantomPartition . offset ) } x2 = { cellX + cellW } y2 = { cellY + ( cellH * phantomPartition . offset ) } stroke = "#34d399" strokeWidth = "3" strokeDasharray = "6,6" / >
) }
{ ( ( ) = > {
// Рендер фантома с учетом min/max
const pMin = phantomPartition . min ;
const pMax = phantomPartition . max ;
let fx1 , fy1 , fx2 , fy2 ;
if ( phantomPartition . axis === 'x' ) {
const px = cellX + ( cellW * phantomPartition . offset ) ;
fx1 = px ; fx2 = px ;
fy1 = cellY + ( cellH * pMin ) ;
fy2 = cellY + ( cellH * pMax ) ;
} else {
const py = cellY + ( cellH * phantomPartition . offset ) ;
fy1 = py ; fy2 = py ;
fx1 = cellX + ( cellW * pMin ) ;
fx2 = cellX + ( cellW * pMax ) ;
}
return < line x1 = { fx1 } y1 = { fy1 } x2 = { fx2 } y2 = { fy2 } stroke = "#34d399" strokeWidth = "3" strokeDasharray = "6,6" / > ;
} ) ( ) }
< / g >
) }
< / g >
@@ -299,113 +386,62 @@ export const LayoutStep: React.FC<Props> = ({ config, splits, onChange }) => {
return (
< div className = "bg-slate-900 p-4 rounded-xl shadow-lg border border-slate-800 h-full flex flex-col relative overflow-hidden" >
{ /* HEADER */ }
< div className = "flex justify-between items-center mb-2 z-20 shrink-0" >
< h2 className = "text-lg font-bold flex items-center gap-2 text-primary" >
< Grid size = { 20 } / > 2 . Р е д а к т о р м а к е т а
< / h2 >
< h2 className = "text-lg font-bold flex items-center gap-2 text-primary" > < Grid size = { 20 } / > 2 . Р е д а к т о р < / h2 >
< div className = "flex bg-slate-800 p-1 rounded-lg border border-slate-700" >
< button onClick = { ( ) = > { setMode ( 'lines' ) ; setSelectedPartitionId ( null ) ; } } className = { ` flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-bold transition-all ${ mode === 'lines' ? 'bg-primary text-white shadow' : 'text-gray-400 hover:text-gray-200' } ` } > Г р а н и ц ы < / button >
< button onClick = { ( ) = > setMode ( 'cells' ) } className = { ` flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-bold transition-all ${ mode === 'cells' ? 'bg-primary text-white shadow' : 'text-gray-400 hover:text-gray-200' } ` } > В н у т р и я ч е е к < / button >
< button onClick = { ( ) = > { setMode ( 'lines' ) ; setSelectedPartitionId ( null ) ; } } className = { ` flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-bold transition-all ${ mode === 'lines' ? 'bg-primary text-white shadow' : 'text-gray-400 hover:text-gray-200' } ` } > < Move size = { 14 } / > Г р а н и ц ы < / button >
< button onClick = { ( ) = > setMode ( 'cells' ) } className = { ` flex items-center gap-2 px-3 py-1.5 rounded-md text-xs font-bold transition-all ${ mode === 'cells' ? 'bg-primary text-white shadow' : 'text-gray-400 hover:text-gray-200' } ` } > < Grid size = { 14 } / > В н у т р и < / button >
< / div >
< button onClick = { ( ) = > onChange ( { x : [ ] , y : [ ] , partitions : { } } ) } className = "px-3 py-1.5 text-xs text-red-400 border border-slate-700 rounded hover:bg-slate-800 transition-colors flex items-center gap-1" >
< RotateCcw size = { 14 } / > С б р о с
< / button >
< button onClick = { ( ) = > onChange ( { x : [ ] , y : [ ] , partitions : { } } ) } className = "px-3 py-1.5 text-xs text-red-400 border border-slate-700 rounded hover:bg-slate-800 transition-colors flex items-center gap-1" > < RotateCcw size = { 14 } / > С б р о с < / button >
< / div >
{ /* INSTRUCTIONS */ }
< div className = "bg-slate-800/50 rounded-lg px-3 py-2 mb-2 flex items-center gap-3 text-[11px] text-gray-300 border border-slate-700/50 shrink-0" >
< MousePointer2 size = { 14 } className = "text-primary" / >
{ mode === 'lines' ? (
< div className = "flex gap-3" >
< span > < b className = "text-blue-400" > Л К М : < / b > Л и н и я < / span >
< span > < b className = "text-red-400" > П К М : < / b > У д а л и т ь < / span >
< / div >
< div className = "flex gap-3" > < span > < b className = "text-blue-400" > Л К М : < / b > Л и н и я < / span > < span > < b className = "text-red-400" > П К М : < / b > У д а л и т ь < / span > < / div >
) : (
< div className = "flex gap-3" >
< span > < b className = "text-green-400" > Л К М в я ч е й к е : < / b > С т е н к а < / span >
< span > < b className = "text-purple-400" > Д р а г : < / b > Д в и г а т ь < / span >
< span > < b className = "text-red-400" > 2 xЛК М : < / b > У д а л и т ь < / span >
< / div >
< div className = "flex gap-3" > < span > < b className = "text-green-400" > Л К М в я ч е й к е : < / b > С т е н к а ( у м н а я ) < / span > < span > < b className = "text-purple-400" > Д р а г : < / b > Д в и г а т ь < / span > < span > < b className = "text-red-400" > 2 xЛК М : < / b > У д а л и т ь < / span > < / div >
) }
< / div >
{ /* CANVAS */ }
< div className = "flex-1 bg-slate-800/30 rounded-lg flex flex-col items-center justify-center relative overflow-hidden border border-slate-700/50 min-h-0 w-full" >
< div className = "relative w-full h-full flex items-center justify-center p-4" >
< div
className = "relative shadow-2xl bg-[#1e293b] border border-slate-600 rounded-sm overflow-hidden"
style = { {
// Удален preserveAspectRatio="none", теперь используем правильный масштабирование
width : aspectRatio > 1 ? 'auto' : '100%' ,
height : aspectRatio > 1 ? '100%' : 'auto' ,
aspectRatio : ` ${ 1 / aspectRatio } ` ,
maxHeight : '100%' , maxWidth : '100%' ,
cursor : mode === 'lines' ? ( dragging ? 'grabbing' : hoveredMainSplit ? 'col-resize' : 'crosshair' )
: ( dragging ? 'grabbing' : hoveredPartition ? 'grab' : hoveredCell ? 'crosshair' : 'default' ) ,
} }
>
< div className = "relative shadow-2xl bg-[#1e293b] border border-slate-600 rounded-sm overflow-hidden"
style = { { width : aspectRatio > 1 ? 'auto' : '100%' , height : aspectRatio > 1 ? '100%' : 'auto' , aspectRatio : ` ${ 1 / aspectRatio } ` , maxHeight : '100%' , maxWidth : '100%' , cursor : mode === 'lines' ? 'crosshair' : 'default' } } >
< svg ref = { svgRef } viewBox = { ` 0 0 ${ viewBoxW } ${ viewBoxH } ` } className = "w-full h-full touch-none block"
preserveAspectRatio = "none"
onMouseMove = { handleMouseMove } onMouseDown = { handleMouseDown } onMouseUp = { ( ) = > setDragging ( null ) } onMouseLeave = { ( ) = > setDragging ( null ) } onContextMenu = { ( e ) = > e . preventDefault ( ) }
>
< defs >
< pattern id = "grid" width = "50" height = "50" patternUnits = "userSpaceOnUse" >
< path d = "M 50 0 L 0 0 0 50" fill = "none" stroke = "rgba(255,255,255,0.03)" strokeWidth = "1" / >
< / pattern >
< / defs >
< defs > < pattern id = "grid" width = "50" height = "50" patternUnits = "userSpaceOnUse" > < path d = "M 50 0 L 0 0 0 50" fill = "none" stroke = "rgba(255,255,255,0.03)" strokeWidth = "1" / > < / pattern > < / defs >
< rect width = "100%" height = "100%" fill = "url(#grid)" / >
{ renderCells ( ) }
{ /* Main Grid Lines */ }
{ safeX . map ( ( x , i ) = > {
const isHovered = hoveredMainSplit ? . axis === 'x' && hoveredMainSplit . index === i ;
return (
< g key = { ` x- ${ i } ` } onMouseMove = { ( e ) = > { if ( mode === 'lines' && ! dragging ) { e . stopPropagation ( ) ; setHoveredMainSplit ( { axis : 'x' , index : i } ) ; setPhantomMainAxis ( null ) ; } } } onDoubleClick = { ( e ) = > { e . stopPropagation ( ) ; removeMainSplit ( 'x' , i ) ; } } >
< line x1 = { x * viewBoxW } y1 = "0" x2 = { x * viewBoxW } y2 = "100%" stroke = "transparent" strokeWidth = "40" className = { mode === 'lines' ? "cursor-col-resize" : "" } / >
< line x1 = { x * viewBoxW } y1 = "0" x2 = { x * viewBoxW } y2 = "100%" stroke = { isHovered ? "#f59e0b" : "#64748b" } strokeWidth = { isHovered ? 6 : 4 } className = "pointer-events-none" / >
< /g >
) ;
} ) }
{ safeY . map ( ( y , i ) = > {
const isHovered = hoveredMainSplit ? . axis === 'y' && hoveredMainSplit . index === i ;
return (
< g key = { ` y- ${ i } ` } onMouseMove = { ( e ) = > { if ( mode === 'lines' && ! dragging ) { e . stopPropagation ( ) ; setHoveredMainSplit ( { axis : 'y' , index : i } ) ; setPhantomMainAxis ( null ) ; } } } onDoubleClick = { ( e ) = > { e . stopPropagation ( ) ; removeMainSplit ( 'y' , i ) ; } } >
< line x1 = "0" y1 = { y * viewBoxH } x2 = "100%" y2 = { y * viewBoxH } stroke = "transparent" strokeWidth = "40" className = { mode === 'lines' ? "cursor-row-resize" : "" } / >
< line x1 = "0" y1 = { y * viewBoxH } x2 = "100%" y2 = { y * viewBoxH } stroke = { isHovered ? "#f59e0b" : "#64748b" } strokeWidth = { isHovered ? 6 : 4 } className = "pointer-events-none" / >
< / g >
) ;
} ) }
{ /* Phantoms */ }
{ mode === 'lines' && ! hoveredMainSplit && ! dragging && phantomMainAxis === 'x' && < line x1 = { mousePos . x * viewBoxW } y1 = "0" x2 = { mousePos . x * viewBoxW } y2 = "100%" stroke = "#3b82f6" strokeWidth = "4" strokeDasharray = "8,8" className = "pointer-events-none opacity-50" / > }
{ mode === 'lines' && ! hoveredMainSplit && ! dragging && phantomMainAxis === 'y' && < line x1 = "0" y1 = { mousePos . y * viewBoxH } x2 = "100%" y2 = { mousePos . y * viewBoxH } stroke = "#3b82f6" strokeWidth = "4" strokeDasharray = "8,8" className = "pointer-events-none opacity-50" / > }
{ safeX . map ( ( x , i ) = > (
< g key = { ` x- ${ i } ` } onMouseMove = { ( e ) = > { if ( mode === 'lines' && ! dragging ) { e . stopPropagation ( ) ; setHoveredMainSplit ( { axis : 'x' , index : i } ) ; setPhantomMainAxis ( null ) ; } } } onDoubleClick = { ( e ) = > { e . stopPropagation ( ) ; removeMainSplit ( 'x' , i ) ; } } >
< line x1 = { x * viewBoxW } y1 = "0" x2 = { x * viewBoxW } y2 = "100%" stroke = "transparent" strokeWidth = "40" className = { mode === 'lines' ? "cursor-col-resize" : "" } / >
< line x1 = { x * viewBoxW } y1 = "0" x2 = { x * viewBoxW } y2 = "100%" stroke = { hoveredMainSplit ? . axis === 'x' && hoveredMainSplit . index === i ? "#f59e0b" : "#64748b" } strokeWidth = { hoveredMainSplit? . axis === 'x' && hoveredMainSplit . index === i ? 6 : 4 } className = "pointer-events-none" / >
< / g >
) ) }
{ safeY . map ( ( y , i ) = > (
< g key = { ` y- ${ i } ` } onMouseMove = { ( e ) = > { if ( mode === 'lines' && ! dragging ) { e . stopPropagation ( ) ; setHoveredMainSplit ( { axis : 'y' , index : i } ) ; setPhantomMainAxis ( null ) ; } } } onDoubleClick = { ( e ) = > { e . stopPropagation ( ) ; removeMainSplit ( 'y' , i ) ; } } >
< line x1 = "0" y1 = { y * viewBoxH } x2 = "100%" y2 = { y * viewBoxH } stroke = "transparent" strokeWidth = "40" className = { mode === 'lines' ? "cursor-row-resize" : "" } / >
< line x1 = "0" y1 = { y * viewBoxH } x2 = "100%" y2 = { y * viewBoxH } stroke = { hoveredMainSplit ? . axis === 'y' && hoveredMainSplit . index === i ? "#f59e0b" : "#64748b" } strokeWidth = { hoveredMainSplit ? . axis === 'y' && hoveredMainSplit . index === i ? 6 : 4 } className = "pointer-events-none" / >
< / g >
) ) }
{ mode === 'lines' && ! hoveredMainSplit && ! dragging && phantomMainAxis && (
< line x1 = { phantomMainAxis === 'x' ? mousePos . x * viewBoxW : 0 } y1 = { phantomMainAxis === 'y' ? mousePos . y * viewBoxH : 0 } x2 = { phantomMainAxis === 'x' ? mousePos . x * viewBoxW : '100%' } y2 = { phantomMainAxis === 'y' ? mousePos . y * viewBoxH : '100%' } stroke = "#3b82f6" strokeWidth = "4" strokeDasharray = "8,8" className = "pointer-events-none opacity-50" / >
) }
< / svg >
< / div >
< / div >
{ /* SIDEBAR */ }
{ mode === 'cells' && selectedData && (
< div className = "absolute top-0 right-0 bottom-0 w-72 bg-slate-900 border-l border-slate-700 p-4 shadow-2xl flex flex-col z-30 animate-in slide-in-from-right duration-200" >
< div className = "flex justify-between items-center mb-6" >
< h3 className = "text-sm font-bold text-white flex items-center gap-2" > Н а с т р о й к и с т е н к и < / h3 >
< button onClick = { ( ) = > setSelectedPartitionId ( null ) } className = "text-gray-400 hover:text-white" > < X size = { 20 } / > < / button >
< / div >
< div className = "flex justify-between items-center mb-6" > < h3 className = "text-sm font-bold text-white flex items-center gap-2" > Н а с т р о й к и с т е н к и < / h3 > < button onClick = { ( ) = > setSelectedPartitionId ( null ) } className = "text-gray-400 hover:text-white" > < X size = { 20 } / > < / button > < / div >
< div className = "bg-slate-800 p-4 rounded border border-slate-700 space-y-6" >
< div >
< div className = "flex justify-between text-xs text-gray-300 mb-2" > < span > В ы с о т а < / span > < span className = "font-mono bg-slate-900 px-1.5 py-0.5 rounded text-xs" > { selectedData . part . height } м м < / span > < / div >
< input type = "range" min = "5" max = { config . drawer . height || 100 } step = "1" value = { selectedData . part . height } onChange = { ( e ) = > updat ePartition( selectedData . key , selectedData . part . id , { height : parseFloat ( e . target . value ) } ) } className = "w-full h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-purple-500" / >
< / div >
< div className = "flex items-center justify-between" >
< label htmlFor = "rounded-check" className = "text-xs text-gray-300 cursor-pointer select-none" > С к р у г л и т ь к р а я < / label >
< input type = "checkbox" id = "rounded-check" checked = { selectedData . part . rounded } onChange = { ( e ) = > updatePartition ( selectedData . key , selectedData . part . id , { rounded : e.target.checked } ) } className = "w-4 h-4 rounded bg-slate-700 border-slate-600 text-purple-500 focus:ring-0 cursor-pointer" / >
< / div >
< button onClick = { ( ) = > removePartition ( selectedData . key , selectedData . part . id ) } className = "w-full py-2 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/30 rounded text-xs flex items-center justify-center gap-2 transition-colors mt-4" >
< Trash2 size = { 14 } / > У д а л и т ь
< / button >
< div > < div className = "flex justify-between text-xs text-gray-300 mb-2" > < span > В ы с о т а < / span > < span className = "font-mono bg-slate-900 px-1.5 py-0.5 rounded text-xs" > { selectedData . part . height } м м < / span > < / div > < input type = "range" min = "5" max = { config . drawer . height || 100 } step = "1" value = { selectedData . part . height } onChange = { ( e ) = > updatePartition ( selectedData . key , selectedData . part . id , { height : parseFloat ( e . target . value ) } ) } className = "w-full h-1 bg-slate-600 rounded-lg appearance-none cursor-pointer accent-purple-500" / > < / div >
< div className = "flex items-center justify-between" > < label htmlFor = "rounded-check" className = "text-xs text-gray-300 cursor-pointer select-none" > С к р у г л и т ь к р а я < / label > < input type = "checkbox" id = "rounded-check" checked = { selectedData . part . rounded } onChange = { ( e ) = > updatePartition ( selectedData . key , selectedData . part . id , { rounded : e.target.checked } ) } className = "w-4 h-4 rounded bg-slate-700 border-slate-600 text-purple-500 focus:ring-0 cursor-pointer" / > < / div >
< button onClick = { ( ) = > remov ePartition( selectedData . key , selectedData . part . id ) } className = "w-full py-2 bg-red-500/10 hover:bg-red-500/20 text-red-400 border border-red-500/30 rounded text-xs flex items-center justify-center gap-2 transition-colors mt-4" > < Trash2 size = { 14 } / > У д а л и т ь < / button >
< / div >
< div className = "mt-auto text-[10px] text-gray-500 text-center leading-relaxed" > В ы д е л и т е с т е н к у д л я н а с т р о й к и . < br / > Д в о й н о й к л и к у д а л я е т е ё . < / div >
< / div >
) }
< / div >