skill
Basic3dView
Public Made by Adomby adom
Minimal GLB 3D viewer for the Adom Viewer — renders GLB files exactly as-is with zero post-processing. No material fixups, no overlays, no camera tours. Includes toolbar (ground, wireframe, axes, meas
← Commit history
viewer-lqfp-48.htmladded+534@@ -0,0 +1,534 @@+<!DOCTYPE html>+<html>+<head><script>(function(){var n=0;function show(p,m){var el=document.createElement('div');el.style.cssText='position:fixed;left:0;right:0;background:#e04040;color:#fff;padding:6px 12px;z-index:'+(99999-n)+';font:13px/1.4 monospace;white-space:pre-wrap;border-bottom:1px solid #b02020;top:'+n*30+'px';el.textContent=p+m;document.body.appendChild(el);n++}window.onerror=function(msg,src,line){show('ERROR '+(src||'').split('/').pop()+':'+line+' \u2014 ',msg)};window.addEventListener('unhandledrejection',function(e){show('REJECT \u2014 ',e.reason&&e.reason.message||e.reason||'unknown')})})()</script>+<meta charset="utf-8">+<title>Basic3dView — LQFP-48</title>+<style>+* { margin: 0; padding: 0; box-sizing: border-box; }+body {+ overflow: hidden;+ height: 100vh;+ display: flex;+ flex-direction: column;+ background: #1a1a1a;+ color: #ccc;+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;+}+#viewer-wrap {+ flex: 1;+ position: relative;+ min-height: 0;+}+.viewer-container { position: relative; width: 100%; height: 100%; overflow: hidden; }+.viewer-container canvas { display: block; width: 100%; height: 100%; outline: none; }++#toolbar {+ position: absolute;+ top: 8px;+ left: 8px;+ z-index: 20;+ display: flex;+ gap: 1px;+ background: #1a1a1a;+ border: 1px solid #333;+ border-radius: 6px;+ padding: 2px;+}+#toolbar button {+ display: flex;+ align-items: center;+ justify-content: center;+ width: 32px;+ height: 28px;+ background: transparent;+ border: none;+ border-radius: 4px;+ cursor: pointer;+ transition: background 0.12s;+ position: relative;+}+#toolbar button svg {+ width: 16px;+ height: 16px;+ fill: none;+ stroke: #888;+ stroke-width: 1.8;+ stroke-linecap: round;+ stroke-linejoin: round;+}+#toolbar button:hover { background: #2a2a2a; }+#toolbar button:hover svg { stroke: #ccc; }+#toolbar button.active svg { stroke: #00b8b1; }+#toolbar button:active { background: #333; }+#toolbar button::after {+ content: attr(data-tip);+ position: absolute;+ top: calc(100% + 6px);+ left: 50%;+ transform: translateX(-50%);+ background: #1a1a1a;+ color: #ccc;+ font-size: 11px;+ padding: 4px 8px;+ border-radius: 4px;+ border: 1px solid #333;+ white-space: nowrap;+ pointer-events: none;+ opacity: 0;+ transition: opacity 0.15s;+ z-index: 30;+}+#toolbar button:hover::after { opacity: 1; }++#caption-bar {+ display: none;+ padding: 8px 14px;+ background: rgba(13, 17, 23, 0.85);+ border-top: 1px solid #222;+ font-size: 13px;+ color: #b0b8c4;+ line-height: 1.4;+}+#caption-bar.visible { display: block; }+#info-bar {+ display: flex;+ align-items: center;+ gap: 12px;+ padding: 6px 12px;+ background: #111;+ border-top: 1px solid #333;+ font-size: 12px;+ color: #888;+ min-height: 32px;+}+#info-bar .label { color: #00b8b1; font-weight: 600; }+#info-bar .viewer-name { margin-left: auto; color: #00b8b1; font-weight: 600; }++#loading {+ position: absolute;+ top: 50%;+ left: 50%;+ transform: translate(-50%, -50%);+ color: #888;+ font-size: 14px;+ z-index: 10;+}+++/* Drag-drop overlay */+#drop-overlay {+ display: none;+ position: absolute;+ top: 0; left: 0; right: 0; bottom: 0;+ background: rgba(0, 184, 177, 0.15);+ border: 3px dashed #00b8b1;+ z-index: 100;+ align-items: center;+ justify-content: center;+ font-size: 18px;+ color: #00b8b1;+ font-weight: 600;+}+#drop-overlay.active { display: flex; }++/* Mesh list panel */+#mesh-panel {+ display: none;+ position: absolute;+ top: 8px;+ right: 8px;+ z-index: 20;+ background: #1a1a1a;+ border: 1px solid #333;+ border-radius: 6px;+ padding: 8px;+ max-height: 60vh;+ overflow-y: auto;+ min-width: 200px;+ max-width: 300px;+ font-size: 11px;+}+#mesh-panel.open { display: block; }+#mesh-panel h3 {+ font-size: 12px;+ color: #00b8b1;+ margin-bottom: 6px;+ font-weight: 600;+}+#mesh-panel .mesh-item {+ padding: 3px 6px;+ border-radius: 3px;+ cursor: pointer;+ display: flex;+ justify-content: space-between;+ gap: 8px;+}+#mesh-panel .mesh-item:hover { background: #2a2a2a; }+#mesh-panel .mesh-item .name { color: #ccc; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }+#mesh-panel .mesh-item .verts { color: #666; flex-shrink: 0; }+</style>+</head>+<body>+<div id="viewer-wrap">+ <div id="loading">Loading 3D engine...</div>+ <div id="drop-overlay">Drop GLB file here</div>+ <div id="toolbar" style="display:none">+ <button id="btn-home" data-tip="Reset camera">+ <svg viewBox="0 0 24 24"><path d="M3 12l9-9 9 9"/><path d="M9 21V12h6v9"/></svg>+ </button>+ <button id="btn-frame" data-tip="Frame model">+ <svg viewBox="0 0 24 24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="12" cy="12" r="3"/></svg>+ </button>+ <button id="btn-ground" data-tip="Toggle ground" class="active">+ <svg viewBox="0 0 24 24"><path d="M2 20L12 4l10 16H2z"/></svg>+ </button>+ <button id="btn-wireframe" data-tip="Toggle wireframe">+ <svg viewBox="0 0 24 24"><path d="M12 2L2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>+ </button>+ <button id="btn-axes" data-tip="Toggle axes">+ <svg viewBox="0 0 24 24"><path d="M12 2v20M2 12h20"/><path d="M12 2l-2 4M12 2l2 4"/><path d="M22 12l-4-2M22 12l-4 2"/></svg>+ </button>+ <button id="btn-meshes" data-tip="Mesh list">+ <svg viewBox="0 0 24 24"><path d="M4 6h16M4 12h16M4 18h16"/></svg>+ </button>+ </div>+ <div id="mesh-panel"><h3>Meshes</h3><div id="mesh-list"></div></div>+</div>+<div id="caption-bar"></div>+<div id="info-bar">+ <span id="info-text">Drag & drop a .glb file or load via MCP tool</span>+ <span class="viewer-name">Basic3dView</span>+</div>++<script>+var viewer = null;+var loadingEl = document.getElementById('loading');+var toolbarEl = document.getElementById('toolbar');+var groundVisible = true;+var wireframeMode = false;+var axesVisible = false;+var axesMeshes = [];+var meshPanelOpen = false;++// ── Drag & Drop ────────────────────────────────────────────────+var dropOverlay = document.getElementById('drop-overlay');+var dragCounter = 0;++document.addEventListener('dragenter', function(e) {+ e.preventDefault();+ dragCounter++;+ dropOverlay.classList.add('active');+});+document.addEventListener('dragleave', function(e) {+ e.preventDefault();+ dragCounter--;+ if (dragCounter <= 0) {+ dragCounter = 0;+ dropOverlay.classList.remove('active');+ }+});+document.addEventListener('dragover', function(e) {+ e.preventDefault();+});+document.addEventListener('drop', function(e) {+ e.preventDefault();+ dragCounter = 0;+ dropOverlay.classList.remove('active');++ var files = e.dataTransfer.files;+ for (var i = 0; i < files.length; i++) {+ var file = files[i];+ if (file.name.toLowerCase().endsWith('.glb') || file.name.toLowerCase().endsWith('.gltf')) {+ loadFromFile(file);+ return;+ }+ }+});++function loadFromFile(file) {+ if (!viewer) return;+ loadingEl.style.display = '';+ loadingEl.textContent = 'Loading ' + file.name + '...';++ var url = URL.createObjectURL(file);+ viewer.clearScene();++ viewer.loadModel(url).then(function() {+ URL.revokeObjectURL(url);+ loadingEl.style.display = 'none';+ viewer.frameModel();+ fixNearClip();+ updateInfo(file.name);+ populateMeshList();+ parent.postMessage(JSON.stringify({ type: 'basic_3d_loaded' }), '*');+ }).catch(function(err) {+ URL.revokeObjectURL(url);+ loadingEl.textContent = 'Load failed: ' + (err.message || err);+ loadingEl.style.color = '#e04040';+ parent.postMessage(JSON.stringify({ type: 'basic_3d_loaded', error: true }), '*');+ });+}++// ── Load from URL ──────────────────────────────────────────────+function loadFromUrl(modelUrl, options) {+ if (!viewer) {+ window._pendingModel = { modelUrl: modelUrl, options: options || {} };+ return;+ }+ options = options || {};++ loadingEl.style.display = '';+ loadingEl.textContent = 'Loading 3D model...';++ var fullUrl = resolveModelUrl(modelUrl);+ viewer.clearScene();++ viewer.loadModel(fullUrl).then(function() {+ loadingEl.style.display = 'none';+ viewer.frameModel();+ fixNearClip();+ updateInfo(options.title || options.fileName || modelUrl.split('/').pop());+ setCaption(options.caption || '');+ populateMeshList();+ parent.postMessage(JSON.stringify({ type: 'basic_3d_loaded' }), '*');+ }).catch(function(err) {+ loadingEl.textContent = 'Load failed: ' + (err.message || err);+ loadingEl.style.color = '#e04040';+ parent.postMessage(JSON.stringify({ type: 'basic_3d_loaded', error: true }), '*');+ });+}++function resolveModelUrl(modelUrl) {+ if (modelUrl.startsWith('http://') || modelUrl.startsWith('https://') || modelUrl.startsWith('blob:')) {+ return modelUrl;+ }+ var base = location.pathname.replace(/\/[^/]*$/, '');+ return base + (modelUrl.startsWith('/') ? '' : '/') + modelUrl;+}++function fixNearClip() {+ var cam = viewer.getCamera();+ if (cam && cam.radius < 0.1) {+ cam.minZ = cam.radius / 1000;+ }+}++function setCaption(text) {+ var el = document.getElementById('caption-bar');+ if (text) {+ el.textContent = text;+ el.classList.add('visible');+ } else {+ el.textContent = '';+ el.classList.remove('visible');+ }+}++function updateInfo(name) {+ var scene = viewer.getScene();+ var meshCount = 0;+ var vertCount = 0;+ scene.meshes.forEach(function(m) {+ if (m.getTotalVertices && m.getTotalVertices() > 0) {+ meshCount++;+ vertCount += m.getTotalVertices();+ }+ });+ var info = name;+ info += ' \u2014 ' + meshCount + ' meshes, ' + vertCount.toLocaleString() + ' verts';+ document.getElementById('info-text').textContent = info;+}++function populateMeshList() {+ var scene = viewer.getScene();+ var listEl = document.getElementById('mesh-list');+ listEl.innerHTML = '';+ scene.meshes.forEach(function(m) {+ if (!m.getTotalVertices || m.getTotalVertices() === 0) return;+ var div = document.createElement('div');+ div.className = 'mesh-item';+ var nameSpan = document.createElement('span');+ nameSpan.className = 'name';+ nameSpan.textContent = m.name || '(unnamed)';+ nameSpan.title = m.name || '(unnamed)';+ var vertSpan = document.createElement('span');+ vertSpan.className = 'verts';+ vertSpan.textContent = m.getTotalVertices().toLocaleString() + 'v';+ div.appendChild(nameSpan);+ div.appendChild(vertSpan);+ div.onclick = function() {+ // Highlight mesh briefly+ var origColor = null;+ var mat = m.material;+ if (mat) {+ origColor = mat.emissiveColor ? mat.emissiveColor.clone() : null;+ mat.emissiveColor = new window.Adom3DViewer.BABYLON.Color3(0, 0.72, 0.69);+ setTimeout(function() {+ if (origColor) mat.emissiveColor = origColor;+ else mat.emissiveColor = new window.Adom3DViewer.BABYLON.Color3(0, 0, 0);+ }, 1500);+ }+ };+ listEl.appendChild(div);+ });+}++// ── postMessage API ────────────────────────────────────────────+window.addEventListener('message', function(e) {+ var msg;+ try { msg = JSON.parse(e.data); } catch(ex) { return; }+ if (msg.type === 'load_model') {+ loadFromUrl(msg.modelUrl, msg.options);+ } else if (msg.type === 'basic3d_toggle') {+ var btn = document.getElementById('btn-' + msg.name);+ if (btn) btn.click();+ } else if (msg.type === 'basic3d_set_camera') {+ var cam = viewer && viewer.getCamera();+ if (cam) {+ if (msg.alpha !== undefined) cam.alpha = msg.alpha;+ if (msg.beta !== undefined) cam.beta = msg.beta;+ if (msg.radius !== undefined) cam.radius = msg.radius;+ }+ } else if (msg.type === 'basic3d_set_caption') {+ setCaption(msg.text || '');+ } else if (msg.type === 'basic3d_fill') {+ // Fill entire iframe with a solid color (for screenshot calibration)+ var overlay = document.getElementById('calibration-overlay');+ if (msg.color) {+ if (!overlay) {+ overlay = document.createElement('div');+ overlay.id = 'calibration-overlay';+ overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;z-index:9999;';+ document.body.appendChild(overlay);+ }+ overlay.style.background = msg.color;+ overlay.style.display = 'block';+ } else if (overlay) {+ overlay.style.display = 'none';+ }+ }+});++// ── Init Babylon.js ────────────────────────────────────────────+var base = location.pathname.replace(/\/[^/]*$/, '');+var script = document.createElement('script');+script.src = base + '/adom-3d-viewer.min.js?v=' + Date.now();+script.onload = function() {+ loadingEl.textContent = 'Initializing viewer...';+ try {+ var wrap = document.getElementById('viewer-wrap');+ loadingEl.style.display = 'none';++ var envUrl = base + '/environmentSpecular.env';+ viewer = window.Adom3DViewer.init(wrap, {+ zUp: true,+ showViewCube: true,+ showGround: true,+ environmentUrl: envUrl,+ });++ toolbarEl.style.display = '';+ initToolbar();++ parent.postMessage(JSON.stringify({ type: 'basic_3d_ready' }), '*');++ // Auto-load model for wiki demo+ (function() {+ var glbUrl = 'lqfp-48.glb';+ loadFromUrl(glbUrl, { title: 'LQFP-48', caption: 'LQFP-48 with RGB axis indicator — Red=X, Green=Y, Blue=Z' });+ // Toggle features after model loads+ var poll = setInterval(function() {+ if (document.getElementById('loading').style.display === 'none' && viewer) {+ clearInterval(poll);+ document.getElementById('btn-axes').click();+ }+ }, 200);+ })();++ if (window._pendingModel) {+ var pending = window._pendingModel;+ window._pendingModel = null;+ loadFromUrl(pending.modelUrl, pending.options);+ }+ } catch (err) {+ loadingEl.style.display = '';+ loadingEl.textContent = 'Failed to init viewer: ' + err.message;+ loadingEl.style.color = '#e04040';+ }+};+script.onerror = function() {+ loadingEl.textContent = 'Failed to load 3D viewer bundle';+ loadingEl.style.color = '#e04040';+};+document.head.appendChild(script);++// ── Toolbar ────────────────────────────────────────────────────+function initToolbar() {+ document.getElementById('btn-home').onclick = function() {+ if (viewer) viewer.goHome();+ };+ document.getElementById('btn-frame').onclick = function() {+ if (viewer) viewer.frameModel();+ };+ document.getElementById('btn-ground').onclick = function() {+ groundVisible = !groundVisible;+ if (viewer) viewer.setGroundVisible(groundVisible);+ this.classList.toggle('active', groundVisible);+ };+ document.getElementById('btn-wireframe').onclick = function() {+ wireframeMode = !wireframeMode;+ var scene = viewer.getScene();+ scene.meshes.forEach(function(m) {+ if (m.material) m.material.wireframe = wireframeMode;+ });+ this.classList.toggle('active', wireframeMode);+ };+ document.getElementById('btn-axes').onclick = function() {+ axesVisible = !axesVisible;+ toggleAxes(axesVisible);+ this.classList.toggle('active', axesVisible);+ };+ document.getElementById('btn-meshes').onclick = function() {+ meshPanelOpen = !meshPanelOpen;+ document.getElementById('mesh-panel').classList.toggle('open', meshPanelOpen);+ this.classList.toggle('active', meshPanelOpen);+ };+}++function toggleAxes(show) {+ if (!viewer) return;+ var scene = viewer.getScene();+ var BABYLON = window.Adom3DViewer.BABYLON;++ // Remove existing+ axesMeshes.forEach(function(m) { m.dispose(); });+ axesMeshes = [];++ if (!show) return;++ var len = 5;+ var colors = [+ new BABYLON.Color3(1, 0.2, 0.2), // X = red+ new BABYLON.Color3(0.2, 1, 0.2), // Y = green+ new BABYLON.Color3(0.3, 0.5, 1), // Z = blue+ ];+ var dirs = [+ new BABYLON.Vector3(len, 0, 0),+ new BABYLON.Vector3(0, len, 0),+ new BABYLON.Vector3(0, 0, len),+ ];+ var labels = ['X', 'Y', 'Z'];++ for (var i = 0; i < 3; i++) {+ var points = [BABYLON.Vector3.Zero(), dirs[i]];+ var lines = BABYLON.MeshBuilder.CreateLines('axis_' + labels[i], {+ points: points,+ }, scene);+ lines.color = colors[i];+ lines.isPickable = false;+ axesMeshes.push(lines);+ }+}+</script>+</body>+</html>