Files
limbach/build_deck.py
T
marcus hinz 93bb19abf0 initial
2026-06-20 14:40:33 +02:00

247 lines
9.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Workshop-Folien 'Vault Technischer Impuls': ruhig, visuell, wenig Text."""
import os
from pptx import Presentation
from pptx.util import Inches, Pt
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN, MSO_ANCHOR
from pptx.enum.shapes import MSO_SHAPE
# ---- Palette ---------------------------------------------------------------
INK = RGBColor(0x2A, 0x2F, 0x3A)
NAVY = RGBColor(0x1F, 0x3A, 0x5F)
ACCENT = RGBColor(0xC0, 0x49, 0x2F)
MUTE = RGBColor(0x8A, 0x93, 0xA0)
SOFT = RGBColor(0xE9, 0xEC, 0xEF) # heller Kasten
SOFT2 = RGBColor(0xDD, 0xE5, 0xEC) # heller Kasten blau
LINEC = RGBColor(0xC3, 0xC9, 0xD1)
BG = RGBColor(0xFF, 0xFF, 0xFF)
WHITE = RGBColor(0xFF, 0xFF, 0xFF)
F = "Calibri"
prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
SW, SH = prs.slide_width, prs.slide_height
BLANK = prs.slide_layouts[6]
MX = Inches(0.95)
def slide(bg=BG):
s = prs.slides.add_slide(BLANK)
r = s.shapes.add_shape(MSO_SHAPE.RECTANGLE, 0, 0, SW, SH)
r.fill.solid(); r.fill.fore_color.rgb = bg; r.line.fill.background()
r.shadow.inherit = False
s.shapes._spTree.remove(r._element); s.shapes._spTree.insert(2, r._element)
return s
def shape(s, kind, x, y, w, h, fill=None, line=None, lw=Pt(1)):
sh = s.shapes.add_shape(kind, x, y, w, h)
if fill is None: sh.fill.background()
else: sh.fill.solid(); sh.fill.fore_color.rgb = fill
if line is None: sh.line.fill.background()
else: sh.line.color.rgb = line; sh.line.width = lw
sh.shadow.inherit = False
return sh
def box(s, x, y, w, h, text="", fill=SOFT, tc=INK, size=15, bold=False,
rounded=True, line=None, sub=None, sub_c=None):
kind = MSO_SHAPE.ROUNDED_RECTANGLE if rounded else MSO_SHAPE.RECTANGLE
sh = shape(s, kind, x, y, w, h, fill=fill, line=line)
tf = sh.text_frame; tf.word_wrap = True
tf.vertical_anchor = MSO_ANCHOR.MIDDLE
tf.margin_top = tf.margin_bottom = Pt(2)
p = tf.paragraphs[0]; p.alignment = PP_ALIGN.CENTER
r = p.add_run(); r.text = text
r.font.size = Pt(size); r.font.bold = bold; r.font.name = F; r.font.color.rgb = tc
if sub:
p2 = tf.add_paragraph(); p2.alignment = PP_ALIGN.CENTER; p2.space_before = Pt(2)
r2 = p2.add_run(); r2.text = sub
r2.font.size = Pt(11); r2.font.name = F; r2.font.color.rgb = sub_c or MUTE
return sh
def text(s, x, y, w, h, runs, align=PP_ALIGN.LEFT, anchor=MSO_ANCHOR.TOP,
leading=1.1, space=0):
tb = s.shapes.add_textbox(x, y, w, h); tf = tb.text_frame
tf.word_wrap = True; tf.vertical_anchor = anchor
tf.margin_left = tf.margin_right = tf.margin_top = tf.margin_bottom = Pt(0)
p = tf.paragraphs[0]; p.alignment = align; p.line_spacing = leading; p.space_after = Pt(space)
for t, kw in runs:
r = p.add_run(); r.text = t
r.font.size = Pt(kw.get("size", 16)); r.font.bold = kw.get("bold", False)
r.font.italic = kw.get("italic", False); r.font.name = F
r.font.color.rgb = kw.get("color", INK)
return tb
def kicker(s, t):
tb = text(s, MX, Inches(0.62), Inches(11), Inches(0.4),
[(t.upper(), {"size": 12.5, "color": ACCENT, "bold": True})])
tb.text_frame.paragraphs[0].runs[0].font._rPr.set('spc', '240')
def head(s, t):
text(s, MX, Inches(1.0), Inches(11.4), Inches(0.9),
[(t, {"size": 32, "color": NAVY, "bold": True})])
shape(s, MSO_SHAPE.RECTANGLE, MX, Inches(1.78), Inches(0.6), Pt(4), fill=ACCENT)
def caption(s, t):
text(s, MX, Inches(6.62), Inches(11.4), Inches(0.6),
[(t, {"size": 16, "color": MUTE})], leading=1.15)
def vline(s, x, y, h, color=LINEC):
shape(s, MSO_SHAPE.RECTANGLE, x, y, Pt(1.6), h, fill=color)
# ===========================================================================
# TITEL
# ===========================================================================
s = slide(NAVY)
shape(s, MSO_SHAPE.RECTANGLE, 0, 0, Inches(0.24), SH, fill=ACCENT)
shape(s, MSO_SHAPE.RECTANGLE, MX, Inches(2.7), Inches(0.85), Pt(5), fill=ACCENT)
text(s, MX, Inches(1.7), Inches(10), Inches(0.5),
[("TECHNISCHER IMPULS", {"size": 14, "color": RGBColor(0xC6,0xD1,0xDE), "bold": True})]
).text_frame.paragraphs[0].runs[0].font._rPr.set('spc', '280')
text(s, MX, Inches(3.05), Inches(11.4), Inches(2.0),
[("LIS wohin", {"size": 50, "color": WHITE, "bold": True})])
text(s, MX, Inches(4.05), Inches(11.4), Inches(2.0),
[("technisch denken?", {"size": 50, "color": WHITE, "bold": True})])
# ===========================================================================
# FOLIE 1 Produkt oder Projekt (Balken-Bild)
# ===========================================================================
s = slide()
kicker(s, "Diagnose")
head(s, "Produkt oder Projekt?")
base = Inches(5.45) # Grundlinie
bw, gap = Inches(0.52), Inches(0.16)
def bars(cx_start, heights, colors, label):
n = len(heights)
for i, (hh, col) in enumerate(zip(heights, colors)):
x = cx_start + i * (bw + gap)
h = Inches(hh)
shape(s, MSO_SHAPE.ROUNDED_RECTANGLE, x, base - h, bw, h, fill=col)
total = n * bw + (n - 1) * gap
# Grundlinie
shape(s, MSO_SHAPE.RECTANGLE, cx_start, base, total, Pt(2), fill=LINEC)
text(s, cx_start, base + Inches(0.18), total, Inches(0.4),
[(label, {"size": 18, "color": NAVY, "bold": True})], align=PP_ALIGN.CENTER)
bars(Inches(1.5), [0.45, 0.5, 2.7, 0.5, 0.45],
[MUTE, MUTE, ACCENT, MUTE, MUTE], "Projekt")
bars(Inches(8.0), [1.7, 2.0, 1.8, 2.1, 1.7],
[NAVY, NAVY, NAVY, NAVY, NAVY], "Produkt")
# kleine neutrale Unterschriften
text(s, Inches(1.5), base + Inches(0.6), Inches(3.42), Inches(0.4),
[("tief in einer Dimension", {"size": 13, "color": MUTE})], align=PP_ALIGN.CENTER)
text(s, Inches(8.0), base + Inches(0.6), Inches(3.42), Inches(0.4),
[("breit über viele", {"size": 13, "color": MUTE})], align=PP_ALIGN.CENTER)
# ===========================================================================
# FOLIE 2 Eine Schicht dazwischen (Layer-Diagramm)
# ===========================================================================
s = slide()
kicker(s, "Architektur")
head(s, "Eine Schicht dazwischen")
cxL, cxR = MX, Inches(12.38)
full_w = cxR - cxL
# oben: Gruppe / Prozesse
top_y = Inches(2.2)
chips = ["Reporting", "DFÜ / Anlieferung", "Fusion"]
cw = Inches(3.2); cg = (full_w - 3 * cw) / 2
for i, c in enumerate(chips):
box(s, cxL + i * (cw + cg), top_y, cw, Inches(0.72), c,
fill=BG, tc=NAVY, size=15, bold=True, line=LINEC)
# Mitte: die Schicht (Hero)
mid_y = Inches(3.55)
box(s, cxL, mid_y, full_w, Inches(0.95),
"Integrations- & Abstraktionsschicht",
fill=NAVY, tc=WHITE, size=20, bold=True)
# unten: Systeme
bot_y = Inches(5.15)
labs = [("Labor A", "LIS"), ("Labor B", "LIS"), ("Vault", "LIS"), ("Labor C", "eigenes LIS")]
lw = Inches(2.65); lg = (full_w - 4 * lw) / 3
for i, (nm, sub) in enumerate(labs):
x = cxL + i * (lw + lg)
box(s, x, bot_y, lw, Inches(0.95), nm, fill=SOFT, tc=INK, size=16, bold=True,
sub=sub, sub_c=MUTE)
# Verbinder Schicht -> System
vline(s, x + lw / 2, mid_y + Inches(0.95), bot_y - (mid_y + Inches(0.95)))
# Verbinder oben -> Schicht
for i in range(3):
cx = cxL + i * (cw + cg) + cw / 2
vline(s, cx, top_y + Inches(0.72), mid_y - (top_y + Inches(0.72)))
caption(s, "Labore docken an und behalten ihr LIS. Die Schicht verbindet.")
# ===========================================================================
# FOLIE 3 Zustand oder Ereignis (Event Sourcing, visuell)
# ===========================================================================
s = slide()
kicker(s, "Modell")
head(s, "Zustand oder Ereignis")
cxL, cxR = MX, Inches(12.38)
full_w = cxR - cxL
# --- obere Spur: Zustand ---
ay = Inches(2.35)
text(s, cxL, ay - Inches(0.05), Inches(3), Inches(0.4),
[("Zustand", {"size": 15, "color": MUTE, "bold": True})])
zy = ay + Inches(0.42)
box(s, cxL, zy, Inches(2.4), Inches(0.95), "Probe", fill=SOFT, tc=INK, size=16, bold=True)
# Pfeil "überschreiben"
shape(s, MSO_SHAPE.RIGHT_ARROW, cxL + Inches(2.65), zy + Inches(0.27),
Inches(1.5), Inches(0.42), fill=LINEC)
text(s, cxL + Inches(2.55), zy - Inches(0.18), Inches(1.7), Inches(0.3),
[("überschreiben", {"size": 11, "color": MUTE, "italic": True})], align=PP_ALIGN.CENTER)
box(s, cxL + Inches(4.4), zy, Inches(2.7), Inches(0.95), "1 Abrechnung",
fill=BG, tc=NAVY, size=16, bold=True, line=LINEC)
# Trennlinie
shape(s, MSO_SHAPE.RECTANGLE, cxL, Inches(4.25), full_w, Pt(1.2), fill=LINEC)
# --- untere Spur: Ereignisse ---
by = Inches(4.55)
text(s, cxL, by - Inches(0.05), Inches(3), Inches(0.4),
[("Ereignisse", {"size": 15, "color": MUTE, "bold": True})])
ey = by + Inches(0.42)
ev = ["Eingang", "Messung", "Abrechnung", "Abrechnung"]
ew = Inches(1.85); eg = Inches(0.32)
for i, e in enumerate(ev):
x = cxL + i * (ew + eg)
col = ACCENT if e == "Abrechnung" else NAVY
box(s, x, ey, ew, Inches(0.85), e, fill=col, tc=WHITE, size=14, bold=True)
if i < len(ev) - 1:
shape(s, MSO_SHAPE.CHEVRON, x + ew + Inches(0.02), ey + Inches(0.24),
Inches(0.28), Inches(0.38), fill=LINEC)
# Pfeil zum abgeleiteten Zustand
last_x = cxL + 4 * (ew + eg)
shape(s, MSO_SHAPE.RIGHT_ARROW, last_x - Inches(0.05), ey + Inches(0.22),
Inches(0.7), Inches(0.42), fill=LINEC)
box(s, last_x + Inches(0.75), ey - Inches(0.05), Inches(2.7), Inches(0.95),
"Zustand", fill=SOFT2, tc=NAVY, size=15, bold=True,
sub="abgeleitet", sub_c=MUTE)
caption(s, "Der Zustand entsteht aus den Ereignissen Mehrfaches inklusive.")
out = os.path.join(os.path.dirname(os.path.abspath(__file__)), "Vault_Impuls_Workshop.pptx")
prs.save(out)
print("saved", out)