From e7e7912211495f6793206fca9816ff62685b9a47 Mon Sep 17 00:00:00 2001 From: breitenbach76 Date: Tue, 9 Jun 2026 08:21:31 +0200 Subject: [PATCH] v21 --- 01_3D-Druck/blender/README.md | 9 +- .../__pycache__/raci-board.cpython-312.pyc | Bin 14305 -> 13549 bytes 01_3D-Druck/blender/raci-board.py | 80 +++++++----------- 04_Tablet-Quiz/app/index.html | 34 ++++---- 04_Tablet-Quiz/app/sw.js | 2 +- 5 files changed, 55 insertions(+), 70 deletions(-) diff --git a/01_3D-Druck/blender/README.md b/01_3D-Druck/blender/README.md index 96a75c3..7cb6ccc 100644 --- a/01_3D-Druck/blender/README.md +++ b/01_3D-Druck/blender/README.md @@ -1,7 +1,12 @@ # Blender-Workflow — RACI-Konsolen-Board -Besserer Look als OpenSCAD: weiche Fasen (Bevel), smooth shading, optional Relief, -schöne Vorschau-Renders — und trotzdem **maßhaltig** (alle Mulden/Passungen im Skript). +**Aufteilung (bewusst):** +- **`raci-board.py` = FUNKTIONS-BLANK** — korrekt & schlicht: runde Platte, exakte + Mulden/Sockel/Chip, Kartenschlitz, klar getrennte RACI-Sektoren, lesbare Labels. + Hier geht es nur um **Maße & Anordnung**, NICHT um den edlen Look. +- **Styling separat** — der Premium-Look (weiche Fasen, Relief, gekrümmte Typo, + Material) wird **interaktiv** draufmodelliert (von Hand in Blender oder durch + eine:n Designer:in), weil sich Ästhetik nicht sinnvoll blind skripten lässt. ## Tooling - **Blender** (gratis): https://www.blender.org/download/ (4.2 LTS empfohlen). diff --git a/01_3D-Druck/blender/__pycache__/raci-board.cpython-312.pyc b/01_3D-Druck/blender/__pycache__/raci-board.cpython-312.pyc index eeb29daf2e85d34527e6c09776a0b586de4e1c48..e7742456cba9cc272a49a520eab59f8408412030 100644 GIT binary patch delta 4191 zcmai1drVu`89(P<8~g%nY%mY=G}xH;J3vT*U9@fB$(X5}cAtwQdQn0&QX|bE zK-!n!0FQJi3PlZ46pdntR4+*lC>9x!X^

