erste Fassung von Client und Server rein

This commit is contained in:
mistangl
2026-04-16 13:34:05 +02:00
parent f44949f0af
commit 5b38af78f0
19 changed files with 589 additions and 0 deletions

4
.gitignore vendored
View File

@@ -87,3 +87,7 @@ ipython_config.py
/work /work
/log /log
/results /results
# Node.js (client)
node_modules/
client/dist/

74
bin/client.bat Normal file
View File

@@ -0,0 +1,74 @@
@echo off
REM ================================================================
REM PLANTPLAN - Client (Vite Dev-Server) verwalten
REM Verwendung: client.bat [start|stop|reload|status]
REM ================================================================
call "%~dp0setenv.bat"
if "%1"=="" goto usage
if /i "%1"=="start" goto start
if /i "%1"=="stop" goto stop
if /i "%1"=="reload" goto reload
if /i "%1"=="status" goto status
goto usage
:start
echo Starting PlantPlan Client (Vite) ...
REM Pruefen ob Client bereits laeuft
tasklist /FI "WINDOWTITLE eq PlantPlan-Client" 2>nul | find /i "cmd.exe" >nul
if not errorlevel 1 (
echo Client laeuft bereits!
goto end
)
REM Pruefen ob node_modules existiert
if not exist "%PV_CLIENT%\node_modules" (
echo FEHLER: node_modules nicht gefunden! Bitte zuerst install_npm.bat ausfuehren.
goto end
)
start "PlantPlan-Client" cmd /k "cd /d "%PV_CLIENT%" && npm run dev"
echo Client gestartet auf %PV_CLIENT_URL%
goto end
:stop
echo Stopping PlantPlan Client ...
taskkill /FI "WINDOWTITLE eq PlantPlan-Client" /T /F >nul 2>&1
if errorlevel 1 (
echo Client war nicht gestartet.
) else (
echo Client gestoppt.
)
goto end
:reload
echo Reloading PlantPlan Client ...
call :stop
timeout /t 2 /nobreak >nul
call :start
goto end
:status
tasklist /FI "WINDOWTITLE eq PlantPlan-Client" 2>nul | find /i "cmd.exe" >nul
if not errorlevel 1 (
echo Client: LAEUFT
) else (
echo Client: GESTOPPT
)
goto end
:usage
echo.
echo Verwendung: client.bat [start^|stop^|reload^|status]
echo.
echo start - Client starten (Vite Dev-Server)
echo stop - Client stoppen
echo reload - Client neu starten
echo status - Pruefen ob Client laeuft
echo.
:end

18
bin/install_npm.bat Normal file
View File

@@ -0,0 +1,18 @@
@echo off
REM ================================================================
REM PLANTPLAN - Node.js Packages fuer Client installieren
REM ================================================================
REM Fuehrt npm install im client/ Verzeichnis aus
REM ================================================================
call "%~dp0setenv.bat"
if not exist "%PV_CLIENT%\node_modules" (
echo Installiere Node.js Packages fuer Client...
cd /d "%PV_CLIENT%"
npm install
echo Erfolgreich.
) else (
echo Node.js Packages bereits installiert!
echo Zum Aktualisieren: cd client ^& npm install
)

19
bin/install_npm.sh Normal file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# ================================================================
# PLANTPLAN - Node.js Packages fuer Client installieren
# ================================================================
# Fuehrt npm install im client/ Verzeichnis aus
# ================================================================
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$SCRIPT_DIR/setenv.sh"
if [ ! -d "$PV_CLIENT/node_modules" ]; then
echo "Installiere Node.js Packages fuer Client..."
cd "$PV_CLIENT"
npm install
echo "Erfolgreich."
else
echo "Node.js Packages bereits installiert!"
echo "Zum Aktualisieren: cd client && npm install"
fi

74
bin/server.bat Normal file
View File

