diff --git a/client/src/App.tsx b/client/src/App.tsx
index 80b282c..ca5ecbe 100644
--- a/client/src/App.tsx
+++ b/client/src/App.tsx
@@ -1,16 +1,27 @@
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
+import './polylinie.css'
import './shapes/shape-types' // Type-Augmentation für Custom Shapes
import { PufferShapeUtil } from './shapes/PufferShapeUtil'
import { KreiselShapeUtil } from './shapes/KreiselShapeUtil'
+import { FoerdererShapeUtil } from './shapes/FoerdererShapeUtil'
+import { PolylinieShapeUtil } from './shapes/PolylinieShapeUtil'
+import { PolylinieTool } from './tools/PolylinieTool'
+import { PolylinieBindingUtil } from './bindings/PolylinieBindingUtil'
import { ShapePanel } from './components/ShapePanel'
-const customShapeUtils = [PufferShapeUtil, KreiselShapeUtil]
+const customShapeUtils = [PufferShapeUtil, KreiselShapeUtil, FoerdererShapeUtil, PolylinieShapeUtil]
+const customTools = [PolylinieTool]
+const customBindingUtils = [PolylinieBindingUtil]
export default function App() {
return (
-
+
diff --git a/client/src/bindings/PolylinieBindingUtil.ts b/client/src/bindings/PolylinieBindingUtil.ts
new file mode 100644
index 0000000..40fe521
--- /dev/null
+++ b/client/src/bindings/PolylinieBindingUtil.ts
@@ -0,0 +1,95 @@
+import {
+ BindingUtil,
+ TLBaseBinding,
+ BindingOnShapeChangeOptions,
+ BindingOnShapeDeleteOptions,
+ T,
+ sortByIndex,
+} from 'tldraw'
+import { PolylinieShape, PolyliniePoint, PolylinieConnectionProps } from '../shapes/shape-types'
+
+export type PolylinieConnectionBinding = TLBaseBinding<
+ 'polylinie-connection',
+ PolylinieConnectionProps
+>
+
+export class PolylinieBindingUtil extends BindingUtil {
+ static override type = 'polylinie-connection' as const
+
+ static override props = {
+ terminal: T.string,
+ normalizedAnchor: T.object({
+ x: T.number,
+ y: T.number,
+ }),
+ }
+
+ override getDefaultProps(): PolylinieConnectionProps {
+ return {
+ terminal: 'end',
+ normalizedAnchor: { x: 0, y: 0 },
+ }
+ }
+
+ override onAfterChangeToShape(options: BindingOnShapeChangeOptions) {
+ const { binding, shapeAfter } = options
+
+ const polyShape = this.editor.getShape(binding.fromId)
+ if (!polyShape) return
+
+ const targetX = shapeAfter.x
+ const targetY = shapeAfter.y
+
+ const anchor = binding.props.normalizedAnchor
+ const termPt = this._getTerminalPoint(polyShape, binding.props.terminal)
+ const dx = targetX + anchor.x - (polyShape.x + termPt.x)
+ const dy = targetY + anchor.y - (polyShape.y + termPt.y)
+
+ if (Math.abs(dx) < 0.01 && Math.abs(dy) < 0.01) return
+
+ const points = { ...polyShape.props.points }
+ const sorted = Object.values(points).sort(sortByIndex)
+
+ if (sorted.length < 2) return
+
+ const isEnd = binding.props.terminal === 'end'
+ const terminalIdx = isEnd ? sorted.length - 1 : 0
+ const terminal = sorted[terminalIdx]
+
+ points[terminal.id] = {
+ ...terminal,
+ x: terminal.x + dx,
+ y: terminal.y + dy,
+ }
+
+ if (sorted.length >= 3) {
+ const adjIdx = isEnd ? sorted.length - 2 : 1
+ const adj = sorted[adjIdx]
+ points[adj.id] = {
+ ...adj,
+ x: adj.x + dx * 0.5,
+ y: adj.y + dy * 0.5,
+ }
+ }
+
+ this.editor.updateShape({
+ id: polyShape.id,
+ type: 'polylinie',
+ props: { points },
+ })
+ }
+
+ override onBeforeDeleteToShape(_options: BindingOnShapeDeleteOptions) {
+ // Binding entfernen, Polylinie behalten
+ }
+
+ private _getTerminalPoint(
+ shape: PolylinieShape,
+ terminal: string
+ ): { x: number; y: number } {
+ const sorted = Object.values(shape.props.points).sort(sortByIndex)
+ if (sorted.length === 0) return { x: 0, y: 0 }
+ if (terminal === 'start') return sorted[0]
+ return sorted[sorted.length - 1]
+ }
+}
diff --git a/client/src/components/ShapePanel.tsx b/client/src/components/ShapePanel.tsx
index a4bd285..f01024b 100644
--- a/client/src/components/ShapePanel.tsx
+++ b/client/src/components/ShapePanel.tsx
@@ -1,6 +1,6 @@
-import { useEditor } from 'tldraw'
+import { useEditor, useValue } from 'tldraw'
import { useCallback } from 'react'
-import { PUFFER_CONFIG, KREISEL_CONFIG } from '../shapes/shape-config'
+import { PUFFER_CONFIG, KREISEL_CONFIG, FOERDERER_CONFIG } from '../shapes/shape-config'
/** Nächste freie Nummer für einen Shape-Typ ermitteln. */
function nextNumber(editor: ReturnType, type: string): number {
@@ -56,6 +56,30 @@ export function ShapePanel() {
})
}, [editor])
+ const createFoerderer = useCallback(() => {
+ const nr = nextNumber(editor, 'foerderer')
+ const center = editor.getViewportScreenCenter()
+ const point = editor.screenToPage(center)
+ editor.createShape({
+ type: 'foerderer',
+ x: point.x - FOERDERER_CONFIG.defaultEndX / 2,
+ y: point.y,
+ props: {
+ endX: FOERDERER_CONFIG.defaultEndX,
+ endY: FOERDERER_CONFIG.defaultEndY,
+ spacing: FOERDERER_CONFIG.defaultSpacing,
+ label: `Förderer_${nr}`,
+ },
+ })
+ }, [editor])
+
+ const activatePolylinie = useCallback(() => {
+ editor.setCurrentTool('polylinie')
+ }, [editor])
+
+ const currentToolId = useValue('current tool', () => editor.getCurrentToolId(), [editor])
+ const isPolylinieActive = currentToolId === 'polylinie'
+
const syncToServer = useCallback(async () => {
const allShapes = editor.getCurrentPageShapes()
@@ -99,6 +123,19 @@ export function ShapePanel() {
+
+