Skip to content

Commit

Permalink
Common interface for async node initialization (#46)
Browse files Browse the repository at this point in the history
* Common interface for async node initialization

* Remove console.logs

* Add Rotunda equirect

* Add rotunda equirect

* Improve VR support

* Tweak example

* Have ready return the element
  • Loading branch information
willeastcott authored Nov 12, 2024
1 parent ae0aa9d commit 4a6fe68
Show file tree
Hide file tree
Showing 19 changed files with 182 additions and 140 deletions.
24 changes: 24 additions & 0 deletions examples/animation.html
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,29 @@
</pc-entity>
</pc-scene>
</pc-app>
<button id="xr-button" style="position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); display: none;">
Enter VR
</button>
<script>
// Add VR support
document.addEventListener('DOMContentLoaded', async () => {
const camera = await document.querySelector('pc-camera').ready();

if (camera.xrAvailable) {
const button = document.getElementById('xr-button');
button.style.display = 'block';

button.addEventListener('click', () => {
camera.startXr();
});

window.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
camera.endXr();
}
});
}
});
</script>
</body>
</html>
Binary file not shown.
2 changes: 1 addition & 1 deletion examples/screen.html
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<!-- Scene -->
<pc-scene>
<!-- Camera -->
<pc-entity name="camera" position="0,0,5">
<pc-entity name="camera">
<pc-camera clear-color="#000000"></pc-camera>
</pc-entity>
<!-- Screen -->
Expand Down
3 changes: 2 additions & 1 deletion examples/spinning-cube-api.html
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
// Use the DOM API to programmatically create the scene
const app = document.createElement('pc-app');
document.body.appendChild(app);
await app.getApplication();

await app.ready();