2NgmL)?p2byPyT&0&$7rDHZg{EO|5t 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 Open -> Run | blender -b -P raci-board.py +# Ziel: korrekt & schlicht (Maße/Passungen stimmen). Der edle Look wird SEPARAT +# interaktiv draufmodelliert. Hier nur: runde Platte, Chip-Mulde, 10 Sockel in +# 4 klar getrennten RACI-Sektoren (R3 A1 C4 I2), Kartenschlitz, lesbare Labels. import bpy, math, os # ----------------------------- Parameter (mm) ----------------------------- R_BOARD, BASE_H = 90.0, 12.0 -EDGE_BEVEL, EDGE_SEG = 1.6, 4 +EDGE_BEVEL, EDGE_SEG = 1.4, 3 CHIP_D, CHIP_DEP = 40.6, 1.8 # Acryl-Chip Ø40 x 2 mm NOTCH_D = 12.0 SOCK_D, SOCK_DEP = 25.3, 1.5 # Figuren-Sockel Ø24,5 -SOCK_LEAD = 0.6 -RING_R = 64.0 +RING_R = 62.0 -PHASE_NAME = "DESIGN" -PHASE_COLOR = (0.184, 0.502, 0.788, 1) # #2f80c9 Design-Blau +PHASE_NAME, PHASE_COLOR = "DESIGN", (0.184, 0.502, 0.788, 1) # #2f80c9 -# Sektoren: Name, Wort-Mittenwinkel, Sockel-Winkel (Grad, 90=oben). Top frei fuer Karte. +# Sektor: Name, Label-Mittenwinkel, Sockel-Winkel (Grad; 90=oben, Top frei fuer Karte). +# Lücken zwischen den Sektoren (34-36°) > Lücken innerhalb (28°) -> Gruppen klar sichtbar. SECTORS = [ - ("RESPONSIBLE", 165, [135, 165, 195]), - ("ACCOUNTABLE", 50, [50]), - ("CONSULTED", -20, [25, -5, -35, -65]), - ("INFORMED", -110, [-95, -125]), + ("RESPONSIBLE", 174, [146, 174, 202]), + ("ACCOUNTABLE", 58, [58]), + ("CONSULTED", -20, [22, -6, -34, -62]), + ("INFORMED", -110, [-96, -124]), ] -DIVIDERS = [37.5, -80, -145] # Grenzen zwischen Sektoren (nicht im Karten-Spalt) -RIDGE_H, RIDGE_W = 2.6, 3.4 +WORD_R = RING_R + SOCK_D/2 + 8 # Labels ausserhalb der Sockel +WORD_SIZE, WORD_DEP = 6.0, 0.9 +DESIGN_SIZE, DESIGN_DEP, DESIGN_POS = 9.0, 1.0, (0, -32) -WORD_SIZE, WORD_DEP = 5.0, 0.8 -WORD_R = RING_R + SOCK_D/2 + 9 # ausserhalb der Sockel -DESIGN_SIZE, DESIGN_DEP = 9.0, 1.0 -DESIGN_POS = (0, -38) # unten im Innenkreis, gegenueber Kartenhalter - -CARD_CY, CARD_BW, CARD_BD, CARD_BH = 70.0, 76.0, 20.0, 16.0 +CARD_CY, CARD_BW, CARD_BD, CARD_BH = 72.0, 76.0, 18.0, 14.0 SLOT_W, SLOT_T, SLOT_TILT = 63.0, 4.0, 12.0 TOP = BASE_H @@ -46,8 +40,7 @@ def _outdir(): try: return os.path.dirname(os.path.abspath(__file__)) except NameError: return os.path.expanduser("~") HERE = _outdir() -STL_OUT = os.path.join(HERE, "raci-board.stl") -PNG_OUT = os.path.join(HERE, "raci_preview.png") +STL_OUT, PNG_OUT = os.path.join(HERE, "raci-board.stl"), os.path.join(HERE, "raci_preview.png") print("Ausgabe-Ordner:", HERE) # ----------------------------- Helfer ----------------------------- @@ -71,8 +64,7 @@ def boolean(obj, tool, op='DIFFERENCE'): try: m.solver = 'EXACT' except Exception: pass bpy.context.view_layer.objects.active = obj - bpy.ops.object.modifier_apply(modifier=m.name) - bpy.data.objects.remove(tool, do_unlink=True) + bpy.ops.object.modifier_apply(modifier=m.name); bpy.data.objects.remove(tool, do_unlink=True) def apply_bevel(obj, width=EDGE_BEVEL, seg=EDGE_SEG, ang=30): m = obj.modifiers.new("bevel", 'BEVEL'); m.width = width; m.segments = seg @@ -93,7 +85,6 @@ def engrave(board, body, x, y, rotz=0, size=WORD_SIZE, dep=WORD_DEP): # ----------------------------- Aufbau ----------------------------- clear_scene() -# Runde Platte + Kartenblock (oben), beide gefast, vereinen base = cyl(R_BOARD*2, BASE_H, (0, 0, BASE_H/2), verts=160) apply_bevel(base) block = cube(CARD_BW, CARD_BD, BASE_H + CARD_BH, (0, CARD_CY, (BASE_H + CARD_BH)/2)) @@ -104,34 +95,26 @@ boolean(base, block, 'UNION') boolean(base, cyl(CHIP_D, 6, (0, 0, TOP - CHIP_DEP + 3)), 'DIFFERENCE') boolean(base, cyl(NOTCH_D, 6, (0, -CHIP_D/2, TOP - CHIP_DEP + 3)), 'DIFFERENCE') -# Sockelmulden (mit Einfuehr-Fase) +# Sockelmulden (4 Sektoren, ueber Luecken getrennt) for _, _, angles in SECTORS: for a in angles: - x = RING_R*math.cos(math.radians(a)); y = RING_R*math.sin(math.radians(a)) - boolean(base, cyl(SOCK_D, 6, (x, y, TOP - SOCK_DEP + 3)), 'DIFFERENCE') + boolean(base, cyl(SOCK_D, 6, (RING_R*math.cos(math.radians(a)), + RING_R*math.sin(math.radians(a)), TOP - SOCK_DEP + 3)), 'DIFFERENCE') -# Sektor-Trennstege (erhaben) -ri, ro = CHIP_D/2 + 3, RING_R + SOCK_D/2 + 4 -for a in DIVIDERS: - rmid = (ri+ro)/2 - r = cube(ro-ri, RIDGE_W, RIDGE_H, (rmid*math.cos(math.radians(a)), - rmid*math.sin(math.radians(a)), TOP + RIDGE_H/2)) - r.rotation_euler = (0, 0, math.radians(a)); bpy.ops.object.transform_apply(rotation=False) - boolean(base, r, 'UNION') - -# Action-Card-Schlitz (oben offen, nach hinten geneigt) +# Action-Card-Schlitz (oben offen, leicht nach hinten geneigt) slot = cube(SLOT_W, SLOT_T, 40, (0, CARD_CY, 22)) slot.rotation_euler = (math.radians(-SLOT_TILT), 0, 0); bpy.ops.object.transform_apply(rotation=True) boolean(base, slot, 'DIFFERENCE') -# Gravierte Rand-Linie (rund) +# dezente Rand-Linie rim = cyl((R_BOARD-7)*2, 1.4, (0, 0, TOP-0.7)) boolean(rim, cyl((R_BOARD-8.6)*2, 2.0, (0, 0, TOP-0.7)), 'DIFFERENCE') boolean(base, rim, 'DIFFERENCE') -# Sektor-Woerter: gleich gross, tangential um die Sockel +# Sektor-Labels (gleich gross): links/rechts vertikal, oben/unten waagerecht -> lesbar & passt for name, wc, _ in SECTORS: - rot = (wc - 90) if math.sin(math.radians(wc)) >= 0 else (wc + 90) # lesbar tangential + c = math.cos(math.radians(wc)) + rot = -90 if c > 0.7 else (90 if c < -0.7 else 0) engrave(base, name, WORD_R*math.cos(math.radians(wc)), WORD_R*math.sin(math.radians(wc)), rot) # Phasenname unten im Innenkreis @@ -143,7 +126,7 @@ except Exception: try: bpy.ops.object.shade_flat() except Exception: pass -# ----------------------------- Material (Design-Blau) ----------------------------- +# Material (Kontext-Farbe; fuer den Blank zweitrangig) mat = bpy.data.materials.new("Phase"); mat.use_nodes = True bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None) if bsdf: @@ -161,15 +144,14 @@ try: except Exception: pass bpy.ops.object.light_add(type='SUN', location=(140, -180, 260)); bpy.context.object.data.energy = 3.5 bpy.ops.object.light_add(type='AREA', location=(-140, -60, 180)) - bpy.context.object.data.energy = 5000; bpy.context.object.data.size = 260 + bpy.context.object.data.energy = 6000; bpy.context.object.data.size = 280 bpy.ops.object.empty_add(location=(0, 0, 5)); tgt = bpy.context.object - bpy.ops.object.camera_add(location=(215, -270, 250)); cam = bpy.context.object + bpy.ops.object.camera_add(location=(235, -295, 270)); cam = bpy.context.object cam.data.lens = 50 con = cam.constraints.new('TRACK_TO'); con.target = tgt con.track_axis = 'TRACK_NEGATIVE_Z'; con.up_axis = 'UP_Y' sc.camera = cam - sc.render.resolution_x, sc.render.resolution_y = 1500, 1100 - sc.render.filepath = PNG_OUT + sc.render.resolution_x, sc.render.resolution_y = 1500, 1100; sc.render.filepath = PNG_OUT try: sc.render.engine = 'BLENDER_EEVEE_NEXT' except Exception: try: sc.render.engine = 'BLENDER_EEVEE' diff --git a/04_Tablet-Quiz/app/index.html b/04_Tablet-Quiz/app/index.html index 63cd6e3..961e874 100644 --- a/04_Tablet-Quiz/app/index.html +++ b/04_Tablet-Quiz/app/index.html @@ -271,7 +271,10 @@ .classifyMain .phaseRow{display:flex;flex-wrap:wrap;justify-content:center;gap:8px;margin-top:6px} .classifyMain .phaseZone{flex:0 1 130px;max-width:150px;padding:14px 10px;font-size:13px} .slcOrient{display:flex;flex-direction:column;align-items:center;margin:6px 0 12px} - .slcDonut{width:172px;height:172px;max-width:70%} + .slcDonut{width:240px;height:240px;max-width:88%} + .slcDonut.clickable .donutSeg{cursor:pointer;transition:opacity .12s} + .slcDonut.clickable .donutSeg:hover{opacity:.82} + .slcDonut .donutSeg.bad{stroke:var(--bad);stroke-width:4} .slcCap{font-size:11px;color:var(--muted);margin-top:4px;text-align:center} @media(max-width:680px){.classifyTop{grid-template-columns:1fr}.classifyCard{max-width:300px;margin:0 auto}.classifyMain{margin-top:6px}} .choice{text-align:left;padding:12px 14px;border:1px solid var(--line);border-radius:10px;background:#fff;cursor:pointer;font-size:15px;font-weight:600} @@ -1382,22 +1385,21 @@ function renderFreigabe(){ } /* SLC-Orientierungs-Donut (5 Phasen, Farben = Phasenfarben der App) */ -function phaseDonut(){ +function phaseDonut(wrongPh, clickable){ const order=["design","transition","operation","support","review"]; const cx=100,cy=100,R=92,r=46,seg=72,start=-90-seg/2; const P=(a,rad)=>[ (cx+rad*Math.cos(a*Math.PI/180)).toFixed(1), (cy+rad*Math.sin(a*Math.PI/180)).toFixed(1) ]; - let s=""; + let segs="", labels=""; order.forEach((ph,i)=>{ const a0=start+i*seg, a1=a0+seg, o0=P(a0,R), o1=P(a1,R), i1=P(a1,r), i0=P(a0,r); - s+=``; + const cls = (clickable?"donutSeg":"") + (ph===wrongPh?" bad":""); + segs+=``; + const mid=a0+seg/2, L=P(mid,(R+r)/2); + labels+=`${PHASEN[ph].label}`; }); - order.forEach((ph,i)=>{ - const mid=start+i*seg+seg/2, L=P(mid,(R+r)/2); - s+=`${PHASEN[ph].label}`; - }); - s+=`Service-`; - s+=`Lifecycle`; - return `${s}`; + labels+=`Service-`; + labels+=`Lifecycle`; + return `${segs}${labels}`; } /* ---------- Aufgabe 3: Einstieg finden (Phase anklicken) ---------------- */ @@ -1408,9 +1410,6 @@ function renderEntry(){ const order = ["design","transition","operation","support","review"]; const cardBig = `${acard(S.service,S.change).titel}`; if(!S.entryDone){ - const zones = order.map(ph=> - ``).join(""); const hint = S.entryWrong ? `

Diese Phase passt nicht zur Change-Art — denkt an die Definition und probiert es erneut.
` : ``; $("#panel").innerHTML = ` @@ -1420,14 +1419,13 @@ function renderEntry(){
Change-Art: ${CHANGE_TYPES[S.change]} · Freigabe: ${FREIGABE_OPTIONS[FREIGABE_CORRECT[S.change]]}

Wo würde die Umsetzung starten — nachdem der Change freigegeben ist?

-

Klickt auf die Lebenszyklus-Phase, in der die Umsetzung beginnt.

+

Klickt im Ring auf die Lebenszyklus-Phase, in der die Umsetzung beginnt.

${hint} -
${phaseDonut()}
Reihenfolge im Lebenszyklus · Operation ⇄ Support laufen parallel
-
${zones}
+
${phaseDonut(S.entryWrong, true)}
Operation ⇄ Support laufen parallel
`; - $("#panel").querySelectorAll(".phaseZone").forEach(el=>{ + $("#panel").querySelectorAll(".donutSeg").forEach(el=>{ el.onclick=()=>{ const ph=el.dataset.ph; if(ph===correctPhase){ S.entryWrong=null; S.entryDone=true; } else { S.entryWrong=ph; } save(); renderEntry(); }; diff --git a/04_Tablet-Quiz/app/sw.js b/04_Tablet-Quiz/app/sw.js index a567e7d..5dfcc9d 100644 --- a/04_Tablet-Quiz/app/sw.js +++ b/04_Tablet-Quiz/app/sw.js @@ -1,5 +1,5 @@ /* Service Worker — SLC-Workshop Companion (App-Shell, offline-first) */ -const CACHE = "slc-companion-v24"; +const CACHE = "slc-companion-v25"; const SHELL = ["./", "index.html", "manifest.webmanifest", "icon.svg"]; // Action-Card-Grafiken (cards/s-c.png) fuer Offline vorab cachen (alle 24). const CARDS = [];