@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.
334 lines (311 loc) • 13 kB
text/typescript
import { isLocalNetwork } from "../engine_networking_utils.js";
import { DeviceUtilities,getParam } from "../engine_utils.js";
import { isDevEnvironment } from "./debug.js";
import { getErrorCount, makeErrorsVisibleForDevelopment } from "./debug_overlay.js";
let consoleInstance: VConsole | null | undefined = undefined;
let consoleHtmlElement: HTMLElement | null = null;
let consoleSwitchButton: HTMLElement | null = null;
let isLoading = false;
let isVisible = false;
let watchInterval: any = null;
const defaultButtonIcon = "terminal";
const showConsole = getParam("console");
const suppressConsole = getParam("noerrors") || getParam("noconsole") || window.crossOriginIsolated;
if (showConsole) {
showDebugConsole();
}
if (!suppressConsole && (showConsole || isLocalNetwork())) {
if (isLocalNetwork() && !showConsole) {
const consoleUrl = new URL(window.location.href);
consoleUrl.searchParams.set("console", "1");
console.log("🌵 Tip: You can add the \"?console\" query parameter to the url to show the debug console (on mobile it will automatically open in the bottom right corner when your get errors during development. In VR a spatial console will appear.)", "\nOpen this page to get the console: " + consoleUrl.toString());
}
const enableConsole = DeviceUtilities.isMobileDevice() || (DeviceUtilities.isQuest() && isDevEnvironment());
if (enableConsole || showConsole) {
// we need to invoke this here - otherwise we will miss errors that happen after the console is loaded
// and calling the method from the root needle-engine.ts import is evaluated later (if we import the method from the toplevel file and then invoke it)
makeErrorsVisibleForDevelopment();
beginWatchingLogs();
createConsole(true);
if (enableConsole) {
const engineElement = document.querySelector("needle-engine");
engineElement?.addEventListener("enter-ar", () => {
if (showConsole || consoleInstance || getErrorCount() > 0) {
if (getParam("noerrors")) return;
}
});
engineElement?.addEventListener("exit-ar", () => {
onResetConsoleElementToDefaultParent();
});
}
}
}
const $defaultConsoleParent = Symbol("consoleParent");
export function showDebugConsole() {
if (consoleInstance) {
isVisible = true;
consoleInstance.showSwitch();
return;
}
createConsole();
}
export function hideDebugConsole() {
if (!consoleInstance) return;
isVisible = false;
consoleInstance.hide();
consoleInstance.hideSwitch();
}
function beginWatchingLogs() {
if (watchInterval) return;
watchInterval = setInterval(consoleElementUpdateInterval, 500);
}
let lastErrorCount = 0;
function consoleElementUpdateInterval() {
const currentCount = getErrorCount();
const receivedNewErrors = currentCount !== lastErrorCount;
lastErrorCount = currentCount;
if (receivedNewErrors) {
onNewConsoleErrors();
}
}
function onNewConsoleErrors() {
showDebugConsole();
if (consoleSwitchButton) {
consoleSwitchButton.setAttribute("error", "true");
consoleSwitchButton.innerText = "🤬"
}
}
function onConsoleSwitchButtonClicked() {
if (consoleSwitchButton) {
consoleSwitchButton.removeAttribute("error");
consoleSwitchButton.innerText = defaultButtonIcon;
}
}
function onResetConsoleElementToDefaultParent() {
if (consoleHtmlElement && consoleHtmlElement[$defaultConsoleParent]) {
consoleHtmlElement[$defaultConsoleParent].appendChild(consoleHtmlElement);
}
}
declare class VConsole {
addPlugin: (plugin: any) => void;
setSwitchPosition: (x: number, y: number) => void;
show: () => void;
hide: () => void;
hideSwitch: () => void;
showSwitch: () => void;
constructor(options: {
defaultPlugins?: string[],
pluginOrder?: string[],
});
static VConsolePlugin: any;
}
declare class PluginBtn {
name: string;
className?: string;
data?: any;
onClick: (event: Event, data: {type: string}) => void;
}
function createConsole(startHidden: boolean = false) {
if (consoleInstance !== undefined) return;
if (isLoading) return;
isLoading = true;
const script = document.createElement("script");
script.onload = () => {
// check if VConsole is now defined on globalThis
if (!globalThis.VConsole) {
console.warn("🌵 Debug console failed to load.");
isLoading = false;
consoleInstance = null;
return;
}
isLoading = false;
isVisible = true;
beginWatchingLogs();
consoleInstance = new VConsole({
// defaultPlugins: ['system', 'network'],
pluginOrder: ['default', 'needle-console'],
}) as VConsole;
const files = globalThis["needle:codegen_files"];
if (files && files.length > 0) {
consoleInstance.addPlugin(createInspectPlugin());
}
consoleHtmlElement = getConsoleElement();
if (consoleHtmlElement) {
consoleHtmlElement[$defaultConsoleParent] = consoleHtmlElement.parentElement;
consoleHtmlElement.style.position = "absolute";
consoleHtmlElement.style.zIndex = Number.MAX_SAFE_INTEGER.toString();
// const styleSheetList = document.styleSheets;
// for (let i = 0; i < styleSheetList.length; i++) {
// const styleSheet = styleSheetList[i];
// const firstRule = styleSheet.cssRules[0] as CSSStyleRule;
// if(firstRule && firstRule.selectorText === "#__vconsole") {
// console.log("found vconsole style sheet");
// const styleTag = document.createElement("style");
// styleTag.innerHTML = "#__needleconsole {}";
// for (let j = 0; j < styleSheet.cssRules.length; j++) {
// const rule = styleSheet.cssRules[j] as CSSStyleRule;
// styleTag.innerHTML += rule.cssText;
// }
// consoleHtmlElement.appendChild(styleTag);
// }
// }
}
consoleInstance.setSwitchPosition(20, 30);
consoleSwitchButton = getConsoleSwitchButton();
if (consoleSwitchButton) {
consoleSwitchButton.innerText = defaultButtonIcon;
consoleSwitchButton.addEventListener("click", onConsoleSwitchButtonClicked);
const styles = document.createElement("style");
const size = 40;
styles.innerHTML = `
#__vconsole .vc-switch {
border: 1px solid rgba(255, 255, 255, .1);
border-radius: 50%;
width: ${size}px;
height: ${size}px;
padding: 0;
line-height: ${size}px;
font-size: ${size * .4}px;
text-align: center;
background: #ffffff5c;
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
user-select: none;
pointer-events: auto;
transition: transform .2s ease-in-out;
box-shadow: 0px 7px 0.5rem 0px rgb(0 0 0 / 6%), inset 0px 0px 1.3rem rgba(0,0,0,.05);
font-family: 'Material Symbols Outlined';
color: black;
font-size: 2.3em;
font-weight: 100;
}
#__vconsole .vc-switch:hover {
cursor: pointer;
transform: scale(1.1);
transition: transform .1s ease-in-out, background .1s linear;
background: rgba(245, 245, 245, .8);
outline: rgba(0, 0, 0, .05) 1px solid;
}
#__vconsole .vc-switch[error] {
background: rgba(255,0,0,.2);
animation: vconsole-notify 1s ease-in-out;
line-height: 35px;
}
@keyframes vconsole-notify {
from {
transform: scale(1, 1);
}
10% {
transform: scale(1.3, 1.3);
}
70% {
transform: scale(1.4, 1.4);
}
to {
transform: scale(1, 1);
}
}
#__vconsole .vc-panel {
font-family: monospace;
font-size: 11px;
}
#__vconsole .vc-plugin-box.vc-actived {
height: 100%;
}
#__vconsole .vc-mask {
overflow: hidden;
}
`;
consoleHtmlElement?.prepend(styles);
if (startHidden === true && getErrorCount() <= 0)
hideDebugConsole();
console.log("🌵 Debug console has loaded");
}
};
script.onerror = () => {
console.warn("🌵 Debug console failed to load." + (window.crossOriginIsolated ? "This page is using cross-origin isolation, so external scripts can't be loaded." : ""));
isLoading = false;
consoleInstance = null;
};
script.src = "https://cdn.jsdelivr.net/npm/vconsole@3.9.1/dist/vconsole.min.js";
document.body.appendChild(script);
}
function createInspectPlugin() {
if (!globalThis.VConsole) return;
const plugin = new VConsole.VConsolePlugin("needle-console", "🌵 Inspect glTF");
const getIframe = () => {
return document.querySelector("#__vc_plug_" + plugin._id + " iframe") as HTMLIFrameElement;
}
plugin.on('renderTab', function(callback) {
const files = globalThis["needle:codegen_files"];
if (!files || files.length === 0) return;
let query = globalThis["needle:codegen_files"][0];
const index = query.indexOf("?");
if (index > -1) query = query.substring(0, index);
const currentAbsolutePath = location.protocol + '//' + location.host + location.pathname;
const currentPath = currentAbsolutePath + "/" + query;
const urlEncoded = encodeURIComponent(currentPath);
plugin.fullUrl = "https://viewer.needle.tools?inspect&file=" + urlEncoded;
var html = `<iframe src="" style="width: 100%; height: 99%; border: none;"></iframe>`;
callback(html);
});
plugin.on('show', function() {
const elem = getIframe();
if (elem && elem.src !== plugin.fullUrl) elem.src = plugin.fullUrl;
});
plugin.on('hide', function() {
const elem = getIframe();
if (elem) elem.src = "";
});
/* bottom tool bar
plugin.on('addTool', function(callback) {
var button = {
name: 'Reload',
onClick: function(event) {
location.reload();
}
};
callback([button]);
});
*/
plugin.on('addTopBar', function(callback) {
var btnList = new Array<PluginBtn>();
btnList.push({
name: 'Open in new window ↗',
onClick: function(_event) {
window.open(plugin.fullUrl, '_blank');
consoleInstance?.hide();
}
});
btnList.push({
name: 'Reload',
onClick: function(_event) {
const iframe = getIframe();
if (iframe) iframe.src = plugin.fullUrl;
}
});
btnList.push({
name: 'Fullscreen',
onClick: function(_event) {
const iframe = getIframe();
if (iframe.requestFullscreen) {
iframe.requestFullscreen();
} else if (iframe["webkitRequestFullscreen"] instanceof Function) {
iframe["webkitRequestFullscreen"]();
}
}
});
callback(btnList);
});
return plugin;
}
function getConsoleSwitchButton(): HTMLElement | null {
const el = document.querySelector("#__vconsole .vc-switch");
if (el) return el as HTMLElement;
return null;
}
function getConsoleElement(): HTMLElement | null {
const el = document.querySelector("#__vconsole");
if (el) return el as HTMLElement;
return null;
}