@@ -0,0 +1,74 @@
@echo off
REM ================================================================
REM PLANTPLAN - Server (FastAPI/Uvicorn) verwalten
REM Verwendung: server.bat [start|stop|reload|status]
REM ================================================================
call "%~dp0setenv.bat"
if "%1"=="" goto usage
if /i "%1"=="start" goto start
if /i "%1"=="stop" goto stop
if /i "%1"=="reload" goto reload
if /i "%1"=="status" goto status
goto usage
:start
echo Starting PlantPlan Server (uvicorn) ...
REM Pruefen ob Server bereits laeuft
tasklist /FI "WINDOWTITLE eq PlantPlan-Server" 2>nul | find /i "cmd.exe" >nul
if not errorlevel 1 (
echo Server laeuft bereits!
goto end
)
REM Pruefen ob venv existiert
if not exist "%PROJECT%\.venv\Scripts\activate.bat" (
echo FEHLER: .venv nicht gefunden! Bitte zuerst install_py.bat ausfuehren.
goto end
)
start "PlantPlan-Server" cmd /k "cd /d "%PV_SERVER%" && "%PROJECT%\.venv\Scripts\activate.bat" && uvicorn main:app --reload --host %PV_SERVER_HOST% --port %PV_SERVER_PORT%"
echo Server gestartet auf %PV_SERVER_URL%
goto end
:stop
echo Stopping PlantPlan Server ...
taskkill /FI "WINDOWTITLE eq PlantPlan-Server" /T /F >nul 2>&1
if errorlevel 1 (
echo Server war nicht gestartet.
) else (
echo Server gestoppt.
)
goto end
:reload
echo Reloading PlantPlan Server ...
call :stop
timeout /t 2 /nobreak >nul
call :start
goto end
:status
tasklist /FI "WINDOWTITLE eq PlantPlan-Server" 2>nul | find /i "cmd.exe" >nul
if not errorlevel 1 (
echo Server: LAEUFT
) else (
echo Server: GESTOPPT
)
goto end
:usage
echo.
echo Verwendung: server.bat [start^|stop^|reload^|status]
echo.
echo start - Server starten (uvicorn mit --reload)
echo stop - Server stoppen
echo reload - Server neu starten
echo status - Pruefen ob Server laeuft
echo.
:end

View File

@@ -20,6 +20,15 @@ set "PV_LOG=%PROJECT%\log"
set "PV_TESTS=%PROJECT%\tests" set "PV_TESTS=%PROJECT%\tests"
set "PV_RESULTS=%PROJECT%\results" set "PV_RESULTS=%PROJECT%\results"
set "PV_EXAMPLES=%PROJECT%\examples" set "PV_EXAMPLES=%PROJECT%\examples"
set "PV_CLIENT=%PROJECT%\client"
set "PV_SERVER=%PROJECT%\server"
REM Netzwerk-Konfiguration
set "PV_SERVER_HOST=127.0.0.1"
set "PV_SERVER_PORT=8000"
set "PV_CLIENT_PORT=5173"
set "PV_SERVER_URL=http://%PV_SERVER_HOST%:%PV_SERVER_PORT%"
set "PV_CLIENT_URL=http://localhost:%PV_CLIENT_PORT%"
REM Python-Pfad erweitern (nur wenn noch nicht vorhanden) REM Python-Pfad erweitern (nur wenn noch nicht vorhanden)
echo %PYTHONPATH% | find /i "%PV_LIB%" >nul echo %PYTHONPATH% | find /i "%PV_LIB%" >nul
@@ -35,6 +44,8 @@ if not exist "%PV_DATA%" mkdir "%PV_DATA%"
if not exist "%PV_LOG%" mkdir "%PV_LOG%" if not exist "%PV_LOG%" mkdir "%PV_LOG%"
if not exist "%PV_RESULTS%" mkdir "%PV_RESULTS%" if not exist "%PV_RESULTS%" mkdir "%PV_RESULTS%"
if not exist "%PV_EXAMPLES%" mkdir "%PV_EXAMPLES%" if not exist "%PV_EXAMPLES%" mkdir "%PV_EXAMPLES%"
if not exist "%PV_CLIENT%" mkdir "%PV_CLIENT%"
if not exist "%PV_SERVER%" mkdir "%PV_SERVER%"
REM Umgebungsvariablen anzeigen REM Umgebungsvariablen anzeigen
echo. echo.
@@ -47,6 +58,10 @@ echo PV_DATA = %PV_DATA%
echo PV_RESULTS = %PV_RESULTS% echo PV_RESULTS = %PV_RESULTS%
echo PV_LOG = %PV_LOG% echo PV_LOG = %PV_LOG%
echo PV_EXAMPLES = %PV_EXAMPLES% echo PV_EXAMPLES = %PV_EXAMPLES%
echo PV_CLIENT = %PV_CLIENT%
echo PV_SERVER = %PV_SERVER%
echo PV_SERVER_URL = %PV_SERVER_URL%
echo PV_CLIENT_URL = %PV_CLIENT_URL%
echo PYTHONPATH = %PYTHONPATH% echo PYTHONPATH = %PYTHONPATH%
echo ================================================================ echo ================================================================
echo. echo.

