v20
This commit is contained in:
parent
6e1bc8d944
commit
4b031fd98a
5 changed files with 115 additions and 118 deletions
Binary file not shown.
|
|
@ -1,44 +1,50 @@
|
||||||
# RACI-Konsolen-Board — Blender-Generator (bpy)
|
# RACI-Konsolen-Board (rund) — Blender-Generator (bpy) · v2
|
||||||
# SLC-Workshop Tabletop · Einheiten: 1 Blender-Unit = 1 mm
|
# SLC-Workshop Tabletop · 1 Blender-Unit = 1 mm
|
||||||
# Baut das Board parametrisch mit gefasten Kanten (Bevel), Mulden, Sektoren,
|
# Runde Puck-Flaeche, zentrale Acryl-Chip-Mulde (Ø40), 10 Sockel in 4 RACI-Sektoren
|
||||||
# gravierten Labels, Action-Card-Steckschlitz; rendert eine Vorschau und
|
# (R3 A1 C4 I2), gleich grosse Sektor-Woerter tangential um die Sockel, Kartenhalter
|
||||||
# exportiert eine STL. Startgeruest v1 — in Blender 4.x getestet gegen die API,
|
# oben (Aussparung), Phasenname (DESIGN) unten im Innenkreis, Design-Blau.
|
||||||
# einzelne Schritte sind per try/except abgesichert (Labels optional).
|
# Lauf: Scripting -> Open -> Run | blender -b -P raci-board.py
|
||||||
#
|
|
||||||
# Lauf: Blender -> Scripting -> Open -> Run ODER blender -b -P raci-board.py
|
|
||||||
|
|
||||||
import bpy, bmesh, math, os
|
import bpy, math, os
|
||||||
from mathutils import Vector
|
|
||||||
|
|
||||||
# ----------------------------- Parameter (mm) -----------------------------
|
# ----------------------------- Parameter (mm) -----------------------------
|
||||||
BOARD_W, BOARD_D, BASE_H = 210.0, 210.0, 10.0
|
R_BOARD, BASE_H = 90.0, 12.0
|
||||||
EDGE_BEVEL, EDGE_SEG = 1.4, 4
|
EDGE_BEVEL, EDGE_SEG = 1.6, 4
|
||||||
|
|
||||||
DIAL_CX, DIAL_CY = 0.0, -15.0
|
|
||||||
CHIP_D, CHIP_DEP = 40.6, 1.8 # Acryl-Chip Ø40 x 2 mm
|
CHIP_D, CHIP_DEP = 40.6, 1.8 # Acryl-Chip Ø40 x 2 mm
|
||||||
NOTCH_D = 12.0
|
NOTCH_D = 12.0
|
||||||
|
|
||||||
SOCK_D, SOCK_DEP = 25.3, 1.5 # Figuren-Sockel Ø24,5
|
SOCK_D, SOCK_DEP = 25.3, 1.5 # Figuren-Sockel Ø24,5
|
||||||
RING_R = 48.0
|
SOCK_LEAD = 0.6
|
||||||
N_SOCK = 10 # Winkel ab oben (90°), 36°-Teilung
|
RING_R = 64.0
|
||||||
RIDGE_H, RIDGE_W = 2.6, 3.2
|
|
||||||
DIVIDERS = [72, -72, -144, 108]
|
|
||||||
# Sektor-Mitten (fuer grosse Buchstaben) R3 A1 C4 I2
|
|
||||||
LETTERS = [(90, "A"), (0, "C"), (-108, "I"), (162, "R")]
|
|
||||||
|
|
||||||
CARD_CY = 78.0
|
PHASE_NAME = "DESIGN"
|
||||||
CARD_BW, CARD_BD, CARD_BH = 84.0, 22.0, 16.0
|
PHASE_COLOR = (0.184, 0.502, 0.788, 1) # #2f80c9 Design-Blau
|
||||||
|
|
||||||
|
# Sektoren: Name, Wort-Mittenwinkel, Sockel-Winkel (Grad, 90=oben). Top frei fuer Karte.
|
||||||
|
SECTORS = [
|
||||||
|
("RESPONSIBLE", 165, [135, 165, 195]),
|
||||||
|
("ACCOUNTABLE", 50, [50]),
|
||||||
|
("CONSULTED", -20, [25, -5, -35, -65]),
|
||||||
|
("INFORMED", -110, [-95, -125]),
|
||||||
|
]
|
||||||
|
DIVIDERS = [37.5, -80, -145] # Grenzen zwischen Sektoren (nicht im Karten-Spalt)
|
||||||
|
RIDGE_H, RIDGE_W = 2.6, 3.4
|
||||||
|
|
||||||
|
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
|
||||||
SLOT_W, SLOT_T, SLOT_TILT = 63.0, 4.0, 12.0
|
SLOT_W, SLOT_T, SLOT_TILT = 63.0, 4.0, 12.0
|
||||||
|
|
||||||
LET_SIZE, LET_DEP = 11.0, 1.0
|
|
||||||
WORD_SIZE, WORD_DEP = 5.0, 0.8
|
|
||||||
TOP = BASE_H
|
TOP = BASE_H
|
||||||
|
|
||||||
def _outdir():
|
def _outdir():
|
||||||
d = os.path.dirname(bpy.data.filepath) # gesetzt, wenn .blend gespeichert
|
d = os.path.dirname(bpy.data.filepath)
|
||||||
if d: return d
|
if d: return d
|
||||||
try: return os.path.dirname(os.path.abspath(__file__))
|
try: return os.path.dirname(os.path.abspath(__file__))
|
||||||
except NameError: return os.path.expanduser("~") # Fallback: Benutzerordner
|
except NameError: return os.path.expanduser("~")
|
||||||
HERE = _outdir()
|
HERE = _outdir()
|
||||||
STL_OUT = os.path.join(HERE, "raci-board.stl")
|
STL_OUT = os.path.join(HERE, "raci-board.stl")
|
||||||
PNG_OUT = os.path.join(HERE, "raci_preview.png")
|
PNG_OUT = os.path.join(HERE, "raci_preview.png")
|
||||||
|
|
@ -46,8 +52,7 @@ print("Ausgabe-Ordner:", HERE)
|
||||||
|
|
||||||
# ----------------------------- Helfer -----------------------------
|
# ----------------------------- Helfer -----------------------------
|
||||||
def clear_scene():
|
def clear_scene():
|
||||||
bpy.ops.object.select_all(action='SELECT')
|
bpy.ops.object.select_all(action='SELECT'); bpy.ops.object.delete(use_global=False)
|
||||||
bpy.ops.object.delete(use_global=False)
|
|
||||||
for blk in (bpy.data.meshes, bpy.data.materials, bpy.data.curves):
|
for blk in (bpy.data.meshes, bpy.data.materials, bpy.data.curves):
|
||||||
for d in list(blk):
|
for d in list(blk):
|
||||||
if d.users == 0: blk.remove(d)
|
if d.users == 0: blk.remove(d)
|
||||||
|
|
@ -55,16 +60,14 @@ def clear_scene():
|
||||||
def cube(sx, sy, sz, loc):
|
def cube(sx, sy, sz, loc):
|
||||||
bpy.ops.mesh.primitive_cube_add(size=1, location=loc)
|
bpy.ops.mesh.primitive_cube_add(size=1, location=loc)
|
||||||
o = bpy.context.object; o.scale = (sx, sy, sz)
|
o = bpy.context.object; o.scale = (sx, sy, sz)
|
||||||
bpy.ops.object.transform_apply(scale=True)
|
bpy.ops.object.transform_apply(scale=True); return o
|
||||||
return o
|
|
||||||
|
|
||||||
def cyl(d, h, loc, verts=96):
|
def cyl(d, h, loc, verts=96):
|
||||||
bpy.ops.mesh.primitive_cylinder_add(radius=d/2.0, depth=h, location=loc, vertices=verts)
|
bpy.ops.mesh.primitive_cylinder_add(radius=d/2.0, depth=h, location=loc, vertices=verts)
|
||||||
return bpy.context.object
|
return bpy.context.object
|
||||||
|
|
||||||
def boolean(obj, tool, op='DIFFERENCE'):
|
def boolean(obj, tool, op='DIFFERENCE'):
|
||||||
m = obj.modifiers.new("bool", 'BOOLEAN'); m.operation = op
|
m = obj.modifiers.new("bool", 'BOOLEAN'); m.operation = op; m.object = tool
|
||||||
m.object = tool
|
|
||||||
try: m.solver = 'EXACT'
|
try: m.solver = 'EXACT'
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
bpy.context.view_layer.objects.active = obj
|
bpy.context.view_layer.objects.active = obj
|
||||||
|
|
@ -72,145 +75,115 @@ def boolean(obj, tool, op='DIFFERENCE'):
|
||||||
bpy.data.objects.remove(tool, do_unlink=True)
|
bpy.data.objects.remove(tool, do_unlink=True)
|
||||||
|
|
||||||
def apply_bevel(obj, width=EDGE_BEVEL, seg=EDGE_SEG, ang=30):
|
def apply_bevel(obj, width=EDGE_BEVEL, seg=EDGE_SEG, ang=30):
|
||||||
m = obj.modifiers.new("bevel", 'BEVEL')
|
m = obj.modifiers.new("bevel", 'BEVEL'); m.width = width; m.segments = seg
|
||||||
m.width = width; m.segments = seg
|
|
||||||
m.limit_method = 'ANGLE'; m.angle_limit = math.radians(ang)
|
m.limit_method = 'ANGLE'; m.angle_limit = math.radians(ang)
|
||||||
bpy.context.view_layer.objects.active = obj
|
bpy.context.view_layer.objects.active = obj
|
||||||
bpy.ops.object.modifier_apply(modifier=m.name)
|
bpy.ops.object.modifier_apply(modifier=m.name)
|
||||||
|
|
||||||
def cut_text(board, body, x, y, rotz=0, size=LET_SIZE, dep=LET_DEP):
|
def engrave(board, body, x, y, rotz=0, size=WORD_SIZE, dep=WORD_DEP):
|
||||||
try:
|
try:
|
||||||
bpy.ops.object.text_add(location=(x, y, TOP - dep))
|
bpy.ops.object.text_add(location=(x, y, TOP - dep))
|
||||||
t = bpy.context.object; t.data.body = body
|
t = bpy.context.object; t.data.body = body; t.data.size = size
|
||||||
t.data.size = size; t.data.extrude = dep + 1.5
|
t.data.extrude = dep + 1.5; t.data.align_x='CENTER'; t.data.align_y='CENTER'
|
||||||
t.data.align_x = 'CENTER'; t.data.align_y = 'CENTER'
|
|
||||||
t.rotation_euler = (0, 0, math.radians(rotz))
|
t.rotation_euler = (0, 0, math.radians(rotz))
|
||||||
bpy.ops.object.convert(target='MESH')
|
bpy.ops.object.convert(target='MESH'); boolean(board, t, 'DIFFERENCE')
|
||||||
boolean(board, t, 'DIFFERENCE')
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Label uebersprungen (%s): %s" % (body, e))
|
print("Label uebersprungen (%s): %s" % (body, e))
|
||||||
|
|
||||||
# ----------------------------- Aufbau -----------------------------
|
# ----------------------------- Aufbau -----------------------------
|
||||||
clear_scene()
|
clear_scene()
|
||||||
|
|
||||||
# Basisplatte + Kartenblock, beide gefast, dann vereinen
|
# Runde Platte + Kartenblock (oben), beide gefast, vereinen
|
||||||
base = cube(BOARD_W, BOARD_D, BASE_H, (0, 0, BASE_H/2))
|
base = cyl(R_BOARD*2, BASE_H, (0, 0, BASE_H/2), verts=160)
|
||||||
apply_bevel(base)
|
apply_bevel(base)
|
||||||
block = cube(CARD_BW, CARD_BD, BASE_H + CARD_BH, (0, CARD_CY, (BASE_H + CARD_BH)/2))
|
block = cube(CARD_BW, CARD_BD, BASE_H + CARD_BH, (0, CARD_CY, (BASE_H + CARD_BH)/2))
|
||||||
apply_bevel(block, width=1.0)
|
apply_bevel(block, width=1.0)
|
||||||
boolean(base, block, 'UNION')
|
boolean(base, block, 'UNION')
|
||||||
|
|
||||||
# Gefaste Ecke vorne rechts (Optik)
|
|
||||||
cc = cube(28, 28, BASE_H + 4, (BOARD_W/2, -BOARD_D/2, BASE_H/2))
|
|
||||||
cc.rotation_euler = (0, 0, math.radians(45)); bpy.ops.object.transform_apply(rotation=True)
|
|
||||||
boolean(base, cc, 'DIFFERENCE')
|
|
||||||
|
|
||||||
# Chip-Mulde + Greifkerbe
|
# Chip-Mulde + Greifkerbe
|
||||||
boolean(base, cyl(CHIP_D, 6, (DIAL_CX, DIAL_CY, TOP - CHIP_DEP + 3)), 'DIFFERENCE')
|
boolean(base, cyl(CHIP_D, 6, (0, 0, TOP - CHIP_DEP + 3)), 'DIFFERENCE')
|
||||||
boolean(base, cyl(NOTCH_D, 6, (DIAL_CX, DIAL_CY - CHIP_D/2, TOP - CHIP_DEP + 3)), 'DIFFERENCE')
|
boolean(base, cyl(NOTCH_D, 6, (0, -CHIP_D/2, TOP - CHIP_DEP + 3)), 'DIFFERENCE')
|
||||||
|
|
||||||
# 10 Sockelmulden im Ring
|
# Sockelmulden (mit Einfuehr-Fase)
|
||||||
for i in range(N_SOCK):
|
for _, _, angles in SECTORS:
|
||||||
a = math.radians(90 - i * 36)
|
for a in angles:
|
||||||
x = DIAL_CX + RING_R * math.cos(a); y = DIAL_CY + RING_R * math.sin(a)
|
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, (x, y, TOP - SOCK_DEP + 3)), 'DIFFERENCE')
|
||||||
|
|
||||||
# Deko-Rillen im Mittelfeld
|
|
||||||
for rr in [24, 27, 30, 33]:
|
|
||||||
ring = cyl(rr*2, 0.8, (DIAL_CX, DIAL_CY, TOP - 0.25))
|
|
||||||
inner = cyl((rr-0.8)*2, 1.2, (DIAL_CX, DIAL_CY, TOP - 0.25))
|
|
||||||
boolean(ring, inner, 'DIFFERENCE')
|
|
||||||
boolean(base, ring, 'DIFFERENCE')
|
|
||||||
|
|
||||||
# Sektor-Trennstege (erhaben)
|
# Sektor-Trennstege (erhaben)
|
||||||
ri, ro = CHIP_D/2 + 3, RING_R + SOCK_D/2 + 4
|
ri, ro = CHIP_D/2 + 3, RING_R + SOCK_D/2 + 4
|
||||||
for a in DIVIDERS:
|
for a in DIVIDERS:
|
||||||
r = cube(ro - ri, RIDGE_W, RIDGE_H, ((ri+ro)/2, 0, TOP + RIDGE_H/2))
|
rmid = (ri+ro)/2
|
||||||
r.rotation_euler = (0, 0, math.radians(a))
|
r = cube(ro-ri, RIDGE_W, RIDGE_H, (rmid*math.cos(math.radians(a)),
|
||||||
# um Dial-Zentrum rotieren: erst an Zentrum verschieben
|
rmid*math.sin(math.radians(a)), TOP + RIDGE_H/2))
|
||||||
r.location = (DIAL_CX + ((ri+ro)/2)*math.cos(math.radians(a)),
|
r.rotation_euler = (0, 0, math.radians(a)); bpy.ops.object.transform_apply(rotation=False)
|
||||||
DIAL_CY + ((ri+ro)/2)*math.sin(math.radians(a)), TOP + RIDGE_H/2)
|
|
||||||
bpy.ops.object.transform_apply(rotation=False)
|
|
||||||
boolean(base, r, 'UNION')
|
boolean(base, r, 'UNION')
|
||||||
|
|
||||||
# Action-Card-Steckschlitz (oben offen, nach hinten geneigt)
|
# Action-Card-Schlitz (oben offen, nach hinten geneigt)
|
||||||
slot = cube(SLOT_W, SLOT_T, 38, (0, CARD_CY, 22))
|
slot = cube(SLOT_W, SLOT_T, 40, (0, CARD_CY, 22))
|
||||||
slot.rotation_euler = (math.radians(-SLOT_TILT), 0, 0)
|
slot.rotation_euler = (math.radians(-SLOT_TILT), 0, 0); bpy.ops.object.transform_apply(rotation=True)
|
||||||
bpy.ops.object.transform_apply(rotation=True)
|
|
||||||
boolean(base, slot, 'DIFFERENCE')
|
boolean(base, slot, 'DIFFERENCE')
|
||||||
|
|
||||||
# Gravierte Rand-Linie
|
# Gravierte Rand-Linie (rund)
|
||||||
outer = cube(BOARD_W-14, BOARD_D-14, 1.4, (0, 0, TOP-0.7))
|
rim = cyl((R_BOARD-7)*2, 1.4, (0, 0, TOP-0.7))
|
||||||
inner = cube(BOARD_W-17, BOARD_D-17, 2.0, (0, 0, TOP-0.7))
|
boolean(rim, cyl((R_BOARD-8.6)*2, 2.0, (0, 0, TOP-0.7)), 'DIFFERENCE')
|
||||||
boolean(outer, inner, 'DIFFERENCE'); boolean(base, outer, 'DIFFERENCE')
|
boolean(base, rim, 'DIFFERENCE')
|
||||||
|
|
||||||
# Labels: grosse R/A/C/I + Woerter
|
# Sektor-Woerter: gleich gross, tangential um die Sockel
|
||||||
for ang, ch in LETTERS:
|
for name, wc, _ in SECTORS:
|
||||||
rl = RING_R + 13
|
rot = (wc - 90) if math.sin(math.radians(wc)) >= 0 else (wc + 90) # lesbar tangential
|
||||||
cut_text(base, ch, DIAL_CX + rl*math.cos(math.radians(ang)),
|
engrave(base, name, WORD_R*math.cos(math.radians(wc)), WORD_R*math.sin(math.radians(wc)), rot)
|
||||||
DIAL_CY + rl*math.sin(math.radians(ang)))
|
|
||||||
cut_text(base, "RESPONSIBLE", -(RING_R+29), DIAL_CY+6, 90, WORD_SIZE, WORD_DEP)
|
# Phasenname unten im Innenkreis
|
||||||
cut_text(base, "CONSULTED", (RING_R+29), DIAL_CY+6, -90, WORD_SIZE, WORD_DEP)
|
engrave(base, PHASE_NAME, DESIGN_POS[0], DESIGN_POS[1], 0, DESIGN_SIZE, DESIGN_DEP)
|
||||||
cut_text(base, "INFORMED", DIAL_CX, DIAL_CY-(RING_R+30), 0, WORD_SIZE, WORD_DEP)
|
|
||||||
cut_text(base, "ACCOUNTABLE", DIAL_CX, CARD_CY - CARD_BD/2 - 8, 0, WORD_SIZE, WORD_DEP)
|
|
||||||
|
|
||||||
base.name = "RACI-Board"
|
base.name = "RACI-Board"
|
||||||
# Auto-Smooth: flache Flaechen scharf, gefaste Kanten glatt
|
try: bpy.ops.object.shade_auto_smooth(angle=math.radians(30))
|
||||||
try:
|
|
||||||
bpy.ops.object.shade_auto_smooth(angle=math.radians(30)) # Blender >= 4.1
|
|
||||||
except Exception:
|
except Exception:
|
||||||
try: bpy.ops.object.shade_flat()
|
try: bpy.ops.object.shade_flat()
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
|
|
||||||
# ----------------------------- Material -----------------------------
|
# ----------------------------- Material (Design-Blau) -----------------------------
|
||||||
mat = bpy.data.materials.new("BoardBlue"); mat.use_nodes = True
|
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)
|
bsdf = next((n for n in mat.node_tree.nodes if n.type == 'BSDF_PRINCIPLED'), None)
|
||||||
if bsdf:
|
if bsdf:
|
||||||
bsdf.inputs["Base Color"].default_value = (0.09, 0.15, 0.30, 1) # dunkelblau
|
bsdf.inputs["Base Color"].default_value = PHASE_COLOR
|
||||||
try: bsdf.inputs["Roughness"].default_value = 0.5
|
try: bsdf.inputs["Roughness"].default_value = 0.5
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
base.data.materials.clear(); base.data.materials.append(mat)
|
base.data.materials.clear(); base.data.materials.append(mat)
|
||||||
|
|
||||||
# ----------------------------- Vorschau-Render (guarded) -----------------------------
|
# ----------------------------- Vorschau-Render -----------------------------
|
||||||
try:
|
try:
|
||||||
sc = bpy.context.scene
|
sc = bpy.context.scene
|
||||||
# Welt etwas aufhellen
|
|
||||||
try:
|
try:
|
||||||
wn = sc.world.node_tree.nodes
|
bg = next((n for n in sc.world.node_tree.nodes if n.type == 'BACKGROUND'), None)
|
||||||
bg = next((n for n in wn if n.type == 'BACKGROUND'), None)
|
|
||||||
if bg: bg.inputs[1].default_value = 1.2
|
if bg: bg.inputs[1].default_value = 1.2
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
# Licht
|
bpy.ops.object.light_add(type='SUN', location=(140, -180, 260)); bpy.context.object.data.energy = 3.5
|
||||||
bpy.ops.object.light_add(type='SUN', location=(120, -160, 240))
|
bpy.ops.object.light_add(type='AREA', location=(-140, -60, 180))
|
||||||
bpy.context.object.data.energy = 3.5
|
bpy.context.object.data.energy = 5000; bpy.context.object.data.size = 260
|
||||||
bpy.ops.object.light_add(type='AREA', location=(-120, -60, 160))
|
bpy.ops.object.empty_add(location=(0, 0, 5)); tgt = bpy.context.object
|
||||||
bpy.context.object.data.energy = 4000; bpy.context.object.data.size = 200
|
bpy.ops.object.camera_add(location=(215, -270, 250)); cam = bpy.context.object
|
||||||
# Kamera blickt aufs Board-Zentrum (Track-To), rahmt das ganze Board
|
cam.data.lens = 50
|
||||||
bpy.ops.object.empty_add(location=(0, 0, 4)); tgt = bpy.context.object
|
|
||||||
bpy.ops.object.camera_add(location=(175, -220, 210)); cam = bpy.context.object
|
|
||||||
cam.data.lens = 52
|
|
||||||
con = cam.constraints.new('TRACK_TO'); con.target = tgt
|
con = cam.constraints.new('TRACK_TO'); con.target = tgt
|
||||||
con.track_axis = 'TRACK_NEGATIVE_Z'; con.up_axis = 'UP_Y'
|
con.track_axis = 'TRACK_NEGATIVE_Z'; con.up_axis = 'UP_Y'
|
||||||
sc.camera = cam
|
sc.camera = cam
|
||||||
sc.render.resolution_x, sc.render.resolution_y = 1500, 1050
|
sc.render.resolution_x, sc.render.resolution_y = 1500, 1100
|
||||||
sc.render.filepath = PNG_OUT
|
sc.render.filepath = PNG_OUT
|
||||||
try: sc.render.engine = 'BLENDER_EEVEE_NEXT'
|
try: sc.render.engine = 'BLENDER_EEVEE_NEXT'
|
||||||
except Exception:
|
except Exception:
|
||||||
try: sc.render.engine = 'BLENDER_EEVEE'
|
try: sc.render.engine = 'BLENDER_EEVEE'
|
||||||
except Exception: pass
|
except Exception: pass
|
||||||
bpy.ops.render.render(write_still=True)
|
bpy.ops.render.render(write_still=True); print("Vorschau:", PNG_OUT)
|
||||||
print("Vorschau:", PNG_OUT)
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("Render uebersprungen:", e)
|
print("Render uebersprungen:", e)
|
||||||
|
|
||||||
# ----------------------------- STL-Export (versionstolerant) -----------------------------
|
# ----------------------------- STL-Export -----------------------------
|
||||||
bpy.ops.object.select_all(action='DESELECT')
|
bpy.ops.object.select_all(action='DESELECT'); base.select_set(True)
|
||||||
base.select_set(True); bpy.context.view_layer.objects.active = base
|
bpy.context.view_layer.objects.active = base
|
||||||
try:
|
try:
|
||||||
bpy.ops.wm.stl_export(filepath=STL_OUT, export_selected_objects=True) # Blender >= 4.1
|
bpy.ops.wm.stl_export(filepath=STL_OUT, export_selected_objects=True)
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try: bpy.ops.export_mesh.stl(filepath=STL_OUT, use_selection=True)
|
||||||
bpy.ops.export_mesh.stl(filepath=STL_OUT, use_selection=True) # Blender <= 4.0
|
except Exception as e: print("STL-Export manuell noetig:", e)
|
||||||
except Exception as e:
|
|
||||||
print("STL-Export fehlgeschlagen — bitte manuell File>Export>STL:", e)
|
|
||||||
print("STL:", STL_OUT)
|
print("STL:", STL_OUT)
|
||||||
|
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 813 KiB After Width: | Height: | Size: 1,016 KiB |
|
|
@ -268,7 +268,11 @@
|
||||||
.classifyTop{display:grid;grid-template-columns:minmax(220px,320px) 1fr;gap:26px;align-items:start;margin:18px 0 6px}
|
.classifyTop{display:grid;grid-template-columns:minmax(220px,320px) 1fr;gap:26px;align-items:start;margin:18px 0 6px}
|
||||||
.classifyCard{display:block;width:100%;border-radius:12px;box-shadow:0 3px 16px rgba(0,0,0,.16)}
|
.classifyCard{display:block;width:100%;border-radius:12px;box-shadow:0 3px 16px rgba(0,0,0,.16)}
|
||||||
.classifyMain{min-width:0}
|
.classifyMain{min-width:0}
|
||||||
.classifyMain .phaseRow{grid-template-columns:repeat(2,1fr);margin-top:12px}
|
.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%}
|
||||||
|
.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}}
|
@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}
|
.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}
|
||||||
.choice:hover{border-color:var(--ink)}
|
.choice:hover{border-color:var(--ink)}
|
||||||
|
|
@ -1377,6 +1381,25 @@ function renderFreigabe(){
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* SLC-Orientierungs-Donut (5 Phasen, Farben = Phasenfarben der App) */
|
||||||
|
function phaseDonut(){
|
||||||
|
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="";
|
||||||
|
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})"/>`;
|
||||||
|
});
|
||||||
|
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>`;
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------- Aufgabe 3: Einstieg finden (Phase anklicken) ---------------- */
|
/* ---------- Aufgabe 3: Einstieg finden (Phase anklicken) ---------------- */
|
||||||
function renderEntry(){
|
function renderEntry(){
|
||||||
const rec = START_EMPFEHLUNG[S.change];
|
const rec = START_EMPFEHLUNG[S.change];
|
||||||
|
|
@ -1399,6 +1422,7 @@ function renderEntry(){
|
||||||
<h2 class="setupTitle" style="margin-top:8px">Wo würde die Umsetzung starten — nachdem der Change freigegeben ist?</h2>
|
<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 auf die Lebenszyklus-Phase, in der die Umsetzung beginnt.</p>
|
||||||
${hint}
|
${hint}
|
||||||
|
<div class="slcOrient">${phaseDonut()}<div class="slcCap">Reihenfolge im Lebenszyklus · Operation ⇄ Support laufen parallel</div></div>
|
||||||
<div class="phaseRow">${zones}</div>
|
<div class="phaseRow">${zones}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
/* Service Worker — SLC-Workshop Companion (App-Shell, offline-first) */
|
/* Service Worker — SLC-Workshop Companion (App-Shell, offline-first) */
|
||||||
const CACHE = "slc-companion-v23";
|
const CACHE = "slc-companion-v24";
|
||||||
const SHELL = ["./", "index.html", "manifest.webmanifest", "icon.svg"];
|
const SHELL = ["./", "index.html", "manifest.webmanifest", "icon.svg"];
|
||||||
// Action-Card-Grafiken (cards/s<service>-c<change>.png) fuer Offline vorab cachen (alle 24).
|
// Action-Card-Grafiken (cards/s<service>-c<change>.png) fuer Offline vorab cachen (alle 24).
|
||||||
const CARDS = [];
|
const CARDS = [];
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue