diff --git a/01_3D-Druck/openscad/artefakt-token.scad b/01_3D-Druck/openscad/artefakt-token.scad new file mode 100644 index 0000000..cc75290 --- /dev/null +++ b/01_3D-Druck/openscad/artefakt-token.scad @@ -0,0 +1,56 @@ +// Artefakt-Token (Service-Akte) — gedruckte "Karte" statt Pappkarte +// SLC-Workshop Tabletop · Einheiten: mm +// Kleine Tile mit gravierter A-Nummer; wird in einen Slot des Artefakt-Trays +// (artefakt-tray.scad) gelegt. Eingefaerbt in der PHASENFARBE der erzeugenden +// Phase (Design/Transition/Operation/Support/Review) -> Farbe via Filament. +// +// "Lebende" Artefakte (A2 Service-Definition, A11 Problem Record, +// A13 Wissensdatenbank) wachsen sichtbar: Basis-Token + je Aktualisierung +// eine duenne Status-Platte oben drauf (Entwurf -> Final -> Aktualisiert). +// +// part = "token" -> ein Artefakt-Token (Nummer via tok_label) +// "plate" -> eine Status-Platte zum Aufstapeln (lebende Artefakte) + +part = "token"; // "token" | "plate" + +/* [Token] */ +tok_w = 30; // Breite +tok_d = 20; // Tiefe +tok_h = 4; // Hoehe (Basis-Token) +tok_r = 2.5; // Eckenradius +tok_label = "A2"; // gravierte A-Nummer +lab_size = 11; // Schriftgroesse A-Nummer +lab_depth = 0.8; // Gravurtiefe + +/* [Status-Platte] (lebende Artefakte) */ +plate_h = 2.5; // Hoehe je Aktualisierungs-Platte + +$fn = 48; + +module rrect(l, w, h, r) { + linear_extrude(h) offset(r) offset(-r) square([l, w], center = true); +} + +module token(label) { + difference() { + rrect(tok_w, tok_d, tok_h, tok_r); + translate([0, 0, tok_h - lab_depth]) + linear_extrude(lab_depth + 0.1) + text(label, size = lab_size, halign = "center", valign = "center"); + } +} + +// duenne Platte zum Aufstapeln; kleine Mittenrille als Status-Markierung +module status_plate() { + difference() { + rrect(tok_w, tok_d, plate_h, tok_r); + translate([0, 0, plate_h - 0.5]) + linear_extrude(0.6) + square([tok_w - 10, 1.2], center = true); + } +} + +if (part == "plate") status_plate(); +else token(tok_label); + +echo(part = part, tok = [tok_w, tok_d, tok_h], plate_h = plate_h); diff --git a/01_3D-Druck/openscad/artefakt-tray.scad b/01_3D-Druck/openscad/artefakt-tray.scad new file mode 100644 index 0000000..741013d --- /dev/null +++ b/01_3D-Druck/openscad/artefakt-tray.scad @@ -0,0 +1,107 @@ +// Artefakt-Tray (Service-Akte) — 3D-Aufnahme fuer die 15 Artefakt-Token +// SLC-Workshop Tabletop · Einheiten: mm +// Flaches Tableau mit 15 beschrifteten Steck-Slots (A1-A15), in 5 PHASEN-Reihen +// gruppiert (Design -> Transition -> Operation -> Support -> Review). Liegt neben +// der aktuellen Station und wandert mit. Die Token (artefakt-token.scad) tragen +// die Phasenfarbe; der Tray selbst ist EINFARBIG (Beschriftung via Gravur). +// +// Mechanik: +// - Artefakt erzeugt -> Token in seinen Slot legen. +// - "Lebende" Artefakte (A2/A11/A13, hier mit eingraviertem Zusatz-Rahmen +// markiert): Status-Platten oben aufstapeln -> Stapel waechst sichtbar. +// - Gate-Kopplung bleibt REGEL: ein Gate "oeffnet" nur, wenn die geforderten +// Token im Tray liegen (keine Mechanik am Gate-Puck). + +/* [Platte] */ +plate_thick = 6; // Dicke +margin = 8; // Aussenrand +corner_r = 5; + +/* [Slots] (Token Ø 30x20 wird REINGELEGT) */ +slot_w = 31; // Slot-Innenbreite (Token 30 + Spiel) +slot_d = 21; // Slot-Innentiefe (Token 20 + Spiel) +pocket_dep = 3.0; // Vertiefung (Token 4 hoch -> steht ~1 mm vor = greifbar) +pocket_r = 2.5; +gap_x = 7; // Abstand zwischen Slots in einer Reihe +row_gap = 8; // Abstand zwischen den Phasen-Reihen +header_h = 9; // Hoehe der Phasen-Kopfzeile ueber den Slots +finger_r = 6; // Greifkerbe an der Slot-Unterkante + +/* [Gravur] */ +num_size = 7; // A-Nummer im Slot-Boden +num_depth = 0.8; +hdr_size = 6; // Phasen-Name +hdr_depth = 0.8; +lf_inset = 3; // "lebend"-Rahmen: Abstand zum Slot-Rand +lf_w = 1.2; // Strichstaerke +lf_depth = 0.6; + +$fn = 48; + +// [Phasen-Reihe, [[A-Nummer, lebend?], ...]] — Reihenfolge = Lebenszyklus +ROWS = [ + ["DESIGN", [["A1", false], ["A2", true], ["A3", false], ["A4", false]]], + ["TRANSITION", [["A5", false], ["A6", false], ["A7", false], ["A8", false]]], + ["OPERATION", [["A9", false]]], + ["SUPPORT", [["A10", false], ["A11", true], ["A12", false], ["A13", true]]], + ["REVIEW", [["A14", false], ["A15", false]]] +]; +cols_max = 4; +n_rows = len(ROWS); + +// abgeleitete Maße +content_w = cols_max * slot_w + (cols_max - 1) * gap_x; // 145 +plate_w = content_w + 2 * margin; // 161 +block_h = header_h + slot_d; // 30 +content_h = n_rows * block_h + (n_rows - 1) * row_gap; // 182 +plate_h = content_h + 2 * margin; // 198 + +// Platz von Ecke (0,0) aus; y oben = plate_h +function blockTopY(r) = plate_h - margin - r * (block_h + row_gap); +function slotCenterY(r) = blockTopY(r) - header_h - slot_d/2; +function slotCenterX(c) = margin + slot_w/2 + c * (slot_w + gap_x); + +module rrect(l, w, h, r) { + linear_extrude(h) offset(r) offset(-r) square([l, w], center = true); +} + +module pocket(cx, cy, label, live) { + // Vertiefung + translate([cx, cy, plate_thick - pocket_dep]) + rrect(slot_w, slot_d, pocket_dep + 0.1, pocket_r); + // Greifkerbe an der Unterkante + translate([cx, cy - slot_d/2, plate_thick - pocket_dep]) + cylinder(r = finger_r, h = pocket_dep + 0.1); + // A-Nummer im Boden (sichtbar bei leerem Slot) + translate([cx, cy, plate_thick - pocket_dep - num_depth]) + linear_extrude(num_depth + 0.1) + text(label, size = num_size, halign = "center", valign = "center"); + // "lebendes" Artefakt: zusaetzlicher Rahmen als Hinweis (stapeln/aktualisieren) + if (live) + translate([cx, cy, plate_thick - pocket_dep - lf_depth]) + linear_extrude(lf_depth + 0.1) + difference() { + square([slot_w - 2*lf_inset, slot_d - 2*lf_inset], center = true); + square([slot_w - 2*lf_inset - 2*lf_w, slot_d - 2*lf_inset - 2*lf_w], center = true); + } +} + +module tray() { + difference() { + translate([plate_w/2, plate_h/2, 0]) rrect(plate_w, plate_h, plate_thick, corner_r); + for (r = [0 : n_rows - 1]) { + hdr = ROWS[r][0]; + slots = ROWS[r][1]; + // Phasen-Kopfzeile (links ueber der Reihe) + translate([margin + 1, blockTopY(r) - header_h/2, plate_thick - hdr_depth]) + linear_extrude(hdr_depth + 0.1) + text(hdr, size = hdr_size, halign = "left", valign = "center"); + for (c = [0 : len(slots) - 1]) + pocket(slotCenterX(c), slotCenterY(r), slots[c][0], slots[c][1]); + } + } +} + +tray(); + +echo(plate_w = plate_w, plate_h = plate_h, plate_thick = plate_thick, slots = 15); diff --git a/04_Tablet-Quiz/app/index.html b/04_Tablet-Quiz/app/index.html index 67c6313..3ba0b80 100644 --- a/04_Tablet-Quiz/app/index.html +++ b/04_Tablet-Quiz/app/index.html @@ -68,6 +68,31 @@ body.navOpen aside { transform: translateX(0); } .navBackdrop { position:fixed; inset:0; background:rgba(15,22,35,.42); z-index:29; opacity:0; pointer-events:none; transition:opacity .2s; } body.navOpen .navBackdrop { opacity:1; pointer-events:auto; } + body.akteOpen .navBackdrop { opacity:1; pointer-events:auto; } + /* Service-Akte als Overlay von rechts (Header-Button 📁 Akte) */ + #akteList { + position: fixed; top:0; right:0; bottom:0; width: 340px; max-width: 88vw; + background: var(--panel); border-left:1px solid var(--line); padding: 14px; + overflow:auto; z-index: 30; + transform: translateX(100%); transition: transform .22s ease; + box-shadow: 0 0 40px rgba(20,30,50,.18); + } + body.akteOpen #akteList { transform: translateX(0); } + body:not(.runMode) #akteList { display:none; } + body:not(.runMode) #akteBtn { display:none; } + #akteList h3 { font-size:11px; text-transform:uppercase; letter-spacing:.8px; color:var(--muted); margin:14px 0 4px; } + .akteCount { font-size:13px; color:var(--muted); margin:2px 0 6px; font-variant-numeric:tabular-nums; } + .akteItem { display:flex; align-items:center; gap:10px; padding:7px 4px; border-radius:8px; font-size:13px; opacity:.5; } + .akteItem.have { opacity:1; } + .akteItem .aId { color:#fff; font-size:11px; font-weight:700; border-radius:6px; padding:2px 7px; min-width:30px; text-align:center; flex:none; } + .akteItem .aNm { flex:1; } + .akteItem .aNm i { color:var(--muted); font-style:italic; } + .akteItem .aChk { color:var(--ok); font-weight:700; } + /* Gate: Artefakt-Anforderung (harte Kopplung) */ + .gateReq { border-radius:8px; padding:9px 12px; margin:8px 0 12px; font-size:14px; font-weight:600; line-height:1.4; } + .gateReq.ok { background:#f1faf4; color:#177a44; border:1px solid #cde6d6; } + .gateReq.bad { background:#fdf3f3; color:#b5202a; border:1px solid #f0c9c9; } + .choice[disabled] { opacity:.45; cursor:default; } .navTop { display:flex; align-items:center; justify-content:space-between; margin-bottom:8px; } .navTop b { font-size:13px; text-transform:uppercase; letter-spacing:.6px; color:var(--muted); } .navTop button { border:none; background:none; font-size:20px; line-height:1; color:var(--muted); cursor:pointer; padding:4px 8px; } @@ -244,6 +269,7 @@ v0.6
+ @@ -252,6 +278,7 @@${st.artefakt}
` } ]; } +// Antwort-Optionen fuer die Artefakt-Choice: richtiges A + 3 Distraktoren +// (bevorzugt aus derselben Phase), deterministisch nach A-Nummer sortiert. +function arteOptions(correct){ + const ids = Object.keys(ARTEFAKTE); + const ph = ARTEFAKTE[correct].phase; + const same = ids.filter(a => a!==correct && ARTEFAKTE[a].phase===ph); + const other = ids.filter(a => a!==correct && ARTEFAKTE[a].phase!==ph); + const opts = [correct].concat(same.concat(other).slice(0,3)); + return opts.sort((a,b)=> a.localeCompare(b, "en", {numeric:true})); +} function renderActivity(st){ const phaseColor = PHASEN[st.phase].color; const next = STATIONEN[S.index+1]; @@ -1224,6 +1336,8 @@ function renderActivity(st){ const i = Math.min(S.actStep||0, steps.length-1); const step = steps[i]; const isLast = i === steps.length-1; + const arteId = STATION_ARTEFAKT[st.id]; // A-Nummer, falls diese Station eine erzeugt + const isArteChoice = step.artefakt && arteId; // Artefakt-Schritt mit echter Choice let html = `✓ ${arteId} — ${ARTEFAKTE[arteId].name} in die Service-Akte gelegt.
Entscheidet: ${roleLabel(keeper)}
+ ${reqLine}