From 64ebafae8f160f4140419887147a253942ed256c Mon Sep 17 00:00:00 2001 From: Michael Stangl Date: Tue, 28 Apr 2026 14:57:40 +0200 Subject: [PATCH] =?UTF-8?q?einfacher=20F=C3=B6rderer=20und=20Polylinie=20h?= =?UTF-8?q?inz?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- client/src/App.tsx | 15 +- client/src/bindings/PolylinieBindingUtil.ts | 95 +++++++++ client/src/components/ShapePanel.tsx | 41 +++- client/src/polylinie.css | 5 + client/src/shapes/FoerdererShapeUtil.tsx | 205 +++++++++++++++++++ client/src/shapes/PolylinieShapeUtil.tsx | 157 +++++++++++++++ client/src/shapes/shape-config.ts | 16 ++ client/src/shapes/shape-types.ts | 57 +++++- client/src/tools/PolylinieDrawing.ts | 212 ++++++++++++++++++++ client/src/tools/PolylinieIdle.ts | 30 +++ client/src/tools/PolylinieTool.ts | 12 ++ client/src/utils/angle-snap.ts | 56 ++++++ 12 files changed, 895 insertions(+), 6 deletions(-) create mode 100644 client/src/bindings/PolylinieBindingUtil.ts create mode 100644 client/src/polylinie.css create mode 100644 client/src/shapes/FoerdererShapeUtil.tsx create mode 100644 client/src/shapes/PolylinieShapeUtil.tsx create mode 100644 client/src/tools/PolylinieDrawing.ts create mode 100644 client/src/tools/PolylinieIdle.ts create mode 100644 client/src/tools/PolylinieTool.ts create mode 100644 client/src/utils/angle-snap.ts 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() { + +