View File

@@ -19,8 +19,17 @@ export PV_CFG="$PROJECT/cfg"
export PV_LOG="$PROJECT/log" export PV_LOG="$PROJECT/log"
export PV_TESTS="$PROJECT/tests" export PV_TESTS="$PROJECT/tests"
export PV_RESULTS="$PROJECT/results" export PV_RESULTS="$PROJECT/results"
export PV_CLIENT="$PROJECT/client"
export PV_SERVER="$PROJECT/server"
export PV_EXAMPLES="$PROJECT/examples" export PV_EXAMPLES="$PROJECT/examples"
# Netzwerk-Konfiguration
export PV_SERVER_HOST="127.0.0.1"
export PV_SERVER_PORT="8000"
export PV_CLIENT_PORT="5173"
export PV_SERVER_URL="http://$PV_SERVER_HOST:$PV_SERVER_PORT"
export PV_CLIENT_URL="http://localhost:$PV_CLIENT_PORT"
# Python-Pfad erweitern (nur wenn noch nicht vorhanden) # Python-Pfad erweitern (nur wenn noch nicht vorhanden)
if [[ ":$PYTHONPATH:" != *":$PV_LIB:"* ]]; then if [[ ":$PYTHONPATH:" != *":$PV_LIB:"* ]]; then
export PYTHONPATH="$PV_LIB:$PYTHONPATH" export PYTHONPATH="$PV_LIB:$PYTHONPATH"
@@ -40,7 +49,11 @@ echo "PV_LIB = $PV_LIB"
echo "PV_DATA = $PV_DATA" echo "PV_DATA = $PV_DATA"
echo "PV_RESULTS = $PV_RESULTS" echo "PV_RESULTS = $PV_RESULTS"
echo "PV_LOG = $PV_LOG" echo "PV_LOG = $PV_LOG"
echo "PV_CLIENT = $PV_CLIENT"
echo "PV_SERVER = $PV_SERVER"
echo "PV_EXAMPLES = $PV_EXAMPLES" echo "PV_EXAMPLES = $PV_EXAMPLES"
echo "PV_SERVER_URL = $PV_SERVER_URL"
echo "PV_CLIENT_URL = $PV_CLIENT_URL"
echo "PYTHONPATH = $PYTHONPATH" echo "PYTHONPATH = $PYTHONPATH"
echo "================================================================" echo "================================================================"
echo "" echo ""

21
bin/start.bat Normal file
View File

@@ -0,0 +1,21 @@
@echo off
REM ================================================================
REM PLANTPLAN - Server und Client gemeinsam starten
REM ================================================================
call "%~dp0setenv.bat"
echo ================================================================
echo PlantPlan - Starte Server und Client ...
echo ================================================================
echo.
call "%~dp0server.bat" start
call "%~dp0client.bat" start
echo.
echo ================================================================
echo Server: %PV_SERVER_URL%
echo Client: %PV_CLIENT_URL%
echo ================================================================

17
bin/stop.bat Normal file
View File

@@ -0,0 +1,17 @@
@echo off
REM ================================================================
REM PLANTPLAN - Server und Client gemeinsam stoppen
REM ================================================================
echo ================================================================
echo PlantPlan - Stoppe Server und Client ...
echo ================================================================
echo.
call "%~dp0server.bat" stop
call "%~dp0client.bat" stop
echo.
echo Alle Prozesse gestoppt.
echo ================================================================

12
client/index.html Normal file
View File

@@ -0,0 +1,12 @@
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>PlantPlan</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