class Rotate extends Script {
update(dt) {
Expand Down
4 changes: 3 additions & 1 deletion examples/splat.html
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,12 @@
<pc-app>
<!-- Assets -->
<pc-asset id="orbit-camera" src="scripts/orbit-camera.mjs" preload></pc-asset>
<pc-asset id="helipad" src="assets/helipad-env-atlas.png" preload></pc-asset>
<pc-asset id="angel" src="assets/angel.compressed.ply" preload></pc-asset>
<pc-asset id="rotunda" src="assets/sky/sepulchral-chapel-rotunda-8k.webp" preload></pc-asset>
<!-- Scene -->
<pc-scene>
<!-- Sky -->
<pc-sky asset="rotunda"></pc-sky>
<!-- Camera -->
<pc-entity name="camera" position="0,1,3.5">
<pc-camera></pc-camera>
Expand Down
27 changes: 12 additions & 15 deletions src/app.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
import { Application, FILLMODE_FILL_WINDOW, Keyboard, Mouse, RESOLUTION_AUTO } from 'playcanvas';

import { AssetElement } from './asset';
import { AsyncElement } from './async-element';
import { MaterialElement } from './material';
import { ModuleElement } from './module';

/**
* The main application element.
*/
class AppElement extends HTMLElement {
class AppElement extends AsyncElement {
/**
* The canvas element.
*/
private _canvas: HTMLCanvasElement | null = null;

private appReadyPromise: Promise<Application>;

private appReadyResolve!: (app: Application) => void;

private _highResolution = false;

/**
Expand All @@ -32,10 +29,6 @@ class AppElement extends HTMLElement {

// Bind methods to maintain 'this' context
this._onWindowResize = this._onWindowResize.bind(this);

this.appReadyPromise = new Promise<Application>((resolve) => {
this.appReadyResolve = resolve;
});
}

async connectedCallback() {
Expand Down Expand Up @@ -84,7 +77,7 @@ class AppElement extends HTMLElement {
// Handle window resize to keep the canvas responsive
window.addEventListener('resize', this._onWindowResize);

this.appReadyResolve(this.app!);
this._onReady();
});
}

Expand All @@ -105,24 +98,28 @@ class AppElement extends HTMLElement {
}
}

async getApplication(): Promise<Application> {
await this.appReadyPromise;
return this.app!;
}

_onWindowResize() {
if (this.app) {
this.app.resizeCanvas();
}
}

/**
* Sets the high resolution flag. When true, the application will render at the device's
* physical resolution. When false, the application will render at CSS resolution.
* @param value - The high resolution flag.
*/
set highResolution(value: boolean) {
this._highResolution = value;
if (this.app) {
this.app.graphicsDevice.maxPixelRatio = value ? window.devicePixelRatio : 1;
}
}

/**
* Gets the high resolution flag.
* @returns The high resolution flag.
*/
get highResolution() {
return this._highResolution;
}
Expand Down
3 changes: 0 additions & 3 deletions src/asset.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,6 @@ class AssetElement extends HTMLElement {
*/
asset: Asset | null = null;

async connectedCallback() {
}

disconnectedCallback() {
this.destroyAsset();
}
Expand Down
45 changes: 45 additions & 0 deletions src/async-element.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { AppElement } from './app';
import { EntityElement } from './entity';

/**
* Base class for all PlayCanvas web components that initialize asynchronously.
*/
class AsyncElement extends HTMLElement {
private _readyPromise: Promise<void>;

private _readyResolve!: () => void;

constructor() {
super();
this._readyPromise = new Promise<void>((resolve) => {
this._readyResolve = resolve;
});
}

get closestApp(): AppElement {
return this.parentElement?.closest('pc-app') as AppElement;
}

get closestEntity(): EntityElement {
return this.parentElement?.closest('pc-entity') as EntityElement;
}

/**
* Called when the element is fully initialized and ready.
* Subclasses should call this when they're ready.
*/
protected _onReady() {
this._readyResolve();
this.dispatchEvent(new CustomEvent('ready'));
}

/**
* Returns a promise that resolves with this element when it's ready.
* @returns A promise that resolves with this element when it's ready.
*/
ready(): Promise<this> {
return this._readyPromise.then(() => this);
}
}

export { AsyncElement };
23 changes: 22 additions & 1 deletion src/components/camera-component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { PROJECTION_ORTHOGRAPHIC, PROJECTION_PERSPECTIVE, CameraComponent, Color, Vec4 } from 'playcanvas';
import { PROJECTION_ORTHOGRAPHIC, PROJECTION_PERSPECTIVE, CameraComponent, Color, Vec4, XRSPACE_LOCAL, XRTYPE_VR } from 'playcanvas';

import { ComponentElement } from './component';
import { parseColor, parseVec4 } from '../utils';
Expand Down Expand Up @@ -66,6 +66,27 @@ class CameraComponentElement extends ComponentElement {
};
}

get xrAvailable() {
const xrManager = this.component?.system.app.xr;
return xrManager && xrManager.supported && xrManager.isAvailable(XRTYPE_VR);
}

startXr() {
if (this.component && this.xrAvailable) {
this.component.startXr(XRTYPE_VR, XRSPACE_LOCAL, {
callback: (err: any) => {
if (err) console.error(`WebXR Immersive VR failed to start: ${err.message}`);
}
});
}
}

endXr() {
if (this.component) {
this.component.endXr();
}
}

/**
* Gets the camera component.
* @returns The camera component.
Expand Down
41 changes: 15 additions & 26 deletions src/components/component.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,13 @@
import { Component } from 'playcanvas';

import { AppElement } from '../app';
import { EntityElement } from '../entity';
import { AsyncElement } from '../async-element';

/**
* Represents a component in the PlayCanvas engine.
*
* @category Components
*/
class ComponentElement extends HTMLElement {
class ComponentElement extends AsyncElement {
private _componentName: string;

private _enabled = true;
Expand All @@ -30,33 +29,23 @@ class ComponentElement extends HTMLElement {
return {};
}

async connectedCallback() {
const appElement = this.closest('pc-app') as AppElement | null;
if (!appElement) {
console.error(`${this.tagName.toLowerCase()} should be a descendant of pc-app`);
return;
async addComponent() {
const entityElement = this.closestEntity;
if (entityElement) {
await entityElement.ready();
// Add the component to the entity
const data = this.getInitialComponentData();
this._component = entityElement.entity!.addComponent(this._componentName, data);
}

await appElement.getApplication();

this.addComponent();
}

addComponent() {
// Access the parent pc-entity's 'entity' property
const entityElement = this.closest('pc-entity') as EntityElement | null;
if (!entityElement) {
console.error(`${this.tagName.toLowerCase()} should be a child of pc-entity`);
return;
}
initComponent() {}

if (entityElement && entityElement.entity) {
// Add the component to the entity
this._component = entityElement.entity.addComponent(
this._componentName,
this.getInitialComponentData()
);
}
async connectedCallback() {
await this.closestApp?.ready();
await this.addComponent();
this.initComponent();
this._onReady();
}

disconnectedCallback() {
Expand Down
4 changes: 1 addition & 3 deletions src/components/element-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,7 @@ class ElementComponentElement extends ComponentElement {
super('element');
}

async connectedCallback() {
await super.connectedCallback();

initComponent() {
this.component!._text._material.useFog = true;
}

Expand Down
7 changes: 1 addition & 6 deletions src/components/render-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,11 @@ class RenderComponentElement extends ComponentElement {
super('render');
}

async connectedCallback() {
await super.connectedCallback();

this.material = this._material;
}

getInitialComponentData() {
return {
type: this._type,
castShadows: this._castShadows,
material: MaterialElement.get(this._material),
receiveShadows: this._receiveShadows
};
}
Expand Down
4 changes: 1 addition & 3 deletions src/components/script-component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,7 @@ class ScriptComponentElement extends ComponentElement {
this.addEventListener('scriptenablechange', this.handleScriptEnableChange.bind(this));
}

async connectedCallback() {
await super.connectedCallback();

initComponent() {
// Handle initial script elements
this.querySelectorAll<ScriptElement>(':scope > pc-script').forEach((scriptElement) => {
const scriptName = scriptElement.getAttribute('name');
Expand Down
17 changes: 7 additions & 10 deletions src/components/sound-slot.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { SoundSlot } from 'playcanvas';

import { AppElement } from '../app';
import { SoundComponentElement } from './sound-component';
import { AssetElement } from '../asset';
import { AsyncElement } from '../async-element';
import { SoundComponentElement } from './sound-component';

/**
* Represents a sound slot in the PlayCanvas engine.
*/
class SoundSlotElement extends HTMLElement {
class SoundSlotElement extends AsyncElement {
private _asset: string = '';

private _autoPlay: boolean = false;
Expand All @@ -32,13 +32,7 @@ class SoundSlotElement extends HTMLElement {
soundSlot: SoundSlot | null = null;

async connectedCallback() {
const appElement = this.closest('pc-app') as AppElement | null;
if (!appElement) {
console.error(`${this.tagName.toLowerCase()} should be a descendant of pc-app`);
return;
}

await appElement.getApplication();
await this.soundElement?.ready();

const options = {
autoPlay: this._autoPlay,
Expand All @@ -51,9 +45,12 @@ class SoundSlotElement extends HTMLElement {
if (this._duration) {
options.duration = this._duration;
}

this.soundSlot = this.soundElement!.component!.addSlot(this._name, options);
this.asset = this._asset;
this.soundSlot!.play();

this._onReady();
}

disconnectedCallback() {
Expand Down
Loading

0 comments on commit 4a6fe68

Please sign in to comment.