mirror of
https://github.com/xmanrui/OpenClaw-bot-review.git
synced 2026-07-27 22:25:52 +00:00
304 lines
15 KiB
TypeScript
304 lines
15 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect, useRef, useCallback } from 'react'
|
|
import { EditTool } from '@/lib/pixel-office/types'
|
|
import type { TileType as TileTypeVal, FloorColor } from '@/lib/pixel-office/types'
|
|
import { getCatalogByCategory, getActiveCategories } from '@/lib/pixel-office/layout/furnitureCatalog'
|
|
import type { FurnitureCategory } from '@/lib/pixel-office/layout/furnitureCatalog'
|
|
import { getCachedSprite } from '@/lib/pixel-office/sprites/spriteCache'
|
|
import { getColorizedFloorSprite, getFloorPatternCount, hasFloorSprites } from '@/lib/pixel-office/floorTiles'
|
|
|
|
const btnStyle: React.CSSProperties = {
|
|
padding: '3px 8px',
|
|
fontSize: '12px',
|
|
background: 'rgba(255, 255, 255, 0.08)',
|
|
color: 'rgba(255, 255, 255, 0.7)',
|
|
border: '2px solid transparent',
|
|
borderRadius: 0,
|
|
cursor: 'pointer',
|
|
}
|
|
|
|
const activeBtnStyle: React.CSSProperties = {
|
|
...btnStyle,
|
|
background: 'rgba(90, 140, 255, 0.25)',
|
|
color: 'rgba(255, 255, 255, 0.9)',
|
|
border: '2px solid #5a8cff',
|
|
}
|
|
|
|
const tabStyle: React.CSSProperties = {
|
|
padding: '2px 6px',
|
|
fontSize: '11px',
|
|
background: 'transparent',
|
|
color: 'rgba(255, 255, 255, 0.5)',
|
|
border: '2px solid transparent',
|
|
borderRadius: 0,
|
|
cursor: 'pointer',
|
|
}
|
|
|
|
const activeTabStyle: React.CSSProperties = {
|
|
...tabStyle,
|
|
background: 'rgba(255, 255, 255, 0.08)',
|
|
color: 'rgba(255, 255, 255, 0.8)',
|
|
border: '2px solid #5a8cff',
|
|
}
|
|
|
|
function FloorPatternPreview({ patternIndex, color, selected, onClick }: {
|
|
patternIndex: number
|
|
color: FloorColor
|
|
selected: boolean
|
|
onClick: () => void
|
|
}) {
|
|
const canvasRef = useRef<HTMLCanvasElement>(null)
|
|
const displaySize = 32
|
|
const tileZoom = 2
|
|
|
|
useEffect(() => {
|
|
const canvas = canvasRef.current
|
|
if (!canvas) return
|
|
const ctx = canvas.getContext('2d')
|
|
if (!ctx) return
|
|
canvas.width = displaySize
|
|
canvas.height = displaySize
|
|
ctx.imageSmoothingEnabled = false
|
|
if (!hasFloorSprites()) {
|
|
ctx.fillStyle = '#444'
|
|
ctx.fillRect(0, 0, displaySize, displaySize)
|
|
return
|
|
}
|
|
const sprite = getColorizedFloorSprite(patternIndex, color)
|
|
const cached = getCachedSprite(sprite, tileZoom)
|
|
ctx.drawImage(cached, 0, 0)
|
|
}, [patternIndex, color])
|
|
|
|
return (
|
|
<button onClick={onClick} title={`Floor ${patternIndex}`} style={{
|
|
width: displaySize, height: displaySize, padding: 0,
|
|
border: selected ? '2px solid #5a8cff' : '2px solid #4a4a6a',
|
|
borderRadius: 0, cursor: 'pointer', overflow: 'hidden', flexShrink: 0, background: '#2A2A3A',
|
|
}}>
|
|
<canvas ref={canvasRef} style={{ width: displaySize, height: displaySize, display: 'block' }} />
|
|
</button>
|
|
)
|
|
}
|
|
|
|
function ColorSlider({ label, value, min, max, onChange }: {
|
|
label: string; value: number; min: number; max: number; onChange: (v: number) => void
|
|
}) {
|
|
return (
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
<span style={{ fontSize: '11px', color: '#999', width: 16, textAlign: 'right', flexShrink: 0 }}>{label}</span>
|
|
<input type="range" min={min} max={max} value={value}
|
|
onChange={(e) => onChange(Number(e.target.value))}
|
|
style={{ flex: 1, height: 12, accentColor: 'rgba(90, 140, 255, 0.8)' }} />
|
|
<span style={{ fontSize: '11px', color: '#999', width: 32, textAlign: 'right', flexShrink: 0 }}>{value}</span>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const DEFAULT_FURNITURE_COLOR: FloorColor = { h: 0, s: 0, b: 0, c: 0 }
|
|
|
|
interface EditorToolbarProps {
|
|
activeTool: EditTool
|
|
selectedTileType: TileTypeVal
|
|
selectedFurnitureType: string
|
|
selectedFurnitureUid: string | null
|
|
selectedFurnitureColor: FloorColor | null
|
|
floorColor: FloorColor
|
|
wallColor: FloorColor
|
|
onToolChange: (tool: EditTool) => void
|
|
onTileTypeChange: (type: TileTypeVal) => void
|
|
onFloorColorChange: (color: FloorColor) => void
|
|
onWallColorChange: (color: FloorColor) => void
|
|
onSelectedFurnitureColorChange: (color: FloorColor | null) => void
|
|
onFurnitureTypeChange: (type: string) => void
|
|
onDeleteFurniture: () => void
|
|
}
|
|
|
|
export function EditorToolbar({
|
|
activeTool, selectedTileType, selectedFurnitureType,
|
|
selectedFurnitureUid, selectedFurnitureColor,
|
|
floorColor, wallColor,
|
|
onToolChange, onTileTypeChange, onFloorColorChange, onWallColorChange,
|
|
onSelectedFurnitureColorChange, onFurnitureTypeChange, onDeleteFurniture,
|
|
}: EditorToolbarProps) {
|
|
const [activeCategory, setActiveCategory] = useState<FurnitureCategory>('desks')
|
|
const [showColor, setShowColor] = useState(false)
|
|
const [showWallColor, setShowWallColor] = useState(false)
|
|
const [showFurnitureColor, setShowFurnitureColor] = useState(false)
|
|
|
|
const handleColorChange = useCallback((key: keyof FloorColor, value: number) => {
|
|
onFloorColorChange({ ...floorColor, [key]: value })
|
|
}, [floorColor, onFloorColorChange])
|
|
|
|
const handleWallColorChange = useCallback((key: keyof FloorColor, value: number) => {
|
|
onWallColorChange({ ...wallColor, [key]: value })
|
|
}, [wallColor, onWallColorChange])
|
|
|
|
const effectiveColor = selectedFurnitureColor ?? DEFAULT_FURNITURE_COLOR
|
|
const handleSelFurnColorChange = useCallback((key: keyof FloorColor, value: number) => {
|
|
onSelectedFurnitureColorChange({ ...effectiveColor, [key]: value })
|
|
}, [effectiveColor, onSelectedFurnitureColorChange])
|
|
|
|
const categoryItems = getCatalogByCategory(activeCategory)
|
|
const patternCount = getFloorPatternCount()
|
|
const floorPatterns = Array.from({ length: patternCount }, (_, i) => i + 1)
|
|
const thumbSize = 36
|
|
|
|
const isSelectActive = activeTool === EditTool.SELECT
|
|
const isFloorActive = activeTool === EditTool.TILE_PAINT || activeTool === EditTool.EYEDROPPER
|
|
const isWallActive = activeTool === EditTool.WALL_PAINT
|
|
const isEraseActive = activeTool === EditTool.ERASE
|
|
const isFurnitureActive = activeTool === EditTool.FURNITURE_PLACE || activeTool === EditTool.FURNITURE_PICK
|
|
|
|
return (
|
|
<div style={{
|
|
position: 'absolute', bottom: 12, left: 10, zIndex: 50,
|
|
background: '#1e1e2e', border: '2px solid #4a4a6a', borderRadius: 0,
|
|
padding: '6px 8px', display: 'flex', flexDirection: 'column-reverse', gap: 6,
|
|
boxShadow: '2px 2px 0px #0a0a14', maxWidth: 'calc(100vw - 20px)',
|
|
}}>
|
|
{/* Tool row */}
|
|
<div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
|
|
<button style={isSelectActive ? activeBtnStyle : btnStyle} onClick={() => onToolChange(EditTool.SELECT)} title="Select / move furniture">Select</button>
|
|
<button style={isFloorActive ? activeBtnStyle : btnStyle} onClick={() => onToolChange(EditTool.TILE_PAINT)} title="Paint floor tiles">Floor</button>
|
|
<button style={isWallActive ? activeBtnStyle : btnStyle} onClick={() => onToolChange(EditTool.WALL_PAINT)} title="Paint walls">Wall</button>
|
|
<button style={isEraseActive ? activeBtnStyle : btnStyle} onClick={() => onToolChange(EditTool.ERASE)} title="Erase tiles">Erase</button>
|
|
<button style={isFurnitureActive ? activeBtnStyle : btnStyle} onClick={() => onToolChange(EditTool.FURNITURE_PLACE)} title="Place furniture">Furniture</button>
|
|
</div>
|
|
|
|
{/* Delete button when furniture is selected */}
|
|
{selectedFurnitureUid && (
|
|
<div style={{ display: 'flex', gap: 4 }}>
|
|
<button
|
|
style={{ ...btnStyle, background: 'rgba(255, 80, 80, 0.2)', border: '2px solid #ff5050', color: '#ff9090' }}
|
|
onClick={onDeleteFurniture}
|
|
title="Delete selected furniture (Delete key)"
|
|
>
|
|
🗑 Delete
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{/* Floor sub-panel */}
|
|
{isFloorActive && (
|
|
<div style={{ display: 'flex', flexDirection: 'column-reverse', gap: 6 }}>
|
|
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
|
<button style={showColor ? activeBtnStyle : btnStyle} onClick={() => setShowColor(v => !v)} title="Adjust floor color">Color</button>
|
|
<button style={activeTool === EditTool.EYEDROPPER ? activeBtnStyle : btnStyle} onClick={() => onToolChange(EditTool.EYEDROPPER)} title="Pick floor pattern + color">Pick</button>
|
|
</div>
|
|
{showColor && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 3, padding: '4px 6px', background: '#181828', border: '2px solid #4a4a6a', borderRadius: 0 }}>
|
|
<ColorSlider label="H" value={floorColor.h} min={0} max={360} onChange={v => handleColorChange('h', v)} />
|
|
<ColorSlider label="S" value={floorColor.s} min={0} max={100} onChange={v => handleColorChange('s', v)} />
|
|
<ColorSlider label="B" value={floorColor.b} min={-100} max={100} onChange={v => handleColorChange('b', v)} />
|
|
<ColorSlider label="C" value={floorColor.c} min={-100} max={100} onChange={v => handleColorChange('c', v)} />
|
|
</div>
|
|
)}
|
|
<div style={{ display: 'flex', gap: 4, overflowX: 'auto', flexWrap: 'nowrap', paddingBottom: 2 }}>
|
|
{floorPatterns.map(patIdx => (
|
|
<FloorPatternPreview key={patIdx} patternIndex={patIdx} color={floorColor} selected={selectedTileType === patIdx}
|
|
onClick={() => onTileTypeChange(patIdx as TileTypeVal)} />
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Wall sub-panel */}
|
|
{isWallActive && (
|
|
<div style={{ display: 'flex', flexDirection: 'column-reverse', gap: 6 }}>
|
|
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
|
<button style={showWallColor ? activeBtnStyle : btnStyle} onClick={() => setShowWallColor(v => !v)} title="Adjust wall color">Color</button>
|
|
</div>
|
|
{showWallColor && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 3, padding: '4px 6px', background: '#181828', border: '2px solid #4a4a6a', borderRadius: 0 }}>
|
|
<ColorSlider label="H" value={wallColor.h} min={0} max={360} onChange={v => handleWallColorChange('h', v)} />
|
|
<ColorSlider label="S" value={wallColor.s} min={0} max={100} onChange={v => handleWallColorChange('s', v)} />
|
|
<ColorSlider label="B" value={wallColor.b} min={-100} max={100} onChange={v => handleWallColorChange('b', v)} />
|
|
<ColorSlider label="C" value={wallColor.c} min={-100} max={100} onChange={v => handleWallColorChange('c', v)} />
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Furniture sub-panel */}
|
|
{isFurnitureActive && (
|
|
<div style={{ display: 'flex', flexDirection: 'column-reverse', gap: 4 }}>
|
|
<div style={{ display: 'flex', gap: 2, flexWrap: 'wrap', alignItems: 'center' }}>
|
|
{getActiveCategories().map(cat => (
|
|
<button key={cat.id} style={activeCategory === cat.id ? activeTabStyle : tabStyle} onClick={() => setActiveCategory(cat.id)}>{cat.label}</button>
|
|
))}
|
|
<div style={{ width: 1, height: 14, background: 'rgba(255,255,255,0.15)', margin: '0 2px', flexShrink: 0 }} />
|
|
<button style={activeTool === EditTool.FURNITURE_PICK ? activeBtnStyle : btnStyle} onClick={() => onToolChange(EditTool.FURNITURE_PICK)} title="Pick furniture type">Pick</button>
|
|
</div>
|
|
<div style={{ display: 'flex', gap: 4, overflowX: 'auto', flexWrap: 'nowrap', paddingBottom: 2 }}>
|
|
{categoryItems.map(entry => {
|
|
const hasSprite = entry.sprite.length > 0
|
|
const isSelected = selectedFurnitureType === entry.type
|
|
return (
|
|
<button key={entry.type} onClick={() => onFurnitureTypeChange(entry.type)} title={entry.label} style={{
|
|
width: thumbSize, height: thumbSize, background: '#2A2A3A',
|
|
border: isSelected ? '2px solid #5a8cff' : '2px solid #4a4a6a',
|
|
borderRadius: 0, cursor: 'pointer', padding: 0, display: 'flex',
|
|
alignItems: 'center', justifyContent: 'center', overflow: 'hidden', flexShrink: 0,
|
|
fontSize: hasSprite ? undefined : 20,
|
|
}}>
|
|
{hasSprite ? (
|
|
<canvas ref={el => {
|
|
if (!el) return
|
|
const ctx = el.getContext('2d')
|
|
if (!ctx) return
|
|
const cached = getCachedSprite(entry.sprite, 2)
|
|
const scale = Math.min(thumbSize / cached.width, thumbSize / cached.height) * 0.85
|
|
el.width = thumbSize; el.height = thumbSize
|
|
ctx.imageSmoothingEnabled = false; ctx.clearRect(0, 0, thumbSize, thumbSize)
|
|
const dw = cached.width * scale; const dh = cached.height * scale
|
|
ctx.drawImage(cached, (thumbSize - dw) / 2, (thumbSize - dh) / 2, dw, dh)
|
|
}} style={{ width: thumbSize, height: thumbSize }} />
|
|
) : (
|
|
<span>{entry.emoji ?? '?'}</span>
|
|
)}
|
|
</button>
|
|
)
|
|
})}
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Selected furniture color panel */}
|
|
{selectedFurnitureUid && (
|
|
<div style={{ display: 'flex', flexDirection: 'column-reverse', gap: 3 }}>
|
|
<div style={{ display: 'flex', gap: 4, alignItems: 'center' }}>
|
|
<button style={showFurnitureColor ? activeBtnStyle : btnStyle} onClick={() => setShowFurnitureColor(v => !v)} title="Adjust selected furniture color">Color</button>
|
|
{selectedFurnitureColor && (
|
|
<button style={{ ...btnStyle, fontSize: '11px', padding: '2px 6px' }} onClick={() => onSelectedFurnitureColorChange(null)} title="Remove color">Clear</button>
|
|
)}
|
|
</div>
|
|
{showFurnitureColor && (
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: 3, padding: '4px 6px', background: '#181828', border: '2px solid #4a4a6a', borderRadius: 0 }}>
|
|
{effectiveColor.colorize ? (
|
|
<>
|
|
<ColorSlider label="H" value={effectiveColor.h} min={0} max={360} onChange={v => handleSelFurnColorChange('h', v)} />
|
|
<ColorSlider label="S" value={effectiveColor.s} min={0} max={100} onChange={v => handleSelFurnColorChange('s', v)} />
|
|
</>
|
|
) : (
|
|
<>
|
|
<ColorSlider label="H" value={effectiveColor.h} min={-180} max={180} onChange={v => handleSelFurnColorChange('h', v)} />
|
|
<ColorSlider label="S" value={effectiveColor.s} min={-100} max={100} onChange={v => handleSelFurnColorChange('s', v)} />
|
|
</>
|
|
)}
|
|
<ColorSlider label="B" value={effectiveColor.b} min={-100} max={100} onChange={v => handleSelFurnColorChange('b', v)} />
|
|
<ColorSlider label="C" value={effectiveColor.c} min={-100} max={100} onChange={v => handleSelFurnColorChange('c', v)} />
|
|
<label style={{ display: 'flex', alignItems: 'center', gap: 4, fontSize: '11px', color: '#999', cursor: 'pointer' }}>
|
|
<input type="checkbox" checked={!!effectiveColor.colorize}
|
|
onChange={e => onSelectedFurnitureColorChange({ ...effectiveColor, colorize: e.target.checked || undefined })}
|
|
style={{ accentColor: 'rgba(90, 140, 255, 0.8)' }} />
|
|
Colorize
|
|
</label>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|