23
client/package.json Normal file
View File

@@ -0,0 +1,23 @@
{
"name": "plantplan-client",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tldraw": "^2.9.0"
},
"devDependencies": {
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"typescript": "^5.7.0",
"vite": "^6.0.0"
}
}

10
client/src/App.tsx Normal file
View File

@@ -0,0 +1,10 @@
import { Tldraw } from 'tldraw'
import 'tldraw/tldraw.css'
export default function App() {
return (
<div style={{ position: 'fixed', inset: 0 }}>
<Tldraw />
</div>
)
}

9
client/src/main.tsx Normal file
View File

@@ -0,0 +1,9 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import App from './App'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)

17
client/tsconfig.json Normal file
View File

@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"isolatedModules": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
"strict": true
},
"include": ["src"]
}

16
client/vite.config.ts Normal file
View File

@@ -0,0 +1,16 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 5173,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, ''),
},
},
},
})

152
doc/architektur.md Normal file
View File

@@ -0,0 +1,152 @@
# PlantPlan Architektur
**Datum:** 16. April 2026
---
## 1. Überblick
PlantPlan ist eine hybride Webanwendung für technische Gleisplan-Skizzen. Das iPad dient als Eingabegerät (PWA im Safari), ein Python-Backend übernimmt Bibliotheksverwaltung, Normalisierung und Export.
```
┌─────────────────────────┐ ┌─────────────────────────┐
│ client/ │ │ server/ │
│ (tldraw + React + Vite) │ HTTP │ (FastAPI + Python) │
│ │────────▶│ │
│ - Canvas & Zeichnung │ JSON │ - Symbolbibliothek │
│ - Custom Shapes │◀────────│ - Normalisierung │
│ - Snap-to-Grid │ │ - JSON/SVG Export │
└─────────────────────────┘ └─────────────────────────┘
```
---
## 2. Projektstruktur
```
plantplan/
├── bin/ Skripte (setenv, install, activate)
├── cfg/ Konfigurationsdateien
├── client/ Frontend wird auf Server deployed
│ ├── src/
│ │ ├── main.tsx React Entry Point
│ │ └── App.tsx tldraw Canvas
│ ├── index.html
│ ├── package.json
│ ├── tsconfig.json
│ └── vite.config.ts Dev-Proxy /api → localhost:8000
├── server/ Backend wird auf Server kopiert
│ ├── catalog/
│ │ └── symbols.json Symbolbibliothek
│ ├── main.py FastAPI Anwendung
│ └── requirements.txt Python-Abhängigkeiten
├── doc/ Dokumentation
├── lib/ Python-Bibliotheken (eigene Module)
├── tests/ Tests
└── examples/ Beispieldaten
```
---
## 3. Client (Frontend)
| Aspekt | Detail |
|---|---|
| Framework | React 18 + TypeScript |
| Canvas | tldraw v2 |
| Bundler | Vite |
| Dev-Server | `localhost:5173` |
| Build-Output | `client/dist/` (statische Dateien) |
Der Vite Dev-Server leitet `/api/*`-Requests per Proxy an das lokale Backend weiter. Im Produktivbetrieb liefert nginx die statischen Dateien aus und proxied API-Calls.
---
## 4. Server (Backend)
| Aspekt | Detail |
|---|---|
| Framework | FastAPI |
| ASGI-Server | Uvicorn |
| Python | >= 3.11 |
| Dev-Server | `localhost:8000` |
### API-Endpunkte
| Methode | Pfad | Beschreibung |
|---|---|---|
| GET | `/symbols` | Symbolbibliothek als JSON ausliefern |
| POST | `/normalize` | Koordinaten auf Grid snappen |
| POST | `/export` | Finales JSON/SVG-Projektfile erzeugen |
### Datenmodell (Request)
```json
{
"shapes": [
{
"id": "shape:abc",
"type": "weiche",
"x": 100,
"y": 200,
"props": { "radius": 300, "direction": "left" }
}
],
"grid_size": 10
}
```
---
## 5. Lokale Entwicklung
Beide Komponenten laufen parallel auf einer Maschine:
```
Terminal 1 (Server):
cd server
pip install -r requirements.txt
uvicorn main:app --reload
Terminal 2 (Client):
cd client
npm install
npm run dev
```
Kein externer Server nötig. Der Vite-Proxy sorgt dafür, dass Frontend und Backend nahtlos kommunizieren.
---
## 6. Deployment (Hetzner)
Beim Deployment werden zwei Teile auf den Server kopiert:
| Was | Wohin | Wie |
|---|---|---|
| `client/dist/` (nach `npm run build`) | nginx Document Root | Statische Dateien |
| `server/` | Python-Umgebung | uvicorn hinter nginx reverse proxy |
```
nginx
├── / → client/dist/ (statische Dateien)
└── /api/* → proxy_pass http://127.0.0.1:8000
```
---
## 7. Umgebungsvariablen (setenv.bat / setenv.sh)
| Variable | Pfad |
|---|---|
| `PROJECT` | Projekt-Root |
| `PV_CLIENT` | `client/` |
| `PV_SERVER` | `server/` |
| `PV_BIN` | `bin/` |
| `PV_LIB` | `lib/` |
| `PV_CFG` | `cfg/` |
| `PV_DATA` | `data/` |
| `PV_LOG` | `log/` |
| `PV_RESULTS` | `results/` |
| `PV_EXAMPLES` | `examples/` |
| `PV_TESTS` | `tests/` |

