247 lines
9.6 KiB
Python
247 lines
9.6 KiB
Python
#!/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)
|