← Commit history
README.md+9
@@ -29,6 +29,15 @@ The two halves mate coplanar at the blade dock, with the project's molecules, ju  ![Control Panel: Scaffold — Block Diagram (User)](diagrams/scaffold-block-diagram-user.png) +## 3D Models++The exact board models from the component pages, in an interactive viewer (orbit, zoom,+animations, bottom light) — click either preview to open it:++| [![Workcell 3D](diagrams/viewer-workcell.png)](/adom/base-scaffold-control-panel/render/readme.html) | [![Scaffold 3D](diagrams/viewer-scaffold.png)](/adom/base-scaffold-control-panel/render/readme.html) |+|---|---|+| [Control Panel: Workcell — open 3D viewer](/adom/base-scaffold-control-panel/render/readme.html) | [Control Panel: Scaffold — open 3D viewer](/adom/base-scaffold-control-panel/render/readme.html) |+ ## Input power  Four independent bench-supply input to the workcell control panel. Every rail is
diagrams/viewer-scaffold.pngadded
⋯ 1 unchanged line ⋯
diagrams/viewer-workcell.pngadded
⋯ 1 unchanged line ⋯
models/Control_Panel_Scaffold_v2.0.1.glbadded
⋯ 1 unchanged line ⋯
models/Control_Panel_Workcell_v2.0.1.glbadded
⋯ 1 unchanged line ⋯
package.json+2−2
@@ -2,7 +2,7 @@   "slug": "base-scaffold-control-panel",   "title": "Base Scaffold Control Panel",   "type": "app",-  "version": "0.1.4",+  "version": "0.1.5",   "description": "The Base Scaffold System's control panel: electrical specifications and the connect/disconnect interface between a base scaffold and the workcell ecosystem.",   "tags": [     "hardware",@@ -29,7 +29,7 @@     "uninstall": "./uninstall.sh"   },   "hero": {-    "headline": "BSS Control Panel",+    "headline": "Base Scaffold Control Panel",     "subhead": "The electrical front door of the Base Scaffold System.",     "screenshot": "docs/hero.png"   }
page.json+1−1
@@ -2,7 +2,7 @@   "slug": "base-scaffold-control-panel",   "title": "Base Scaffold Control Panel",   "type": "app",-  "version": "0.1.1",+  "version": "0.1.5",   "description": "The Base Scaffold System's control panel: electrical specifications and the connect/disconnect interface between a base scaffold and the workcell ecosystem.",   "tags": [     "hardware",
readme.htmladded+157
@@ -0,0 +1,157 @@+<!doctype html>+<html lang="en">+<head>+<meta charset="utf-8">+<meta name="viewport" content="width=device-width, initial-scale=1">+<title>Base Scaffold Control Panel — 3D</title>+<style>+* { margin: 0; padding: 0; box-sizing: border-box; }+html, body { height: 100%; }+body {+  background: #0d1117; color: #e6edf3; overflow: hidden;+  font-family: 'Segoe UI', system-ui, sans-serif; font-size: 14px;+}+#bar {+  position: fixed; top: 0; left: 0; right: 0; height: 44px; z-index: 10;+  display: flex; align-items: center; gap: 10px; padding: 0 14px;+  background: rgba(22,27,34,.92); border-bottom: 1px solid #30363d;+}+#bar .t { font-weight: 700; font-size: 14px; }+#bar .t span { color: #00b8b0; }+.seg { display: flex; border: 1px solid #30363d; border-radius: 8px; overflow: hidden; }+.seg button {+  padding: 6px 14px; background: none; border: none; color: #8b949e;+  font: inherit; font-size: 12.5px; cursor: pointer;+}+.seg button + button { border-left: 1px solid #30363d; }+.seg button.on { background: rgba(0,184,176,.2); color: #00e6dc; font-weight: 700; }+.pill {+  margin-left: auto; padding: 5px 12px; border-radius: 999px; border: 1px solid #30363d;+  background: #1c2128; color: #8b949e; font-size: 12px; cursor: pointer;+}+.pill.on { color: #00e6dc; border-color: rgba(0,184,176,.4); }+#hint { position: fixed; bottom: 12px; left: 50%; transform: translateX(-50%); z-index: 10;+  font-size: 11.5px; color: #8b949e; background: rgba(22,27,34,.8); padding: 5px 12px; border-radius: 999px; }+#load { position: fixed; inset: 0; display: flex; align-items: center; justify-content: center;+  color: #8b949e; z-index: 5; pointer-events: none; }+canvas { display: block; }+</style>+</head>+<body>+<div id="bar">+  <span class="t">Control Panel <span>3D</span></span>+  <div class="seg" id="boards">+    <button data-b="workcell" class="on">Workcell</button>+    <button data-b="scaffold">Scaffold</button>+  </div>+  <button class="pill" id="anim">Animation on</button>+  <button class="pill" id="bottom">Bottom light</button>+</div>+<div id="load">loading model&hellip;</div>+<div id="hint">drag to orbit &middot; wheel to zoom &middot; right-drag to pan</div>+<script type="importmap">{"imports":{"three":"https://unpkg.com/[email protected]/build/three.module.js","three/addons/":"https://unpkg.com/[email protected]/examples/jsm/"}}</script>+<script type="module">+import * as THREE from 'three';+import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';+import { DRACOLoader } from 'three/addons/loaders/DRACOLoader.js';+import { OrbitControls } from 'three/addons/controls/OrbitControls.js';++const MODELS = {+  workcell: 'models/Control_Panel_Workcell_v2.0.1.glb',+  scaffold: 'models/Control_Panel_Scaffold_v2.0.1.glb',+};++const renderer = new THREE.WebGLRenderer({ antialias: true });+renderer.outputColorSpace = THREE.SRGBColorSpace;+renderer.setPixelRatio(Math.min(devicePixelRatio, 2));+document.body.appendChild(renderer.domElement);++const scene = new THREE.Scene();+// component-viewer style vertical gradient background+const bgCanvas = document.createElement('canvas');+bgCanvas.width = 2; bgCanvas.height = 256;+const bctx = bgCanvas.getContext('2d');+const grad = bctx.createLinearGradient(0, 0, 0, 256);+grad.addColorStop(0, '#1e2a3d'); grad.addColorStop(0.2, '#1e2a3d'); grad.addColorStop(1, '#0a121f');+bctx.fillStyle = grad; bctx.fillRect(0, 0, 2, 256);+scene.background = new THREE.CanvasTexture(bgCanvas);++scene.add(new THREE.HemisphereLight(0xddeeff, 0x222428, 1.1));+scene.add(new THREE.AmbientLight(0xffffff, 0.65));+const key = new THREE.DirectionalLight(0xfff0d8, 0.95);+key.position.set(0.5, 1, 0.8); scene.add(key);+const bottomLight = new THREE.DirectionalLight(0xbfd7ff, 0.0);+bottomLight.position.set(0, -1, 0.2); scene.add(bottomLight);++const camera = new THREE.PerspectiveCamera(35, 1, 0.001, 100);+const controls = new OrbitControls(camera, renderer.domElement);+controls.enableDamping = true;+controls.zoomToCursor = true;++const loader = new GLTFLoader();+const draco = new DRACOLoader();+draco.setDecoderPath('https://www.gstatic.com/draco/versioned/decoders/1.5.6/');+loader.setDRACOLoader(draco);++const cache = {};+let current = null, mixer = null, animOn = true;+const clock = new THREE.Clock();++async function show(name) {+  document.getElementById('load').style.display = 'flex';+  if (current) { scene.remove(current.root); current = null; mixer = null; }+  if (!cache[name]) {+    try { cache[name] = await loader.loadAsync(MODELS[name]); }+    catch (e) { document.getElementById('load').textContent = 'load failed: ' + e.message; return; }+  }+  const g = cache[name];+  const root = g.scene;+  scene.add(root);+  current = { root, gltf: g };+  if (g.animations && g.animations.length) {+    mixer = new THREE.AnimationMixer(root);+    mixer.clipAction(g.animations[0]).play();+  }+  const box = new THREE.Box3().setFromObject(root);+  const c = box.getCenter(new THREE.Vector3());+  const r = box.getSize(new THREE.Vector3()).length() / 2;+  controls.target.copy(c);+  camera.position.set(c.x + r * 0.9, c.y - r * 1.1, c.z + r * 1.4);+  camera.near = r / 100; camera.far = r * 100;+  camera.up.set(0, 0, 1);+  camera.updateProjectionMatrix();+  controls.update();+  document.getElementById('load').style.display = 'none';+}++document.querySelectorAll('#boards button').forEach(b => b.onclick = () => {+  document.querySelectorAll('#boards button').forEach(o => o.classList.toggle('on', o === b));+  show(b.dataset.b);+});+const animBtn = document.getElementById('anim');+animBtn.classList.add('on');+animBtn.onclick = () => { animOn = !animOn; animBtn.classList.toggle('on', animOn); animBtn.textContent = animOn ? 'Animation on' : 'Animation off'; };+const bottomBtn = document.getElementById('bottom');+bottomBtn.onclick = () => {+  const on = bottomLight.intensity === 0;+  bottomLight.intensity = on ? 0.9 : 0;+  bottomBtn.classList.toggle('on', on);+};++function frame() {+  requestAnimationFrame(frame);+  const w = innerWidth, h = innerHeight;+  if (renderer.domElement.width !== w * renderer.getPixelRatio()) {+    renderer.setSize(w, h); camera.aspect = w / h; camera.updateProjectionMatrix();+  }+  const dt = clock.getDelta();+  if (mixer && animOn) mixer.update(dt);+  controls.update();+  renderer.render(scene, camera);+}+frame();+show('workcell');+addEventListener('resize', () => renderer.setSize(innerWidth, innerHeight));+</script>+</body>+</html>