v21
This commit is contained in:
parent
4b031fd98a
commit
e7e7912211
5 changed files with 55 additions and 70 deletions
|
|
@ -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).
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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+=`<path d="M${o0[0]} ${o0[1]} A${R} ${R} 0 0 1 ${o1[0]} ${o1[1]} L${i1[0]} ${i1[1]} A${r} ${r} 0 0 0 ${i0[0]} ${i0[1]} Z" fill="var(--${ph})"/>`;
|
||||
const cls = (clickable?"donutSeg":"") + (ph===wrongPh?" bad":"");
|
||||
segs+=`<path class="${cls}" data-ph="${ph}" d="M${o0[0]} ${o0[1]} A${R} ${R} 0 0 1 ${o1[0]} ${o1[1]} L${i1[0]} ${i1[1]} A${r} ${r} 0 0 0 ${i0[0]} ${i0[1]} Z" fill="var(--${ph})"/>`;
|
||||
const mid=a0+seg/2, L=P(mid,(R+r)/2);
|
||||
labels+=`<text x="${L[0]}" y="${(+L[1]+3).toFixed(1)}" text-anchor="middle" font-size="9.5" font-weight="700" fill="#fff">${PHASEN[ph].label}</text>`;
|
||||
});
|
||||
order.forEach((ph,i)=>{
|
||||
const mid=start+i*seg+seg/2, L=P(mid,(R+r)/2);
|
||||
s+=`<text x="${L[0]}" y="${(+L[1]+3).toFixed(1)}" text-anchor="middle" font-size="9.5" font-weight="700" fill="#fff">${PHASEN[ph].label}</text>`;
|
||||
});
|
||||
s+=`<text x="100" y="96" text-anchor="middle" font-size="9" font-weight="700" fill="var(--muted)">Service-</text>`;
|
||||
s+=`<text x="100" y="108" text-anchor="middle" font-size="9" font-weight="700" fill="var(--muted)">Lifecycle</text>`;
|
||||
return `<svg viewBox="0 0 200 200" class="slcDonut" role="img" aria-label="Service-Lifecycle: Design, Transition, Operation und Support (parallel), Review">${s}</svg>`;
|
||||
labels+=`<text x="100" y="96" text-anchor="middle" font-size="9" font-weight="700" fill="var(--muted)">Service-</text>`;
|
||||
labels+=`<text x="100" y="108" text-anchor="middle" font-size="9" font-weight="700" fill="var(--muted)">Lifecycle</text>`;
|
||||
return `<svg viewBox="0 0 200 200" class="slcDonut${clickable?' clickable':''}" role="img" aria-label="Service-Lifecycle: Design, Transition, Operation und Support (parallel), Review">${segs}<g pointer-events="none">${labels}</g></svg>`;
|
||||
}
|
||||
|
||||
/* ---------- Aufgabe 3: Einstieg finden (Phase anklicken) ---------------- */
|
||||
|
|
@ -1408,9 +1410,6 @@ function renderEntry(){
|
|||
const order = ["design","transition","operation","support","review"];
|
||||
const cardBig = `<img class="classifyCard" src="cards/s${S.service}-c${S.change}.png" alt="${acard(S.service,S.change).titel}">`;
|
||||
if(!S.entryDone){
|
||||
const zones = order.map(ph=>
|
||||
`<button class="phaseZone ${S.entryWrong===ph?'bad':''}" data-ph="${ph}"
|
||||
style="background:${PHASEN[ph].color}">${PHASEN[ph].label}</button>`).join("");
|
||||
const hint = S.entryWrong
|
||||
? `<div class="hint bad">Diese Phase passt nicht zur Change-Art — denkt an die Definition und probiert es erneut.</div>` : ``;
|
||||
$("#panel").innerHTML = `
|
||||
|
|
@ -1420,14 +1419,13 @@ function renderEntry(){
|
|||
<div class="classifyMain">
|
||||
<div class="hint ok">Change-Art: ${CHANGE_TYPES[S.change]} · Freigabe: ${FREIGABE_OPTIONS[FREIGABE_CORRECT[S.change]]}</div>
|
||||
<h2 class="setupTitle" style="margin-top:8px">Wo würde die Umsetzung starten — nachdem der Change freigegeben ist?</h2>
|
||||
<p class="muted">Klickt auf die Lebenszyklus-Phase, in der die Umsetzung beginnt.</p>
|
||||
<p class="muted">Klickt <b>im Ring</b> auf die Lebenszyklus-Phase, in der die Umsetzung beginnt.</p>
|
||||
${hint}
|
||||
<div class="slcOrient">${phaseDonut()}<div class="slcCap">Reihenfolge im Lebenszyklus · Operation ⇄ Support laufen parallel</div></div>
|
||||
<div class="phaseRow">${zones}</div>
|
||||
<div class="slcOrient">${phaseDonut(S.entryWrong, true)}<div class="slcCap">Operation ⇄ Support laufen parallel</div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="actions"><button class="ghost" id="backFreigabe">← zurück</button></div>`;
|
||||
$("#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(); };
|
||||
|
|
|
|||
|
|
@ -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<service>-c<change>.png) fuer Offline vorab cachen (alle 24).
|
||||
const CARDS = [];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue