UNPKG

@needle-tools/engine

Version:

Needle Engine is a web-based runtime for 3D apps. It runs on your machine for development with great integrations into editors like Unity or Blender - and can be deployed onto any device! It is flexible, extensible and networking and XR are built-in.

391 lines • 14.5 kB
import { Layers, Material, Matrix3, Matrix4, Object3D, PerspectiveCamera, Quaternion, Texture, Vector2, Vector3, Vector4 } from "three"; import ThreeMeshUI from "three-mesh-ui"; import { ContextRegistry } from "../engine_context_registry.js"; import { OneEuroFilterXYZ } from "../engine_math.js"; import { lookAtObject } from "../engine_three_utils.js"; import { isDevEnvironment } from "./debug.js"; import { onError } from "./debug_overlay.js"; let _isActive = false; // enable the spatial console if we receive an error while in dev session and in XR onError((...args) => { if (isDevEnvironment() && ContextRegistry.Current?.isInXR) { enableSpatialConsole(true); onLog("error", ...args); } }); /** Enable a spatial debug console that follows the camera */ export function enableSpatialConsole(active) { if (active) { if (_isActive) return; _isActive = true; onEnable(); } else { if (!_isActive) return; _isActive = false; onDisable(); } } const originalConsoleMethods = { "log": undefined, "warn": undefined, "error": undefined, }; class SpatialMessagesHandler { familyName = "needle-xr"; root = null; context = null; defaultFontSize = .06; constructor() { this.ensureFont(); } onEnable() { this.context = ContextRegistry.Current || ContextRegistry.All[0]; this.context.pre_render_callbacks.push(this.onBeforeRender); } onDisable() { this.context?.pre_render_callbacks.splice(this.context?.pre_render_callbacks.indexOf(this.onBeforeRender), 1); this.root?.removeFromParent(); } targetObject = new Object3D(); /** this is a point in forward view of the user */ userForwardViewPoint = new Vector3(); oneEuroFilter = new OneEuroFilterXYZ(90, .8); _lastElementRemoveTime = 0; onBeforeRender = () => { const cam = this.context?.mainCamera; if (this.context && cam instanceof PerspectiveCamera) { const root = this.getRoot(); // TODO: need to figure out why this happens when entering VR (in the simulator at least) if (Number.isNaN(root.position.x)) root.position.set(0, 0, 0); if (Number.isNaN(root.quaternion.x)) root.quaternion.set(0, 0, 0, 1); this.context.scene.add(this.targetObject); const rigScale = this.context.xr?.rigScale ?? 1; const dist = 3.5 * rigScale; const forward = cam.worldForward; forward.y = 0; forward.normalize().multiplyScalar(dist); this.userForwardViewPoint.copy(cam.worldPosition).sub(forward); const distFromForwardView = this.targetObject.position.distanceTo(this.userForwardViewPoint); if (distFromForwardView > 2 * rigScale) { this.targetObject.position.copy(this.userForwardViewPoint); lookAtObject(this.targetObject, cam, true, true); this.targetObject.rotateY(Math.PI); } this.oneEuroFilter.filter(this.targetObject.position, root.position, this.context.time.time); const step = this.context.time.deltaTime; root.quaternion.slerp(this.targetObject.quaternion, step * 5); root.scale.setScalar(rigScale); this.targetObject.removeFromParent(); this.context.scene.add(root); if (this.context.time.time - this._lastElementRemoveTime > .1) { this._lastElementRemoveTime = this.context.time.time; const now = Date.now(); for (let i = 0; i < this._activeTexts.length; i++) { const el = this._activeTexts[i]; if (el instanceof ThreeMeshUI.Text && now - el["_activatedTime"] > 20000) { el.removeFromParent(); this._textBuffer.push(el); this._activeTexts.splice(i, 1); break; } } } } }; addLog(type, message) { const root = this.getRoot(); const text = this.getText(); let backgroundColor = 0xffffff; let fontColor = 0x000000; switch (type) { case "log": backgroundColor = 0xffffff; fontColor = 0x000000; break; case "warn": backgroundColor = 0xffee99; fontColor = 0x442200; break; case "error": backgroundColor = 0xffaaaa; fontColor = 0x770000; break; } if (message.length > 1000) message = message.substring(0, 1000) + "..."; const minuteSecondMilliSecond = new Date().toISOString().split("T")[1].split(".")[0]; text.textContent = "[" + minuteSecondMilliSecond + "] " + message; text.visible = true; text["_activatedTime"] = Date.now(); root.add(text); this._activeTexts.push(text); if (this.context) this.context.scene.add(root); text.set({ backgroundColor: backgroundColor, color: fontColor, }); ThreeMeshUI.update(); } ensureFont() { let fontFamily = ThreeMeshUI.FontLibrary.getFontFamily(this.familyName); if (!fontFamily) { fontFamily = ThreeMeshUI.FontLibrary.addFontFamily(this.familyName); const variant = fontFamily.addVariant("normal", "normal", "./include/needle/arial-msdf.json", "./include/needle/arial.png"); /** @ts-ignore */ variant?.addEventListener('ready', () => { ThreeMeshUI.update(); }); } } textOptions = { fontSize: this.defaultFontSize, fontFamily: this.familyName, padding: .03, margin: .005, color: 0x000000, backgroundColor: 0xffffff, backgroundOpacity: .4, borderRadius: .03, offset: .025, }; _textBuffer = []; _activeTexts = []; getText() { const root = this.getRoot(); if (this._textBuffer.length > 0) { const text = this._textBuffer.pop(); text.visible = true; setTimeout(() => this.disableDepthTestRecursive(text), 100); return text; } if (root.children.length > 20 && this._activeTexts.length > 0) { const active = this._activeTexts.shift(); return active; } const newText = new ThreeMeshUI.Text(this.textOptions); setTimeout(() => this.disableDepthTestRecursive(newText), 500); setTimeout(() => this.disableDepthTestRecursive(newText), 1500); return newText; } disableDepthTestRecursive(obj, level = 0) { for (let i = 0; i < obj.children.length; i++) { const child = obj.children[i]; if (child instanceof Object3D) { this.disableDepthTestRecursive(child, level + 1); } } obj.renderOrder = 10 * level; obj.layers.set(2); // obj.position.z = .01 * level; const mat = obj.material; if (mat) { mat.depthWrite = false; mat.depthTest = false; mat.transparent = true; } if (level === 0) ThreeMeshUI.update(); } getRoot() { if (this.root) { return this.root; } const fontSize = this.defaultFontSize; const defaultOptions = { boxSizing: 'border-box', fontFamily: this.familyName, width: "2.6", fontSize: fontSize, color: 0x000000, lineHeight: 1, backgroundColor: 0xffffff, backgroundOpacity: 0, // borderColor: 0xffffff, // borderOpacity: .5, // borderWidth: 0.01, // padding: 0.01, whiteSpace: 'pre-wrap', flexDirection: 'column-reverse', }; this.root = new ThreeMeshUI.Block(defaultOptions); return this.root; } } let messagesHandler = null; function onEnable() { // create messages handler if (!messagesHandler) messagesHandler = new SpatialMessagesHandler(); messagesHandler.onEnable(); // save original console methods for (const key in originalConsoleMethods) { originalConsoleMethods[key] = console[key]; let isLogging = false; console[key] = function () { // call original console method originalConsoleMethods[key]?.apply(console, arguments); // prevent circular calls if (isLogging) return; try { isLogging = true; onLog(key, ...arguments); } finally { isLogging = false; } }; } } function onDisable() { messagesHandler?.onDisable(); for (const key in originalConsoleMethods) { console[key] = originalConsoleMethods[key]; } } const seen = new Map(); function onLog(key, ...args) { try { seen.clear(); switch (key) { case "log": messagesHandler?.addLog("log", getLogString()); break; case "warn": messagesHandler?.addLog("warn", getLogString()); break; case "error": messagesHandler?.addLog("error", getLogString()); break; } } catch (err) { console.error("Error in spatial console", err); } finally { seen.clear(); } function getLogString() { let str = ""; for (let i = 0; i < args.length; i++) { const el = args[i]; str += serialize(el); if (i < args.length - 1) str += ", "; } return str; } function serialize(value, level = 0) { if (typeof value === "string") { return "\"" + value + "\""; } else if (typeof value === "number") { const hasDecimal = value % 1 !== 0; if (hasDecimal) { const str = value.toFixed(5); const dotIndex = str.indexOf("."); let i = str.length - 1; while (i > dotIndex && str[i] === "0") i--; return str.substring(0, i + 1); } return value.toString(); } else if (Array.isArray(value)) { let res = "["; for (let i = 0; i < value.length; i++) { const val = value[i]; res += serialize(val, level + 1); if (i < value.length - 1) res += ", "; } res += "]"; return res; } else if (value === null) { return "null"; } else if (value === undefined) { return "undefined"; } else if (typeof value === "function") { return value.name + "()"; } // if (value instanceof Object3D) { // const name = value.name?.length > 0 ? value.name : ("object@" + (value["guid"] ?? value.uuid));; // return name; // } if (value instanceof Vector2) return `(${serialize(value.x)}, ${serialize(value.y)})`; if (value instanceof Vector3) return `(${serialize(value.x)}, ${serialize(value.y)}, ${serialize(value.z)})`; if (value instanceof Vector4) return `(${serialize(value.x)}, ${serialize(value.y)}, ${serialize(value.z)}, ${serialize(value.w)})`; if (value instanceof Quaternion) return `(${serialize(value.x)}, ${serialize(value.y)}, ${serialize(value.z)}, ${serialize(value.w)})`; if (value instanceof Material) return value.name; if (value instanceof Texture) return value.name; if (value instanceof Matrix3) return `[${value.elements.join(", ")}]`; if (value instanceof Matrix4) return `[${value.elements.join(", ")}]`; if (value instanceof Layers) return value.mask.toString(); if (typeof value === "object") { if (seen.has(value)) return "*"; let res = "{\n"; res += pad(level); const keys = Object.keys(value); let line = ""; for (let i = 0; i < keys.length; i++) { const key = keys[i]; const val = value[key]; if (seen.has(val)) { line += ""; continue; } seen.set(val, true); line += key + ":" + serialize(val, level + 1); if (i < keys.length - 1) line += ", "; if (line.length >= 60) { line += "\n"; line += pad(level); res += line; line = ""; } } res += line; res += "\n}"; return res; // return JSON.stringify(value, (_key, value) => { // if (seen.has(value)) return seen.get(value); // seen.set(value, "-"); // const res = serialize(value); // seen.set(value, res); // return _key; // }, 1); } return value; } function pad(spaces) { let res = ""; for (let i = 0; i < spaces; i++) { res += " "; } return res; } } // // this is just a hack - the spatial console should be enabled from the user or the NeedleXRSession // if (getParam("debugwebxr") || getParam("console")) // setTimeout(() => enableSpatialConsole(true), 1000); //# sourceMappingURL=debug_spatial_console.js.map