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.

195 lines (161 loc) 7.37 kB
import { writeFile } from 'fs'; import { tryParseNeedleEngineSrcAttributeFromHtml } from '../common/needle-engine.js'; import { needleLog } from './logging.js'; /** * @param {'serve' | 'build'} command * @param {{} | undefined | null} config * @param {import('../types').userSettings} userSettings * @returns {import('vite').Plugin[] | null} */ export function needleApp(command, config, userSettings) { if (command !== "build") { return null; } /** @type {Array<import("rollup").OutputChunk>} */ const entryFiles = new Array(); let outputDir = "dist"; /** * @type {import('vite').Plugin} */ return [ { name: 'needle:app', enforce: "post", configResolved(config) { outputDir = config.build.outDir || "dist"; }, transformIndexHtml: { handler: async function (html, context) { const name = context.filename; if (name.includes("index.html")) { if (context.chunk?.isEntry) { try { entryFiles.push(context.chunk); const path = context.chunk.fileName; // console.log("[needle-dependencies] entry chunk imports", { // name: context.chunk.fileName, // imports: context.chunk.imports, // dynamicImports: context.chunk.dynamicImports, // refs: context.chunk.referencedFiles, // }); // TODO: here we try to find the main asset (entrypoint) for this web app. It's a bit hacky right now but we have to assign this url directly to make it work with `gen.js` where multiple needle-apps are on different (or the same) pages. const attribute_src = tryParseNeedleEngineSrcAttributeFromHtml(html); const imported_glbs = Array.from(context.chunk.viteMetadata?.importedAssets?.values() || [])?.filter(f => f.endsWith(".glb") || f.endsWith(".gltf")); const main_asset = attribute_src?.[0] || imported_glbs?.[0]; const webComponent = generateNeedleEmbedWebComponent(path, main_asset); await writeFile(`${outputDir}/needle-app.js`, webComponent, (err) => { if (err) { needleLog("needle-app", "Could not create needle-app.js: " + err.message, "error", { dimBody: false }); } else { needleLog("needle-app", "Created needle-app.js", "log", { dimBody: false }); } }); } catch (e) { needleLog("needle-app", "Could not create needle-app.js: " + e.message, "warn", { dimBody: false }); } } } } }, } ] } /** * @param {string} filepath * @param {string | null} src * @returns {string} */ function generateNeedleEmbedWebComponent(filepath, src) { // filepath is e.g. `assets/index-XXXXXXXX.js` // we want to make sure the path is correct relative to where the component will be used // this script will be emitted in the output directory root (e.g. needle-embed.js) const componentDefaultName = 'needle-app'; return ` // Needle Engine attributes we want to allow to be overriden const knownAttributes = [ "src", "background-color", "background-image", "environment-image", "focus-rect", ]; const scriptUrl = new URL(import.meta.url); const componentName = scriptUrl.searchParams.get("component-name") || '${componentDefaultName}'; if (!customElements.get(componentName)) { console.debug(\`Defining needle-app as <\${componentName}>\`); customElements.define(componentName, class extends HTMLElement { static get observedAttributes() { return knownAttributes; } constructor() { super(); this.attachShadow({ mode: 'open' }); const template = document.createElement('template'); template.innerHTML = \` <style> :host { position: relative; display: block; width: max(360px 100%); height: max(240px, 100%); margin: 0; padding: 0; } needle-engine { position: absolute; top: 0; left: 0; width: 100%; height: 100%; } </style> \`; this.shadowRoot.appendChild(template.content.cloneNode(true)); const script = document.createElement('script'); script.type = 'module'; const url = new URL('.', import.meta.url); this.basePath = this.getAttribute('base-path') || \`\${url.protocol}//\${url.host}\${url.pathname}\`; while(this.basePath.endsWith('/')) { this.basePath = this.basePath.slice(0, -1); } script.src = this.getAttribute('script-src') || \`\${this.basePath}/${filepath}\`; this.shadowRoot.appendChild(script); this.needleEngine = document.createElement('needle-engine'); this.updateAttributes(); this.shadowRoot.appendChild(this.needleEngine); console.debug(this.basePath, script.src, this.needleEngine.getAttribute("src")); } onConnectedCallback() { console.debug('NeedleEmbed connected to the DOM'); } disconnectedCallback() { console.debug('NeedleEmbed disconnected from the DOM'); } attributeChangedCallback(name, oldValue, newValue) { console.debug(\`NeedleApp attribute changed: \${name} from \${oldValue} to \${newValue}\`); this.updateAttributes(); } updateAttributes() { console.debug("NeedleApp updating attributes"); const src = this.getAttribute('src') || ${src?.length ? `\`\${this.basePath}/${src}\`` : null}; if(src) this.needleEngine.setAttribute("src", src); else this.needleEngine.removeAttribute("src"); for(const attr of knownAttributes) { if(attr === "src") continue; // already handled above if(this.hasAttribute(attr)) { this.needleEngine.setAttribute(attr, this.getAttribute(attr)); } else { this.needleEngine.removeAttribute(attr); } } } }); } else { console.warn(\`needle-app <\${componentName}> already defined.\`); } ` }