@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
JavaScript
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