erste Funktionierende Fassung
This commit is contained in:
@@ -48,23 +48,23 @@ if not exist "%PV_CLIENT%" mkdir "%PV_CLIENT%"
|
|||||||
if not exist "%PV_SERVER%" mkdir "%PV_SERVER%"
|
if not exist "%PV_SERVER%" mkdir "%PV_SERVER%"
|
||||||
|
|
||||||
REM Umgebungsvariablen anzeigen
|
REM Umgebungsvariablen anzeigen
|
||||||
echo.
|
REM echo.
|
||||||
echo ================================================================
|
REM echo ================================================================
|
||||||
echo PROJECT = %PROJECT%
|
REM echo PROJECT = %PROJECT%
|
||||||
echo PV_BIN = %PV_BIN%
|
REM echo PV_BIN = %PV_BIN%
|
||||||
echo PV_CFG = %PV_CFG%
|
REM echo PV_CFG = %PV_CFG%
|
||||||
echo PV_LIB = %PV_LIB%
|
REM echo PV_LIB = %PV_LIB%
|
||||||
echo PV_DATA = %PV_DATA%
|
REM echo PV_DATA = %PV_DATA%
|
||||||
echo PV_RESULTS = %PV_RESULTS%
|
REM echo PV_RESULTS = %PV_RESULTS%
|
||||||
echo PV_LOG = %PV_LOG%
|
REM echo PV_LOG = %PV_LOG%
|
||||||
echo PV_EXAMPLES = %PV_EXAMPLES%
|
REM echo PV_EXAMPLES = %PV_EXAMPLES%
|
||||||
echo PV_CLIENT = %PV_CLIENT%
|
REM echo PV_CLIENT = %PV_CLIENT%
|
||||||
echo PV_SERVER = %PV_SERVER%
|
REM echo PV_SERVER = %PV_SERVER%
|
||||||
echo PV_SERVER_URL = %PV_SERVER_URL%
|
REM echo PV_SERVER_URL = %PV_SERVER_URL%
|
||||||
echo PV_CLIENT_URL = %PV_CLIENT_URL%
|
REM echo PV_CLIENT_URL = %PV_CLIENT_URL%
|
||||||
echo PYTHONPATH = %PYTHONPATH%
|
REM echo PYTHONPATH = %PYTHONPATH%
|
||||||
echo ================================================================
|
REM echo ================================================================
|
||||||
echo.
|
REM echo.
|
||||||
|
|
||||||
REM Optionally keep window open
|
REM Optionally keep window open
|
||||||
if "%1"=="--keep-open" pause
|
if "%1"=="--keep-open" pause
|
||||||
|
|||||||
4653
client/package-lock.json
generated
Normal file
4653
client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,18 @@
|
|||||||
import { Tldraw } from 'tldraw'
|
import { Tldraw } from 'tldraw'
|
||||||
import 'tldraw/tldraw.css'
|
import 'tldraw/tldraw.css'
|
||||||
|
import './shapes/shape-types' // Type-Augmentation für Custom Shapes
|
||||||
|
import { PufferShapeUtil } from './shapes/PufferShapeUtil'
|
||||||
|
import { KreiselShapeUtil } from './shapes/KreiselShapeUtil'
|
||||||
|
import { ShapePanel } from './components/ShapePanel'
|
||||||
|
|
||||||
|
const customShapeUtils = [PufferShapeUtil, KreiselShapeUtil]
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'fixed', inset: 0 }}>
|
<div style={{ position: 'fixed', inset: 0 }}>
|
||||||
<Tldraw />
|
<Tldraw shapeUtils={customShapeUtils}>
|
||||||
|
<ShapePanel />
|
||||||
|
</Tldraw>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
131
client/src/components/ShapePanel.tsx
Normal file
131
client/src/components/ShapePanel.tsx
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { useEditor } from 'tldraw'
|
||||||
|
import { useCallback } from 'react'
|
||||||
|
import { PUFFER_CONFIG, KREISEL_CONFIG } from '../shapes/shape-config'
|
||||||
|
|
||||||
|
/** Nächste freie Nummer für einen Shape-Typ ermitteln. */
|
||||||
|
function nextNumber(editor: ReturnType<typeof useEditor>, type: string): number {
|
||||||
|
const existing = editor.getCurrentPageShapes().filter((s) => s.type === type)
|
||||||
|
// Höchste bestehende Nummer finden
|
||||||
|
let max = 0
|
||||||
|
for (const s of existing) {
|
||||||
|
const lbl = (s.props as { label?: string }).label ?? ''
|
||||||
|
const m = lbl.match(/_(\d+)$/)
|
||||||
|
if (m) max = Math.max(max, parseInt(m[1], 10))
|
||||||
|
}
|
||||||
|
return max + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Panel links oben: Buttons zum Erstellen von Symbolen + Sync zum Server.
|
||||||
|
* Wird als Kind von <Tldraw> gerendert, daher ist useEditor() verfügbar.
|
||||||
|
*/
|
||||||
|
export function ShapePanel() {
|
||||||
|
const editor = useEditor()
|
||||||
|
|
||||||
|
const createPuffer = useCallback(() => {
|
||||||
|
const nr = nextNumber(editor, 'puffer')
|
||||||
|
const center = editor.getViewportScreenCenter()
|
||||||
|
const point = editor.screenToPage(center)
|
||||||
|
editor.createShape({
|
||||||
|
type: 'puffer',
|
||||||
|
x: point.x - PUFFER_CONFIG.defaultW / 2,
|
||||||
|
y: point.y - PUFFER_CONFIG.defaultH / 2,
|
||||||
|
props: {
|
||||||
|
w: PUFFER_CONFIG.defaultW,
|
||||||
|
h: PUFFER_CONFIG.defaultH,
|
||||||
|
label: `Puffer_${nr}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [editor])
|
||||||
|
|
||||||
|
const createKreisel = useCallback(() => {
|
||||||
|
const nr = nextNumber(editor, 'kreisel')
|
||||||
|
const center = editor.getViewportScreenCenter()
|
||||||
|
const point = editor.screenToPage(center)
|
||||||
|
const totalW = 2 * KREISEL_CONFIG.defaultRadius + KREISEL_CONFIG.defaultAbstand
|
||||||
|
const totalH = 2 * KREISEL_CONFIG.defaultRadius
|
||||||
|
editor.createShape({
|
||||||
|
type: 'kreisel',
|
||||||
|
x: point.x - totalW / 2,
|
||||||
|
y: point.y - totalH / 2,
|
||||||
|
props: {
|
||||||
|
abstand: KREISEL_CONFIG.defaultAbstand,
|
||||||
|
radius: KREISEL_CONFIG.defaultRadius,
|
||||||
|
label: `Kreisel_${nr}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}, [editor])
|
||||||
|
|
||||||
|
const syncToServer = useCallback(async () => {
|
||||||
|
const allShapes = editor.getCurrentPageShapes()
|
||||||
|
|
||||||
|
const puffer = allShapes
|
||||||
|
.filter((s) => s.type === 'puffer')
|
||||||
|
.map((s) => {
|
||||||
|
const p = s.props as { w: number; h: number }
|
||||||
|
return { id: s.id, x: s.x, y: s.y, w: p.w, h: p.h }
|
||||||
|
})
|
||||||
|
|
||||||
|
const kreisel = allShapes
|
||||||
|
.filter((s) => s.type === 'kreisel')
|
||||||
|
.map((s) => {
|
||||||
|
const p = s.props as { abstand: number; radius: number }
|
||||||
|
return { id: s.id, x: s.x, y: s.y, abstand: p.abstand, radius: p.radius }
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/shapes/update', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ puffer, kreisel }),
|
||||||
|
})
|
||||||
|
const data = await res.json()
|
||||||
|
console.log('Server response:', data)
|
||||||
|
alert(`Sync OK – ${data.received.puffer_count} Puffer, ${data.received.kreisel_count} Kreisel`)
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Sync error:', err)
|
||||||
|
alert('Sync fehlgeschlagen – siehe Konsole')
|
||||||
|
}
|
||||||
|
}, [editor])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={panelStyle}>
|
||||||
|
<div style={{ fontWeight: 'bold', fontSize: 13, marginBottom: 4 }}>
|
||||||
|
Symbole
|
||||||
|
</div>
|
||||||
|
<button onClick={createPuffer} style={btnStyle}>
|
||||||
|
▭ Puffer
|
||||||
|
</button>
|
||||||
|
<button onClick={createKreisel} style={btnStyle}>
|
||||||
|
◎ Kreisel
|
||||||
|
</button>
|
||||||
|
<hr style={{ width: '100%', margin: '4px 0' }} />
|
||||||
|
<button onClick={syncToServer} style={{ ...btnStyle, background: '#e0f0ff' }}>
|
||||||
|
↑ Sync Server
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const panelStyle: React.CSSProperties = {
|
||||||
|
position: 'absolute',
|
||||||
|
top: 60,
|
||||||
|
left: 10,
|
||||||
|
zIndex: 1000,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
gap: 6,
|
||||||
|
background: 'white',
|
||||||
|
padding: 10,
|
||||||
|
borderRadius: 8,
|
||||||
|
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
|
||||||
|
}
|
||||||
|
|
||||||
|
const btnStyle: React.CSSProperties = {
|
||||||
|
padding: '6px 12px',
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: '1px solid #ccc',
|
||||||
|
borderRadius: 4,
|
||||||
|
background: '#f8f8f8',
|
||||||
|
fontSize: 13,
|
||||||
|
}
|
||||||
142
client/src/shapes/KreiselShapeUtil.tsx
Normal file
142
client/src/shapes/KreiselShapeUtil.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import {
|
||||||
|
ShapeUtil,
|
||||||
|
TLHandle,
|
||||||
|
TLShapePartial,
|
||||||
|
Rectangle2d,
|
||||||
|
HTMLContainer,
|
||||||
|
IndexKey,
|
||||||
|
} from 'tldraw'
|
||||||
|
import { KreiselShape, kreiselShapeProps } from './shape-types'
|
||||||
|
import { KREISEL_CONFIG as CFG } from './shape-config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Kreisel: Zwei Kreise (links/rechts) mit tangentialen Verbindungslinien.
|
||||||
|
* Handles im Zentrum beider Kreise zum Ändern des Abstands.
|
||||||
|
*/
|
||||||
|
export class KreiselShapeUtil extends ShapeUtil<KreiselShape> {
|
||||||
|
static override type = 'kreisel' as const
|
||||||
|
static override props = kreiselShapeProps
|
||||||
|
|
||||||
|
getDefaultProps(): KreiselShape['props'] {
|
||||||
|
return { abstand: CFG.defaultAbstand, radius: CFG.defaultRadius, label: 'Kreisel' }
|
||||||
|
}
|
||||||
|
|
||||||
|
getGeometry(shape: KreiselShape) {
|
||||||
|
const { abstand, radius } = shape.props
|
||||||
|
return new Rectangle2d({
|
||||||
|
width: 2 * radius + abstand,
|
||||||
|
height: 2 * radius,
|
||||||
|
isFilled: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override canResize() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override onRotate(initial: KreiselShape, current: KreiselShape): TLShapePartial<KreiselShape> {
|
||||||
|
const HALF_PI = Math.PI / 2
|
||||||
|
const snapped = Math.round(current.rotation / HALF_PI) * HALF_PI
|
||||||
|
return { id: current.id, type: 'kreisel', x: initial.x, y: initial.y, rotation: snapped }
|
||||||
|
}
|
||||||
|
|
||||||
|
override getHandles(shape: KreiselShape): TLHandle[] {
|
||||||
|
const { abstand, radius } = shape.props
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'left',
|
||||||
|
type: 'vertex',
|
||||||
|
x: radius,
|
||||||
|
y: radius,
|
||||||
|
index: 'a1' as IndexKey,
|
||||||
|
canSnap: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'right',
|
||||||
|
type: 'vertex',
|
||||||
|
x: radius + abstand,
|
||||||
|
y: radius,
|
||||||
|
index: 'a2' as IndexKey,
|
||||||
|
canSnap: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override onHandleDrag(
|
||||||
|
shape: KreiselShape,
|
||||||
|
{ handle }: { handle: TLHandle }
|
||||||
|
): TLShapePartial<KreiselShape> | void {
|
||||||
|
const r = shape.props.radius
|
||||||
|
|
||||||
|
if (handle.id === 'right') {
|
||||||
|
const newAbstand = Math.min(CFG.maxAbstand, Math.max(CFG.minAbstand, handle.x - r))
|
||||||
|
return {
|
||||||
|
id: shape.id,
|
||||||
|
type: 'kreisel',
|
||||||
|
props: { ...shape.props, abstand: newAbstand },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle.id === 'left') {
|
||||||
|
const oldRightX = r + shape.props.abstand
|
||||||
|
const raw = oldRightX - handle.x
|
||||||
|
const newAbstand = Math.min(CFG.maxAbstand, Math.max(CFG.minAbstand, raw))
|
||||||
|
const dx = shape.props.abstand - newAbstand
|
||||||
|
return {
|
||||||
|
id: shape.id,
|
||||||
|
type: 'kreisel',
|
||||||
|
x: shape.x + dx,
|
||||||
|
props: { ...shape.props, abstand: newAbstand },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component(shape: KreiselShape) {
|
||||||
|
const { abstand, radius: r } = shape.props
|
||||||
|
const totalW = 2 * r + abstand
|
||||||
|
const totalH = 2 * r
|
||||||
|
|
||||||
|
return (
|
||||||
|
<HTMLContainer>
|
||||||
|
<svg
|
||||||
|
width={totalW}
|
||||||
|
height={totalH}
|
||||||
|
style={{ overflow: 'visible' }}
|
||||||
|
>
|
||||||
|
{/* Linker Kreis */}
|
||||||
|
<circle cx={r} cy={r} r={r} fill="none" stroke="black" strokeWidth={2} />
|
||||||
|
{/* Rechter Kreis */}
|
||||||
|
<circle cx={r + abstand} cy={r} r={r} fill="none" stroke="black" strokeWidth={2} />
|
||||||
|
{/* Obere Tangentiallinie */}
|
||||||
|
<line x1={r} y1={0} x2={r + abstand} y2={0} stroke="black" strokeWidth={2} />
|
||||||
|
{/* Untere Tangentiallinie */}
|
||||||
|
<line x1={r} y1={totalH} x2={r + abstand} y2={totalH} stroke="black" strokeWidth={2} />
|
||||||
|
{/* Label */}
|
||||||
|
<text
|
||||||
|
x={totalW / 2}
|
||||||
|
y={r}
|
||||||
|
textAnchor="middle"
|
||||||
|
dominantBaseline="central"
|
||||||
|
fontSize={36}
|
||||||
|
fill="#666"
|
||||||
|
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
||||||
|
>
|
||||||
|
{shape.props.label}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</HTMLContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator(shape: KreiselShape) {
|
||||||
|
const { abstand, radius: r } = shape.props
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
width={2 * r + abstand}
|
||||||
|
height={2 * r}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
127
client/src/shapes/PufferShapeUtil.tsx
Normal file
127
client/src/shapes/PufferShapeUtil.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import {
|
||||||
|
ShapeUtil,
|
||||||
|
TLHandle,
|
||||||
|
TLShapePartial,
|
||||||
|
Rectangle2d,
|
||||||
|
HTMLContainer,
|
||||||
|
IndexKey,
|
||||||
|
} from 'tldraw'
|
||||||
|
import { PufferShape, pufferShapeProps } from './shape-types'
|
||||||
|
import { PUFFER_CONFIG as CFG } from './shape-config'
|
||||||
|
|
||||||
|
/** Puffer: Rechteck mit Handles oben-mittig (Höhe) und rechts-mittig (Breite) */
|
||||||
|
export class PufferShapeUtil extends ShapeUtil<PufferShape> {
|
||||||
|
static override type = 'puffer' as const
|
||||||
|
static override props = pufferShapeProps
|
||||||
|
|
||||||
|
getDefaultProps(): PufferShape['props'] {
|
||||||
|
return { w: CFG.defaultW, h: CFG.defaultH, label: 'Puffer' }
|
||||||
|
}
|
||||||
|
|
||||||
|
getGeometry(shape: PufferShape) {
|
||||||
|
return new Rectangle2d({
|
||||||
|
width: shape.props.w,
|
||||||
|
height: shape.props.h,
|
||||||
|
isFilled: false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
override canResize() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
override onRotate(initial: PufferShape, current: PufferShape): TLShapePartial<PufferShape> {
|
||||||
|
const HALF_PI = Math.PI / 2
|
||||||
|
const snapped = Math.round(current.rotation / HALF_PI) * HALF_PI
|
||||||
|
return { id: current.id, type: 'puffer', x: initial.x, y: initial.y, rotation: snapped }
|
||||||
|
}
|
||||||
|
|
||||||
|
override getHandles(shape: PufferShape): TLHandle[] {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
id: 'top',
|
||||||
|
type: 'vertex',
|
||||||
|
x: shape.props.w / 2,
|
||||||
|
y: 0,
|
||||||
|
index: 'a1' as IndexKey,
|
||||||
|
canSnap: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'right',
|
||||||
|
type: 'vertex',
|
||||||
|
x: shape.props.w,
|
||||||
|
y: shape.props.h / 2,
|
||||||
|
index: 'a2' as IndexKey,
|
||||||
|
canSnap: true,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
override onHandleDrag(
|
||||||
|
shape: PufferShape,
|
||||||
|
{ handle }: { handle: TLHandle }
|
||||||
|
): TLShapePartial<PufferShape> | void {
|
||||||
|
if (handle.id === 'top') {
|
||||||
|
const newH = Math.min(CFG.maxH, Math.max(CFG.minH, shape.props.h - handle.y))
|
||||||
|
return {
|
||||||
|
id: shape.id,
|
||||||
|
type: 'puffer',
|
||||||
|
y: shape.y + (shape.props.h - newH),
|
||||||
|
props: { ...shape.props, h: newH },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (handle.id === 'right') {
|
||||||
|
const newW = Math.min(CFG.maxW, Math.max(CFG.minW, handle.x))
|
||||||
|
return {
|
||||||
|
id: shape.id,
|
||||||
|
type: 'puffer',
|
||||||
|
props: { ...shape.props, w: newW },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
component(shape: PufferShape) {
|
||||||
|
const { w, h } = shape.props
|
||||||
|
return (
|
||||||
|
<HTMLContainer>
|
||||||
|
<svg
|
||||||
|
width={w}
|
||||||
|
height={h}
|
||||||
|
style={{ overflow: 'visible' }}
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
width={w}
|
||||||
|
height={h}
|
||||||
|
fill="none"
|
||||||
|
stroke="#2563eb"
|
||||||
|
strokeWidth={2}
|
||||||
|
/>
|
||||||
|
<text
|
||||||
|
x={w / 2}
|
||||||
|
y={h / 2}
|
||||||
|
textAnchor="middle"
|
||||||
|
dominantBaseline="central"
|
||||||
|
fontSize={36}
|
||||||
|
fill="#666"
|
||||||
|
style={{ pointerEvents: 'none', userSelect: 'none' }}
|
||||||
|
>
|
||||||
|
{shape.props.label}
|
||||||
|
</text>
|
||||||
|
</svg>
|
||||||
|
</HTMLContainer>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
indicator(shape: PufferShape) {
|
||||||
|
return (
|
||||||
|
<rect
|
||||||
|
x={0}
|
||||||
|
y={0}
|
||||||
|
width={shape.props.w}
|
||||||
|
height={shape.props.h}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
26
client/src/shapes/shape-config.ts
Normal file
26
client/src/shapes/shape-config.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
/**
|
||||||
|
* Zentrale Konfiguration für alle Custom Shapes.
|
||||||
|
* Defaults, Minima und Maxima an einer Stelle gepflegt.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const PUFFER_CONFIG = {
|
||||||
|
// Defaults
|
||||||
|
defaultW: 120,
|
||||||
|
defaultH: 60,
|
||||||
|
// Minima
|
||||||
|
minW: 20,
|
||||||
|
minH: 20,
|
||||||
|
// Maxima
|
||||||
|
maxW: 500,
|
||||||
|
maxH: 300,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const KREISEL_CONFIG = {
|
||||||
|
// Defaults
|
||||||
|
defaultAbstand: 200,
|
||||||
|
defaultRadius: 80,
|
||||||
|
// Minima
|
||||||
|
minAbstand: 20,
|
||||||
|
// Maxima
|
||||||
|
maxAbstand: 4000,
|
||||||
|
}
|
||||||
39
client/src/shapes/shape-types.ts
Normal file
39
client/src/shapes/shape-types.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { T, TLBaseShape } from 'tldraw'
|
||||||
|
|
||||||
|
// --- Puffer ---
|
||||||
|
export type PufferShapeProps = {
|
||||||
|
w: number
|
||||||
|
h: number
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PufferShape = TLBaseShape<'puffer', PufferShapeProps>
|
||||||
|
|
||||||
|
export const pufferShapeProps = {
|
||||||
|
w: T.number,
|
||||||
|
h: T.number,
|
||||||
|
label: T.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Kreisel ---
|
||||||
|
export type KreiselShapeProps = {
|
||||||
|
abstand: number
|
||||||
|
radius: number
|
||||||
|
label: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type KreiselShape = TLBaseShape<'kreisel', KreiselShapeProps>
|
||||||
|
|
||||||
|
export const kreiselShapeProps = {
|
||||||
|
abstand: T.number,
|
||||||
|
radius: T.number,
|
||||||
|
label: T.string,
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- tldraw Type-Augmentation: Custom Shapes registrieren ---
|
||||||
|
declare module '@tldraw/tlschema' {
|
||||||
|
interface TLGlobalShapePropsMap {
|
||||||
|
puffer: PufferShapeProps
|
||||||
|
kreisel: KreiselShapeProps
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@ PlantPlan FastAPI Backend
|
|||||||
- Symbolbibliothek ausliefern
|
- Symbolbibliothek ausliefern
|
||||||
- Koordinaten normalisieren
|
- Koordinaten normalisieren
|
||||||
- JSON/SVG Export
|
- JSON/SVG Export
|
||||||
|
- Shape-Änderungen empfangen (Puffer + Kreisel)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -24,14 +25,42 @@ app.add_middleware(
|
|||||||
CATALOG_DIR = Path(__file__).parent / "catalog"
|
CATALOG_DIR = Path(__file__).parent / "catalog"
|
||||||
|
|
||||||
|
|
||||||
# --- Models ---
|
# ---------------------------------------------------------------------------
|
||||||
|
# Pydantic Models
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
class SketchPayload(BaseModel):
|
class SketchPayload(BaseModel):
|
||||||
shapes: list[dict]
|
shapes: list[dict]
|
||||||
grid_size: int = 10
|
grid_size: int = 10
|
||||||
|
|
||||||
|
|
||||||
# --- Endpoints ---
|
class PufferUpdate(BaseModel):
|
||||||
|
"""Puffer-Symbol: Rechteck mit Breite und Höhe."""
|
||||||
|
id: str
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
w: float # Breite
|
||||||
|
h: float # Höhe
|
||||||
|
|
||||||
|
|
||||||
|
class KreiselUpdate(BaseModel):
|
||||||
|
"""Kreisel-Symbol: Zwei Kreise mit Abstand und Radius."""
|
||||||
|
id: str
|
||||||
|
x: float
|
||||||
|
y: float
|
||||||
|
abstand: float # Abstand zwischen den Kreismittelpunkten
|
||||||
|
radius: float # Radius beider Kreise
|
||||||
|
|
||||||
|
|
||||||
|
class ShapeUpdatePayload(BaseModel):
|
||||||
|
"""Payload für Shape-Änderungen vom Client."""
|
||||||
|
puffer: list[PufferUpdate] = []
|
||||||
|
kreisel: list[KreiselUpdate] = []
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Endpoints
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
@app.get("/symbols")
|
@app.get("/symbols")
|
||||||
async def get_symbols():
|
async def get_symbols():
|
||||||
@@ -64,3 +93,42 @@ async def export_sketch(payload: SketchPayload):
|
|||||||
"shapes": payload.shapes,
|
"shapes": payload.shapes,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@app.post("/shapes/update")
|
||||||
|
async def update_shapes(payload: ShapeUpdatePayload):
|
||||||
|
"""
|
||||||
|
Shape-Änderungen vom Client empfangen.
|
||||||
|
|
||||||
|
Gibt für jeden Puffer Breite/Höhe und für jeden Kreisel
|
||||||
|
Abstand/Radius + berechnete Gesamtlänge zurück.
|
||||||
|
"""
|
||||||
|
result = {
|
||||||
|
"received": {
|
||||||
|
"puffer_count": len(payload.puffer),
|
||||||
|
"kreisel_count": len(payload.kreisel),
|
||||||
|
},
|
||||||
|
"puffer": [],
|
||||||
|
"kreisel": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
for p in payload.puffer:
|
||||||
|
result["puffer"].append({
|
||||||
|
"id": p.id,
|
||||||
|
"x": p.x,
|
||||||
|
"y": p.y,
|
||||||
|
"breite": p.w,
|
||||||
|
"hoehe": p.h,
|
||||||
|
})
|
||||||
|
|
||||||
|
for k in payload.kreisel:
|
||||||
|
result["kreisel"].append({
|
||||||
|
"id": k.id,
|
||||||
|
"x": k.x,
|
||||||
|
"y": k.y,
|
||||||
|
"abstand": k.abstand,
|
||||||
|
"radius": k.radius,
|
||||||
|
"gesamtlaenge": 2 * k.radius + k.abstand,
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
|
|||||||
Reference in New Issue
Block a user