app
Adom 3D Viewer
Public Made by Adomby adom
The Babylon 9.5 engine behind every component page's 3D tab on wiki.adom.inc. Versioned ESM bundle with GLB loading, view cube, layers toolbar, ground shadows, and Z-up CAD framing.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
/**
* Standalone entry point for the Adom 3D Viewer.
* Bundles Svelte + Babylon.js + the ThreeDViewer component into a single IIFE.
* Exposes window.Adom3DViewer with init/loadModel/clearScene/getScene APIs.
* Includes laser etch and Y-up→Z-up helpers inside the bundle scope.
*/
// Browser shim for the Node `process` global. @babylonjs/inspector v9 pulls
// in React + Fluent UI, which contain runtime `process.env.FA_VERSION` and
// `process.env.NODE_ENV` lookups. Vite's `define` handles NODE_ENV at build
// time, but FA_VERSION still references `process.*` at runtime — without a
// shim the inspector throws "process is not defined" the moment a user clicks
// the toolbar Inspector button. Define `process` BEFORE any other imports so
// every module that touches it sees the shim.
if (typeof (globalThis as any).process === 'undefined') {
(globalThis as any).process = { env: {}, browser: true, platform: 'browser' };
}
import ThreeDViewer from './src/ThreeDViewer.svelte';
// Side-effect: registers the debugLayer getter on Scene.
import '@babylonjs/core/Debug/debugLayer';
// Bring in the entire @babylonjs/core namespace so apps poking at the scene
// from the outside (chiplinter, scene-graph utility scripts) can use it.
import * as BABYLON_CORE from '@babylonjs/core';
// Babylon.js classes (used directly inside this bundle for laser etch etc.)
import { DynamicTexture } from '@babylonjs/core/Materials/Textures/dynamicTexture';
import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial';
import { MeshBuilder } from '@babylonjs/core/Meshes/meshBuilder';
import { Vector3 } from '@babylonjs/core/Maths/math.vector';
import { Color3 } from '@babylonjs/core/Maths/math.color';
import type { Scene } from '@babylonjs/core/scene';
import type { AbstractMesh } from '@babylonjs/core/Meshes/abstractMesh';
interface ViewerInstance {
loadModel: (url: string) => Promise<void>;
clearScene: () => void;
frameModel: (fillFraction?: number) => void;
getScene: () => Scene | null;
getEngine: () => any;
getCamera: () => any;
getShadowGenerator: () => any;
addContentRoot: (node: any, options?: any) => void;
removeContentRoot: (node: any) => void;
toggleDebugLayer: () => void;
showDebugLayer: () => void;
hideDebugLayer: () => void;
setGroundVisible: (visible: boolean) => void;
setBottomLight: (on: boolean) => void;
getBottomLightState: () => boolean;
goHome: () => void;
setProjectionMode: (ortho: boolean) => void;
getProjectionMode: () => 'perspective' | 'orthographic';
/** 3d-viewer-design §8 — toggle world-origin or mesh-local axes. The
* screen-space corner triad (§8c) is always on and has no toggle. */
toggleAxes: (target: 'world' | 'mesh-local', enabled?: boolean) => boolean;
getAxesState: () => { world: boolean; meshLocal: boolean };
/** Layers detected from the naming convention (docs/CONTENT-CONVENTIONS.md);
* empty when the loaded GLB has fewer than two named layers. */
getLayers: () => { name: string; label: string; visible: boolean }[];
setLayerVisible: (name: string, visible: boolean) => boolean;
destroy: () => void;
}
function init(container: HTMLElement, options?: {
zUp?: boolean;
modelUrl?: string;
showViewCube?: boolean;
showGround?: boolean;
environmentUrl?: string;
initialViewMode?: 'perspective' | 'orthographic';
}): ViewerInstance {
const opts = options || {};
const component = new ThreeDViewer({
target: container,
props: {
zUp: opts.zUp !== undefined ? opts.zUp : true,
modelUrl: opts.modelUrl,
showViewCube: opts.showViewCube !== undefined ? opts.showViewCube : true,
showGround: opts.showGround !== undefined ? opts.showGround : false,
environmentUrl: opts.environmentUrl,
initialViewMode: opts.initialViewMode || 'perspective',
},
});
return {
loadModel: (url: string) => component.loadModel(url),
clearScene: () => component.clearScene(),
frameModel: (fillFraction?: number) => component.frameModel(fillFraction),
getScene: () => component.getScene(),
getEngine: () => component.getEngine(),
getCamera: () => component.getCamera(),
getShadowGenerator: () => component.getShadowGenerator(),
addContentRoot: (node: any, opts?: any) => component.addContentRoot(node, opts),
removeContentRoot: (node: any) => component.removeContentRoot(node),
toggleDebugLayer: () => component.toggleDebugLayer(),
showDebugLayer: () => component.showDebugLayer(),
hideDebugLayer: () => component.hideDebugLayer(),
setGroundVisible: (visible: boolean) => component.setGroundVisible(visible),
setBottomLight: (on: boolean) => component.setBottomLight(on),
getBottomLightState: () => component.getBottomLightState(),
goHome: () => component.goHome(),
setProjectionMode: (ortho: boolean) => component.setProjectionMode(ortho),
getProjectionMode: () => component.getProjectionMode(),
toggleAxes: (target: 'world' | 'mesh-local', enabled?: boolean) => component.toggleAxes(target, enabled),
getAxesState: () => component.getAxesState(),
getLayers: () => component.getLayers(),
setLayerVisible: (name: string, visible: boolean) => component.setLayerVisible(name, visible),
destroy: () => component.$destroy(),
};
}
/**
* Rotate all __root__ nodes from Y-up (kicad-cli GLB) to Z-up (adom viewer).
* Call after loadModel() resolves.
*/
function rotateYUpToZUp(scene: Scene | null): void {
if (!scene) return;
scene.meshes.forEach((m: AbstractMesh) => {
if (m.name === '__root__') {
m.rotation = new Vector3(Math.PI / 2, 0, 0);
m.computeWorldMatrix(true);
}
});
// Force all children to recompute
scene.meshes.forEach((m: AbstractMesh) => {
m.computeWorldMatrix(true);
});
}
interface LaserEtchOptions {
bodySize: { x: number; y: number; z?: number };
partName?: string;
manufacturer?: string;
markingLines?: string[];
}
/**
* Add laser etch chip markings on the IC body top surface.
* Must be called after loadModel() + rotateYUpToZUp() + frameModel().
* Returns the created plane mesh, or null if failed.
*/
function addLaserEtch(scene: Scene | null, options: LaserEtchOptions): any {
if (!scene) return null;
const MM = 0.001; // GLB is in meters
const bodySize = options.bodySize;
const bw = bodySize.x * MM;
const bd = bodySize.y * MM;
const longSide = Math.max(bw, bd);
const shortSide = Math.min(bw, bd);
// Build marking lines
const markingLines = options.markingLines ? options.markingLines.slice() : [];
if (markingLines.length === 0) {
if (options.manufacturer) markingLines.push(options.manufacturer.toUpperCase());
if (options.partName) markingLines.push(options.partName.toUpperCase());
const now = new Date();
markingLines.push(String(now.getFullYear()).slice(-2) + String(now.getMonth() + 1).padStart(2, '0'));
}
if (markingLines.length === 0) return null;
// Find IC body top Z from loaded meshes (Z-up coordinate system)
// Skip scene infrastructure meshes
const skipNames = new Set(['skyBox', 'shadowGround', 'ViewerRoot', 'laserEtch', 'ground']);
let topZ = -Infinity;
scene.meshes.forEach((m: AbstractMesh) => {
if (skipNames.has(m.name)) return;
if (m.name.startsWith('viewCube')) return;
if (m.getTotalVertices && m.getTotalVertices() > 0) {
m.computeWorldMatrix(true);
const bi = m.getBoundingInfo();
if (bi.boundingBox.maximumWorld.z > topZ) {
topZ = bi.boundingBox.maximumWorld.z;
}
}
});
if (!isFinite(topZ)) return null;
// DynamicTexture for laser etch text
const texW = 1024;
const texH = Math.round(1024 * (shortSide / longSide));
const dtex = new DynamicTexture('laserEtchTex', { width: texW, height: texH }, scene, true);
const ctx = dtex.getContext();
ctx.clearRect(0, 0, texW, texH);
ctx.fillStyle = 'rgba(210, 212, 215, 0.8)';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
const lineCount = markingLines.length;
const usableH = texH * 0.65;
const lineH = usableH / lineCount;
const fontSize = Math.min(Math.round(lineH * 0.7), Math.round(texW * 0.09));
ctx.font = `${fontSize}px monospace`;
const startY = (texH - usableH) / 2 + lineH / 2;
for (let i = 0; i < lineCount; i++) {
ctx.fillText(markingLines[i], texW / 2, startY + lineH * i);
}
// Pin 1 dot
ctx.beginPath();
ctx.arc(texW * 0.07, texH * 0.09, fontSize * 0.18, 0, Math.PI * 2);
ctx.fill();
dtex.update();
// Material — self-illuminating silver text
const mat = new StandardMaterial('etchMat', scene);
mat.diffuseTexture = dtex;
mat.diffuseTexture.hasAlpha = true;
mat.useAlphaFromDiffuseTexture = true;
mat.emissiveColor = new Color3(0.82, 0.83, 0.84);
mat.backFaceCulling = false;
mat.zOffset = -2;
// Plane on IC body top (Z-up: plane faces +Z by default)
// In right-handed Z-up, CreatePlane makes an XY plane with normal along Z
const plane = MeshBuilder.CreatePlane('laserEtch', {
width: longSide * 0.85,
height: shortSide * 0.85,
sideOrientation: 2, // DOUBLESIDE
}, scene);
plane.position = new Vector3(0, 0, topZ + 0.00002);
plane.material = mat;
return plane;
}
// Required by the CDN-loaded Babylon Inspector — it expects to bind to
// `window.BABYLON`. We expose the full @babylonjs/core namespace.
if (!(window as any).BABYLON) {
(window as any).BABYLON = BABYLON_CORE;
}
// Expose on window. Distinct global from window.Adom3DViewer (Colby's bundle)
// so apps can have both loaded side-by-side during migration. The BABYLON
// sub-namespace re-exports the FULL @babylonjs/core surface so consumers
// can use any Babylon class (TransformNode, PBRMaterial, Quaternion, Matrix,
// CSG, Mesh, ...) without each app needing its own import side.
(window as any).Adom3DViewerBabylon9 = {
init,
rotateYUpToZUp,
addLaserEtch,
BABYLON: BABYLON_CORE,
};