Add --tosvg switch to render seating as SVG, plus test images and docs

Sitzplatzverteilung.alsSVG() (libs/Strukturdaten.py) draws tables as
white circles and people as colored circles inside their table's
circle, colored by group membership. Table radius is derived from
the number of seats so all person-circles fit without touching; the
table-to-table grid spacing is derived from the largest table radius
so tables never overlap regardless of their Koordinaten values.

libs/platz.py gains a --tosvg switch that writes
<indir>/Sitzplatzverteilung.svg alongside the existing text/CSV
output.

The three unittest scenarios now each write their rendering to
doc/images/, and doc/docu_tests.md explains the three configurations
and what each test actually verifies (including why the "split
groups land on true neighbor tables" property only holds for a
specifically chosen seed in the circle scenario, not in general).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Michael Stangl
2026-07-09 12:55:32 +02:00
parent 0232e54f37
commit 74e08f389c
9 changed files with 292 additions and 4 deletions
+110
View File
@@ -13,6 +13,7 @@ from itertools import chain
from random import *
import configparser, os
import copy
import math
from functools import total_ordering
from xml.dom import minidom
@@ -532,6 +533,115 @@ class Sitzplatzverteilung:
fout.writelines( Liste )
fout.close()
# Farbpalette fuer die Gruppen-Einfaerbung im SVG (wird zyklisch
# wiederverwendet, falls es mehr Gruppen als Farben gibt)
SVGGruppenFarben = [
'#e6194b', '#3cb44b', '#4363d8', '#f58231', '#911eb4',
'#42d4f4', '#f032e6', '#bfef45', '#fabed4', '#469990',
'#dcbeff', '#9a6324', '#800000', '#aaffc3', '#000075' ]
def alsSVG(self, OutputSVG, PersonenRadius=12, Puffer=10):
"""Zeichnet die Sitzplatzverteilung als SVG:
Tische werden als weisse Kreise dargestellt, Personen als
farbige Kreise (Farbe = Gruppe) innerhalb ihres Tischkreises.
Das Koordinatenraster der Tische wird so skaliert, dass alle
Personenkreise in ihrem Tischkreis Platz haben und sich die
Tischkreise untereinander nicht ueberschneiden.
"""
# Tischradius je Tisch berechnen: gross genug, damit alle
# Personenkreise (auf einem inneren Kreis gleichmaessig verteilt)
# nebeneinander Platz haben, ohne sich zu beruehren. Der Faktor
# 1.1 auf den rechnerischen Mindestabstand ist ein kleiner
# Sicherheitsabstand gegen Rundungsfehler und damit sich die
# Kreise auch optisch sichtbar nicht beruehren.
def SitzRadiusVon(Plaetze):
if Plaetze <= 1:
return 0.0
return 1.1 * PersonenRadius / math.sin(math.pi / Plaetze)
def TischRadius(Plaetze):
return SitzRadiusVon(Plaetze) + PersonenRadius + Puffer
TidZuRadius = {}
GroessterRadius = 0
for T in self.Tische:
r = TischRadius( T.Plaetze )
TidZuRadius[T.Id] = r
if r > GroessterRadius:
GroessterRadius = r
# Rasterabstand der Tisch-Koordinaten: doppelter groesster
# Tischradius (plus Puffer) verhindert Ueberschneidungen
# unabhaengig davon, welche Tische tatsaechlich benachbart sind
Rasterabstand = 2 * GroessterRadius + Puffer
Xs = [ T.X_Koordinate for T in self.Tische ]
Ys = [ T.Y_Koordinate for T in self.Tische ]
MinX, MinY = min(Xs), min(Ys)
MaxX, MaxY = max(Xs), max(Ys)
Rand = GroessterRadius + Puffer
Breite = (MaxX - MinX) * Rasterabstand + 2 * Rand
Hoehe = (MaxY - MinY) * Rasterabstand + 2 * Rand
def TischMitte(T):
x = (T.X_Koordinate - MinX) * Rasterabstand + Rand
y = (T.Y_Koordinate - MinY) * Rasterabstand + Rand
return (x, y)
def GruppeVonPerson(P):
try:
return self.Gruppen.GruppeVonPid(P.Id)
except KeyError:
return None
GidZuFarbe = {}
def FarbeVonGruppe(G):
if G is None:
return '#888888'
if G.Id not in GidZuFarbe:
Index = len(GidZuFarbe) % len(self.SVGGruppenFarben)
GidZuFarbe[G.Id] = self.SVGGruppenFarben[Index]
return GidZuFarbe[G.Id]
Teile = []
Teile.append( '<?xml version="1.0" encoding="UTF-8"?>\n' )
Teile.append(
'<svg xmlns="http://www.w3.org/2000/svg" width="%g" height="%g" '
'viewBox="0 0 %g %g">\n' % (Breite, Hoehe, Breite, Hoehe) )
Teile.append( '<rect width="100%" height="100%" fill="white"/>\n' )
for T in self.Tische:
Mx, My = TischMitte(T)
TR = TidZuRadius[T.Id]
Teile.append(
'<circle cx="%g" cy="%g" r="%g" fill="white" '
'stroke="black" stroke-width="1.5"/>\n' % (Mx, My, TR) )
Teile.append(
'<text x="%g" y="%g" text-anchor="middle" '
'font-size="10" fill="black">Tisch %s</text>\n'
% (Mx, My - TR - 2, T.Id) )
Plaetze = list(T)
n = len(Plaetze)
SitzRadius = SitzRadiusVon( T.Plaetze )
for i, P in enumerate(Plaetze):
Winkel = 2 * math.pi * i / max(n, 1) - math.pi / 2
Px = Mx + SitzRadius * math.cos(Winkel)
Py = My + SitzRadius * math.sin(Winkel)
Farbe = FarbeVonGruppe( GruppeVonPerson(P) )
Teile.append(
'<circle cx="%g" cy="%g" r="%g" fill="%s" '
'stroke="black" stroke-width="1"/>\n'
% (Px, Py, PersonenRadius, Farbe) )
Teile.append( '</svg>\n' )
fout = open( OutputSVG, 'w' )
fout.writelines( Teile )
fout.close()
def mutieren(self):
"""Aendert die vorhandene Sitzplatzverteilung hoffentlich zum Besseren"""
[IdsSchlechteGruppen, Wertigkeiten] = self.SchlechteGruppenTopX(Anzahl=self.MutationsGroesse)