View File

@@ -0,0 +1,26 @@
{
"symbols": [
{
"id": "weiche_links_r300",
"label": "Weiche links R300",
"type": "weiche",
"radius": 300,
"direction": "left",
"svg_path": "M 0,0 L 100,0 Q 150,0 200,50",
"snap_points": [[0,0], [100,0], [200,50]],
"ports": ["in", "out_gerade", "out_abzweig"]
},
{
"id": "kreisel_standard",
"label": "Kreisel",
"type": "kreisel",
"geometry": {
"circle_top_r": 40,
"circle_bottom_r": 40,
"straight_length": 80
},
"svg_path": "M -40,0 A 40,40 0 1,1 40,0 A 40,40 0 1,1 -40,0 M -40,160 A 40,40 0 1,1 40,160 A 40,40 0 1,1 -40,160 M -40,0 L -40,160 M 40,0 L 40,160",
"snap_points": [[0,0], [0,160]]
}
]
}

66
server/main.py Normal file
View File

@@ -0,0 +1,66 @@
"""
PlantPlan FastAPI Backend
- Symbolbibliothek ausliefern
- Koordinaten normalisieren
- JSON/SVG Export
"""
import json
from pathlib import Path
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from pydantic import BaseModel
app = FastAPI(title="PlantPlan API")
# CORS: Erlaubt Zugriff vom lokalen Frontend Dev-Server
app.add_middleware(
CORSMiddleware,
allow_origins=["http://localhost:5173"],
allow_methods=["*"],
allow_headers=["*"],
)
CATALOG_DIR = Path(__file__).parent / "catalog"
# --- Models ---
class SketchPayload(BaseModel):
shapes: list[dict]
grid_size: int = 10
# --- Endpoints ---
@app.get("/symbols")
async def get_symbols():
"""Symbolbibliothek als JSON ausliefern."""
catalog_file = CATALOG_DIR / "symbols.json"
with open(catalog_file, encoding="utf-8") as f:
return json.load(f)
@app.post("/normalize")
async def normalize(payload: SketchPayload):
"""Koordinaten auf Grid snappen und SVG-Pfade normalisieren."""
cleaned = []
for shape in payload.shapes:
s = dict(shape)
if "x" in s:
s["x"] = round(s["x"] / payload.grid_size) * payload.grid_size
if "y" in s:
s["y"] = round(s["y"] / payload.grid_size) * payload.grid_size
cleaned.append(s)
return {"shapes": cleaned}
@app.post("/export")
async def export_sketch(payload: SketchPayload):
"""Finales Projektfile als JSON exportieren."""
return {
"json": {
"version": "1.0",
"shapes": payload.shapes,
}
}

3
server/requirements.txt Normal file
View File

@@ -0,0 +1,3 @@
fastapi>=0.115.0
uvicorn[standard]>=0.34.0
pydantic>=2.0.0