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 eeb29da..e774245 100644 Binary files a/01_3D-Druck/blender/__pycache__/raci-board.cpython-312.pyc and b/01_3D-Druck/blender/__pycache__/raci-board.cpython-312.pyc differ diff --git a/01_3D-Druck/blender/raci-board.py b/01_3D-Druck/blender/raci-board.py index 2fe2bdf..27811be 100644 --- a/01_3D-Druck/blender/raci-board.py +++ b/01_3D-Druck/blender/raci-board.py @@ -1,41 +1,35 @@ -# RACI-Konsolen-Board (rund) — Blender-Generator (bpy) · v2 +# RACI-Konsolen-Board (rund) — FUNKTIONS-BLANK · Blender-Generator (bpy) # SLC-Workshop Tabletop · 1 Blender-Unit = 1 mm -# Runde Puck-Flaeche, zentrale Acryl-Chip-Mulde (Ø40), 10 Sockel in 4 RACI-Sektoren -# (R3 A1 C4 I2), gleich grosse Sektor-Woerter tangential um die Sockel, Kartenhalter -# oben (Aussparung), Phasenname (DESIGN) unten im Innenkreis, Design-Blau. -# Lauf: Scripting -> 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 = [];