- gen_board_layout.py zeichnet jetzt runde Pucks (Aussenring + 7 Figurenmulden + zentrales Etikett, Gate-Puck rot) statt eckiger Steck-Tiles; board-layout.svg neu generiert (40 Pucks, well-formed XML). - Entfernt (veraltet, nirgends referenziert, hier nicht verifizierbar neu zeichenbar): board-layout.png (alte Tiles), bauteile-masse.svg, 00_Konzept/raci-aktiv-feld.svg, raci-tile-variante.svg. Massgeblich bleiben materialliste.md + die OpenSCAD-Modelle (echte Renderings). - README_3d-druck Inhaltstabelle nachgezogen. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
206 lines
8.9 KiB
Python
206 lines
8.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Generiert die Board-Layout-Skizze (SVG) fuer den SLC-Workshop.
|
|
Lineares Phasen-Swimlane-Layout: jede Phase eine Zeile, Pucks links->rechts.
|
|
Exakt 40 Pucks (37 Aktivitaeten + 3 Gate-Pucks). Reproduzierbar: bei Aenderungen
|
|
einfach erneut ausfuehren -> board-layout.svg.
|
|
"""
|
|
|
|
import math
|
|
|
|
# (id, kurzname, is_gate)
|
|
PHASES = [
|
|
("DESIGN", "#2F80C9", [
|
|
("ds_01", "Eigenschaften definieren", False),
|
|
("ds_02", "Komponenten designen", False),
|
|
("ds_03", "Vorgehen beschreiben", False),
|
|
("ds_04", "Implementierung vorbereiten", False),
|
|
]),
|
|
("TRANSITION", "#E8893B", [
|
|
("tr_01", "Entw. / Konfig.?", True),
|
|
("tr_02", "Entwicklung koordinieren", False),
|
|
("tr_03", "Anwendungen entwickeln", False),
|
|
("tr_04", "Komponenten annehmen", False),
|
|
("tr_05", "Komponenten konfigurieren", False),
|
|
("tr_06", "Betriebsdoku erstellen", False),
|
|
("tr_07", "Komponenten testen", False),
|
|
("tr_08", "Formale Uebergabe", False),
|
|
("tr_09", "Entry-Pruefung", True),
|
|
("tr_10", "Ausrollen", False),
|
|
("tr_11", "Aktivierung vorbereiten", False),
|
|
("tr_12", "Go-Live-Freigabe", True),
|
|
]),
|
|
("OPERATION", "#5BAE5B", [
|
|
("op_01", "Early Life Support", False),
|
|
("op_02", "Betriebs-Leitlinien", False),
|
|
("op_03", "Laufender Betrieb", False),
|
|
("op_04", "Ressourcen & Budget", False),
|
|
("op_05", "Services ueberwachen", False),
|
|
("op_06", "Qualitaetsbericht", False),
|
|
("op_07", "Proaktive Problemerkennung", False),
|
|
]),
|
|
("SUPPORT", "#3FB5B5", [
|
|
("sp_01", "Support-Leitlinien", False),
|
|
("sp_02", "Wissensdatenbank", False),
|
|
("sp_03", "Incidents/Requests verteilen", False),
|
|
("sp_04", "Requests bearbeiten", False),
|
|
("sp_05", "Incident 1st Level", False),
|
|
("sp_06", "Incident 2nd Level", False),
|
|
("sp_07", "Record geloest", False),
|
|
("sp_08", "Schliessen", False),
|
|
("sp_09", "Problem Record anlegen", False),
|
|
("sp_10", "Wiederk. Incidents -> Problem", False),
|
|
("sp_11", "RCA & Workaround", False),
|
|
]),
|
|
("REVIEW", "#8E63B5", [
|
|
("rv_01", "Taktische RCA + KPIs", False),
|
|
("rv_02", "Performance & Improvement", False),
|
|
("rv_03", "SOR Periodischer Review", False),
|
|
("rv_04", "Service Improvement", False),
|
|
("rv_05", "Redesign / Erweiterung", False),
|
|
("rv_06", "Ausserbetriebnahme", False),
|
|
]),
|
|
]
|
|
|
|
# Layout-Parameter
|
|
TILE_W, TILE_H = 86, 86 # Zelle je Puck (rund, inscribed)
|
|
GAP_X, GAP_Y = 12, 40
|
|
PUCK_R = 35 # Puck-Radius in px (= Ø100 mm)
|
|
LABEL_W = 150
|
|
X0 = 30 + LABEL_W + 20
|
|
Y0 = 96
|
|
MAX_TILES = max(len(t) for _, _, t in PHASES)
|
|
WIDTH = X0 + MAX_TILES * (TILE_W + GAP_X) + 200
|
|
HEIGHT = Y0 + len(PHASES) * (TILE_H + GAP_Y) + 120
|
|
TILE_MM = 100 # ein Puck = Ø100 mm
|
|
|
|
|
|
def esc(s):
|
|
return s.replace("&", "&").replace("<", "<").replace(">", ">")
|
|
|
|
|
|
def lighten(hexcol, f=0.85):
|
|
h = hexcol.lstrip("#")
|
|
r, g, b = int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16)
|
|
r = int(r + (255 - r) * f)
|
|
g = int(g + (255 - g) * f)
|
|
b = int(b + (255 - b) * f)
|
|
return f"#{r:02x}{g:02x}{b:02x}"
|
|
|
|
|
|
def tile_svg(x, y, tid, name, color, is_gate):
|
|
"""Zeichnet einen runden Puck: Aussenring, 7 Figurenmulden, zentrales Etikett."""
|
|
cx, cy = x + TILE_W / 2.0, y + TILE_H / 2.0
|
|
fill = color if is_gate else lighten(color, 0.90)
|
|
stroke = color
|
|
sw = 3 if is_gate else 2
|
|
parts = []
|
|
# Puck-Koerper
|
|
parts.append(f'<circle cx="{cx}" cy="{cy}" r="{PUCK_R}" '
|
|
f'fill="{fill}" stroke="{stroke}" stroke-width="{sw}"/>')
|
|
# 7 Figurenmulden im Ring
|
|
for k in range(7):
|
|
a = math.radians(360.0 / 7 * k - 90)
|
|
wx = cx + (PUCK_R - 8) * math.cos(a)
|
|
wy = cy + (PUCK_R - 8) * math.sin(a)
|
|
parts.append(f'<circle cx="{wx:.1f}" cy="{wy:.1f}" r="3" fill="none" '
|
|
f'stroke="{stroke}" stroke-width="1.1" opacity="0.6"/>')
|
|
# zentrales Etikett-Feld
|
|
parts.append(f'<circle cx="{cx}" cy="{cy}" r="16" fill="#ffffff" '
|
|
f'opacity="0.92" stroke="{stroke}" stroke-width="0.8"/>')
|
|
parts.append(f'<text x="{cx}" y="{cy+0.5}" text-anchor="middle" '
|
|
f'font-size="11.5" font-weight="700" fill="#1a1a1a">{esc(tid)}</text>')
|
|
# Name unter dem Puck
|
|
parts.append(f'<text x="{cx}" y="{cy+PUCK_R+12}" text-anchor="middle" '
|
|
f'font-size="9.5" fill="#333">{esc(name)}</text>')
|
|
if is_gate:
|
|
parts.append(f'<text x="{cx}" y="{cy-PUCK_R-5}" text-anchor="middle" '
|
|
f'font-size="10" font-weight="700" fill="{stroke}">GATE</text>')
|
|
return "\n".join(parts)
|
|
|
|
|
|
def arrow(x1, y1, x2, y2, color="#666", w=2.2):
|
|
return (f'<line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" stroke="{color}" '
|
|
f'stroke-width="{w}" marker-end="url(#ah)"/>')
|
|
|
|
|
|
svg = []
|
|
svg.append(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {WIDTH} {HEIGHT}" '
|
|
f'font-family="Arial, Helvetica, sans-serif">')
|
|
svg.append(f'<rect x="0" y="0" width="{WIDTH}" height="{HEIGHT}" fill="#f7f7f5"/>')
|
|
svg.append('<defs><marker id="ah" markerWidth="9" markerHeight="9" refX="7" refY="3" '
|
|
'orient="auto" markerUnits="strokeWidth">'
|
|
'<path d="M0,0 L7,3 L0,6 Z" fill="#666"/></marker></defs>')
|
|
# Titel
|
|
svg.append(f'<text x="30" y="44" font-size="26" font-weight="800" fill="#1a1a1a">'
|
|
f'Service-Lifecycle — Board-Layout (40 Pucks)</text>')
|
|
svg.append(f'<text x="30" y="68" font-size="14" fill="#555">'
|
|
f'37 Aktivitaeten + 3 Gate-Pucks · 1 Puck = Ø{TILE_MM} mm · '
|
|
f'lose Bahn, Sequenz links nach rechts</text>')
|
|
|
|
row_y = {}
|
|
for ri, (pname, color, tiles) in enumerate(PHASES):
|
|
y = Y0 + ri * (TILE_H + GAP_Y)
|
|
row_y[pname] = y
|
|
# Phasen-Label
|
|
svg.append(f'<rect x="30" y="{y}" width="{LABEL_W}" height="{TILE_H}" rx="9" '
|
|
f'fill="{color}"/>')
|
|
svg.append(f'<text x="{30+LABEL_W/2}" y="{y+TILE_H/2-2}" text-anchor="middle" '
|
|
f'font-size="17" font-weight="800" fill="#fff">{esc(pname)}</text>')
|
|
svg.append(f'<text x="{30+LABEL_W/2}" y="{y+TILE_H/2+18}" text-anchor="middle" '
|
|
f'font-size="12" fill="#fff">{len(tiles)} Pucks</text>')
|
|
# Tiles
|
|
prev = None
|
|
for ti, (tid, name, is_gate) in enumerate(tiles):
|
|
x = X0 + ti * (TILE_W + GAP_X)
|
|
if prev is not None:
|
|
svg.append(arrow(prev + 8, y + TILE_H/2, x - 2, y + TILE_H/2))
|
|
svg.append(tile_svg(x, y, tid, name, color, is_gate))
|
|
prev = x + TILE_W
|
|
# Connector zur naechsten Phase (von letztem Tile runter zur naechsten Zeile Start)
|
|
if ri < len(PHASES) - 1:
|
|
lastx = X0 + (len(tiles) - 1) * (TILE_W + GAP_X) + TILE_W/2
|
|
ny = y + TILE_H + GAP_Y
|
|
svg.append(f'<path d="M {lastx} {y+TILE_H} V {y+TILE_H+GAP_Y/2} '
|
|
f'H {X0+TILE_W/2} V {ny-2}" fill="none" stroke="#999" '
|
|
f'stroke-width="2.2" stroke-dasharray="5 4" marker-end="url(#ah)"/>')
|
|
|
|
# Operation <-> Support Loop (links neben den Labels)
|
|
oy = row_y["OPERATION"] + TILE_H/2
|
|
sy = row_y["SUPPORT"] + TILE_H/2
|
|
svg.append(f'<path d="M 22 {oy} C 4 {oy}, 4 {sy}, 22 {sy}" fill="none" '
|
|
f'stroke="#d23" stroke-width="2.6" marker-end="url(#ah)" marker-start="url(#ah)"/>')
|
|
svg.append(f'<text x="2" y="{(oy+sy)/2}" font-size="11" fill="#d23" '
|
|
f'transform="rotate(-90 8 {(oy+sy)/2})" text-anchor="middle">Betriebs-Loop</text>')
|
|
|
|
# Exit nach Review (DPM-Ruecklauf)
|
|
ry = row_y["REVIEW"] + TILE_H/2
|
|
rx = X0 + (len(PHASES[-1][2]) - 1) * (TILE_W + GAP_X) + TILE_W
|
|
svg.append(arrow(rx + 6, ry, rx + 70, ry, color="#8E63B5", w=2.6))
|
|
svg.append(f'<text x="{rx+78}" y="{ry-6}" font-size="12.5" font-weight="700" '
|
|
f'fill="#8E63B5">zurueck in DPM</text>')
|
|
svg.append(f'<text x="{rx+78}" y="{ry+12}" font-size="11" fill="#666">'
|
|
f'rv_05 Redesign / rv_06 Retirement</text>')
|
|
|
|
# Legende / Massstab
|
|
ly = HEIGHT - 64
|
|
svg.append(f'<circle cx="43" cy="{ly+9}" r="10" fill="#d23"/>')
|
|
svg.append(f'<text x="60" y="{ly+14}" font-size="12.5" fill="#333">Gate-Puck (rot, Etikett G1/G2/G3 + Icon)</text>')
|
|
svg.append(f'<circle cx="373" cy="{ly+9}" r="10" fill="{lighten("#2F80C9",0.90)}" stroke="#2F80C9" stroke-width="2"/>')
|
|
svg.append(f'<text x="390" y="{ly+14}" font-size="12.5" fill="#333">Station-Puck (Ø100, 7 Figurenmulden + Etikett)</text>')
|
|
|
|
# Gesamtbreite-Hinweis
|
|
total_mm = MAX_TILES * (TILE_MM + 10)
|
|
svg.append(f'<text x="30" y="{HEIGHT-28}" font-size="12.5" fill="#555">'
|
|
f'Breiteste Phase: {MAX_TILES} Pucks ~ {total_mm/10:.0f} cm '
|
|
f'(bei Ø{TILE_MM} mm Pucks + ~10 mm Abstand). Bahn bei Platzmangel maeandrierend.</text>')
|
|
|
|
svg.append('</svg>')
|
|
|
|
out = "board-layout.svg"
|
|
with open(out, "w", encoding="utf-8") as f:
|
|
f.write("\n".join(svg))
|
|
|
|
total = sum(len(t) for _, _, t in PHASES)
|
|
gates = sum(1 for _, _, t in PHASES for _, _, g in t if g)
|
|
print(f"geschrieben: {out}")
|
|
print(f"Pucks gesamt: {total} (Aktivitaeten: {total-gates}, Gate-Pucks: {gates})")
|