Erster Commit

This commit is contained in:
2026-04-23 22:10:45 +02:00
commit 5d47482d2a
30 changed files with 6340 additions and 0 deletions

View File

@@ -0,0 +1,298 @@
import { useState, useEffect, useCallback, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import DrawCanvas, { type DrawCanvasHandle } from '../components/DrawCanvas'
import ObjectsList from '../components/ObjectsList'
import { getImages, getObjects, cropImage, saveImage } from '../api'
import type { ObjectMeta, Selection } from '../types'
const FIELD_LABELS: Record<string, string> = {
title_de: 'Titel / title_de',
position_de: 'Position / position_de',
action_de: 'Status (sitzt/schwimmt/segelt) / action_de',
condition_de: 'Zustand (alt/jung/rostig) / condition_de',
}
const FIELD_PLACEHOLDERS: Record<string, string> = {
action_de: 'z.B. sitzt',
condition_de: 'z.B. rostig',
}
type FormKey = 'title_de' | 'position_de' | 'action_de' | 'condition_de'
export default function DrawIt() {
const navigate = useNavigate()
const [imageList, setImageList] = useState<string[]>([])
const [currentIndex, setCurrentIndex] = useState(-1)
const [objects, setObjects] = useState<ObjectMeta[]>([])
const [currentSelections, setCurrentSelections] = useState<Selection[]>([])
const [status, setStatus] = useState('')
const [statusError, setStatusError] = useState(false)
const [mode, setMode] = useState<'rect' | 'polygon'>('rect')
const [hasSelection, setHasSelection] = useState(false)
const [selectedObjectId, setSelectedObjectId] = useState<string | null>(null)
const [form, setForm] = useState<Record<FormKey, string>>({
title_de: '',
position_de: '',
action_de: '',
condition_de: '',
})
const canvasRef = useRef<DrawCanvasHandle>(null)
const currentFilename = currentIndex >= 0 && currentIndex < imageList.length
? imageList[currentIndex]
: null
useEffect(() => {
getImages('draw')
.then(imgs => {
setImageList(imgs)
setCurrentIndex(imgs.length - 1)
})
.catch(console.error)
}, [])
useEffect(() => {
if (!currentFilename) {
setObjects([])
setSelectedObjectId(null)
return
}
getObjects(currentFilename)
.then(objs => {
setObjects(objs.map(o => ({ ...o, visible: true })))
setSelectedObjectId(objs[0]?.id ?? null)
})
.catch(console.error)
}, [currentFilename])
const handleHasSelection = useCallback((has: boolean) => setHasSelection(has), [])
const showStatus = (msg: string, isError = false) => {
setStatus(msg)
setStatusError(isError)
}
const addSelection = () => {
const sel = canvasRef.current?.getCurrentSelection()
if (!sel) {
showStatus('Bitte zuerst einen Bereich auswählen.', true)
return
}
setCurrentSelections(prev => {
const next = [...prev, sel]
showStatus(`Auswahl ${next.length} hinzugefügt.`)
return next
})
canvasRef.current?.resetSelection()
setHasSelection(false)
}
const saveObject = async () => {
if (!currentFilename || currentSelections.length === 0) return
try {
showStatus('Speichere Objekt...')
const result = await cropImage({
filename: currentFilename,
selections: currentSelections.map((sel, idx) => ({
number: idx + 1,
mode: sel.mode,
bbox: sel.bbox ?? null,
polygon: sel.polygon ?? null,
})),
...form,
})
showStatus(`Gespeichert ID: ${result.id} (${currentSelections.length} Auswahl(en))`)
setCurrentSelections([])
setForm({ title_de: '', position_de: '', action_de: '', condition_de: '' })
const objs = await getObjects(currentFilename)
setObjects(objs.map(o => ({ ...o, visible: true })))
} catch (e) {
showStatus(e instanceof Error ? e.message : 'Fehler beim Speichern.', true)
}
}
const handleSaveImage = async () => {
if (!currentFilename) return
try {
showStatus('Bild wird gespeichert...')
await saveImage(currentFilename)
const imgs = await getImages('draw')
setImageList(imgs)
setCurrentIndex(imgs.length - 1)
showStatus('Bild gespeichert.')
} catch (e) {
showStatus(e instanceof Error ? e.message : 'Fehler.', true)
}
}
return (
<div className="container">
<h1>DrawIt</h1>
<div className="panel image-nav">
<div className="image-nav-left">
<button
onClick={() => setCurrentIndex(i => i - 1)}
disabled={currentIndex <= 0}
>
</button>
<span>Bild: <code>{currentFilename || ''}</code></span>
<button
onClick={() => setCurrentIndex(i => i + 1)}
disabled={currentIndex >= imageList.length - 1}
>
</button>
<button onClick={handleSaveImage} disabled={!currentFilename}>💾</button>
</div>
<div className="page-switch">
<select value="/draw" onChange={e => navigate(e.target.value)}>
<option value="/draw">DrawIt</option>
<option value="/generate">GenerateIt</option>
</select>
</div>
</div>
<div className="main-layout">
{/* Left: Objects */}
<div className="objects-pane sidebar-section">
<h2>Objekte zu diesem Bild</h2>
<ObjectsList
objects={objects}
selectedObjectId={selectedObjectId}
onSelect={setSelectedObjectId}
onVisibilityChange={(id, visible) =>
setObjects(prev => prev.map(o => o.id === id ? { ...o, visible } : o))
}
onObjectsChange={setObjects}
isGeneratePage={false}
/>
</div>
{/* Center: Canvas */}
<div className="left-pane">
<div className="canvas-wrapper">
<DrawCanvas
ref={canvasRef}
imageSrc={
currentFilename
? `/pictures/${encodeURIComponent(currentFilename)}`
: null
}
objects={objects}
selectedObjectId={selectedObjectId}
mode={mode}
onHasSelection={handleHasSelection}
/>
</div>
</div>
{/* Right: Controls */}
<div className="right-pane">
<div className="sidebar-section">
<h2>Auswahl</h2>
<div className="sidebar-row">
<span>Auswahl-Typ (Interface / Backend):</span>
</div>
<div className="sidebar-row">
<label className="mode-option">
<input
type="radio"
name="mode"
value="rect"
checked={mode === 'rect'}
onChange={() => setMode('rect')}
/>
Rechteck / <code>BBox</code>
</label>
</div>
<div className="sidebar-row">
<label className="mode-option">
<input
type="radio"
name="mode"
value="polygon"
checked={mode === 'polygon'}
onChange={() => setMode('polygon')}
/>
Polygon / <code>polygon</code>
</label>
</div>
<div className="sidebar-row">
<button type="button" onClick={() => canvasRef.current?.resetSelection()}>
Auswahl zurücksetzen
</button>
</div>
</div>
<div className="sidebar-section">
<h2>Metadaten</h2>
{(['title_de', 'position_de', 'action_de', 'condition_de'] as FormKey[]).map(key => (
<div className="sidebar-row" key={key}>
<label htmlFor={key}>{FIELD_LABELS[key]}</label>
<input
id={key}
type="text"
value={form[key]}
placeholder={FIELD_PLACEHOLDERS[key] || ''}
onChange={e => setForm(f => ({ ...f, [key]: e.target.value }))}
/>
</div>
))}
</div>
<div className="sidebar-section">
<h2>Auswahlen</h2>
<div className="selections-list">
{currentSelections.length === 0 ? (
<div className="selections-empty">Noch keine Auswahlen hinzugefügt.</div>
) : (
currentSelections.map((sel, i) => (
<div className="selection-item" key={i}>
<strong>Auswahl {i + 1}</strong> ({sel.mode === 'rect' ? 'Rechteck' : 'Polygon'}):
{sel.mode === 'rect' && sel.bbox
? ` x=${sel.bbox.x}, y=${sel.bbox.y}, w=${sel.bbox.width}, h=${sel.bbox.height}`
: ` ${sel.polygon?.length ?? 0} Punkte`}
</div>
))
)}
</div>
<div className="sidebar-row">
<button
onClick={addSelection}
disabled={!hasSelection || !currentFilename}
>
Auswahl hinzufügen
</button>
</div>
<div className="sidebar-row">
<button
onClick={saveObject}
disabled={!currentFilename || currentSelections.length === 0}
>
💾 Objekt speichern
</button>
</div>
<div className="sidebar-row">
<button
type="button"
onClick={() => {
setCurrentSelections([])
canvasRef.current?.resetSelection()
showStatus('Alle Auswahlen gelöscht.')
}}
>
🗑 Alle Auswahlen löschen
</button>
</div>
{status && (
<span className={`status ${statusError ? 'error' : 'ok'}`}>{status}</span>
)}
</div>
</div>
</div>
</div>
)
}