@playcanvas/web-components
Version:
Web Components for the PlayCanvas Engine
361 lines (315 loc) • 10.4 kB
text/typescript
import { AppBase, Entity, Vec3 } from 'playcanvas';
import { AsyncElement } from './async-element';
import { parseVec3 } from './utils';
/**
* The EntityElement interface provides properties and methods for manipulating
* {@link https://developer.playcanvas.com/user-manual/web-components/tags/pc-entity/ | `<pc-entity>`} elements.
* The EntityElement interface also inherits the properties and methods of the
* {@link HTMLElement} interface.
*/
class EntityElement extends AsyncElement {
/**
* Whether the entity is enabled.
*/
private _enabled = true;
/**
* The name of the entity.
*/
private _name = 'Untitled';
/**
* The position of the entity.
*/
private _position = new Vec3();
/**
* The rotation of the entity.
*/
private _rotation = new Vec3();
/**
* The scale of the entity.
*/
private _scale = new Vec3(1, 1, 1);
/**
* The tags of the entity.
*/
private _tags: string[] = [];
/**
* The pointer event listeners for the entity.
*/
private _listeners: { [key: string]: EventListener[] } = {};
/**
* The PlayCanvas entity instance.
*/
entity: Entity | null = null;
createEntity(app: AppBase) {
// Create a new entity
this.entity = new Entity(this.getAttribute('name') || this._name, app);
const enabled = this.getAttribute('enabled');
if (enabled) {
this.entity.enabled = enabled !== 'false';
}
const position = this.getAttribute('position');
if (position) {
this.entity.setLocalPosition(parseVec3(position));
}
const rotation = this.getAttribute('rotation');
if (rotation) {
this.entity.setLocalEulerAngles(parseVec3(rotation));
}
const scale = this.getAttribute('scale');
if (scale) {
this.entity.setLocalScale(parseVec3(scale));
}
const tags = this.getAttribute('tags');
if (tags) {
this.entity.tags.add(tags.split(',').map(tag => tag.trim()));
}
// Handle pointer events
const pointerEvents = [
'onpointerenter',
'onpointerleave',
'onpointerdown',
'onpointerup',
'onpointermove'
];
pointerEvents.forEach((eventName) => {
const handler = this.getAttribute(eventName);
if (handler) {
const eventType = eventName.substring(2); // remove 'on' prefix
const eventHandler = (event: Event) => {
try {
/* eslint-disable-next-line no-new-func */
new Function('event', handler).call(this, event);
} catch (e) {
console.error('Error in event handler:', e);
}
};
this.addEventListener(eventType, eventHandler);
}
});
}
buildHierarchy(app: AppBase) {
if (!this.entity) return;
const closestEntity = this.closestEntity;
if (closestEntity?.entity) {
closestEntity.entity.addChild(this.entity);
} else {
app.root.addChild(this.entity);
}
this._onReady();
}
connectedCallback() {
// Wait for app to be ready
const closestApp = this.closestApp;
if (!closestApp) return;
// If app is already running, create entity immediately
if (closestApp.hierarchyReady) {
const app = closestApp.app!;
this.createEntity(app);
this.buildHierarchy(app);
// Handle any child entities that might exist
const childEntities = this.querySelectorAll<EntityElement>('pc-entity');
childEntities.forEach((child) => {
child.createEntity(app);
});
childEntities.forEach((child) => {
child.buildHierarchy(app);
});
}
}
disconnectedCallback() {
if (this.entity) {
// Notify all children that their entities are about to become invalid
const children = this.querySelectorAll('pc-entity');
children.forEach((child) => {
(child as EntityElement).entity = null;
});
// Destroy the entity
this.entity.destroy();
this.entity = null;
}
}
/**
* Sets the enabled state of the entity.
* @param value - Whether the entity is enabled.
*/
set enabled(value) {
this._enabled = value;
if (this.entity) {
this.entity.enabled = value;
}
}
/**
* Gets the enabled state of the entity.
* @returns Whether the entity is enabled.
*/
get enabled() {
return this._enabled;
}
/**
* Sets the name of the entity.
* @param value - The name of the entity.
*/
set name(value) {
this._name = value;
if (this.entity) {
this.entity.name = value;
}
}
/**
* Gets the name of the entity.
* @returns The name of the entity.
*/
get name() {
return this._name;
}
/**
* Sets the position of the entity.
* @param value - The position of the entity.
*/
set position(value) {
this._position = value;
if (this.entity) {
this.entity.setLocalPosition(this._position);
}
}
/**
* Gets the position of the entity.
* @returns The position of the entity.
*/
get position() {
return this._position;
}
/**
* Sets the rotation of the entity.
* @param value - The rotation of the entity.
*/
set rotation(value) {
this._rotation = value;
if (this.entity) {
this.entity.setLocalEulerAngles(this._rotation);
}
}
/**
* Gets the rotation of the entity.
* @returns The rotation of the entity.
*/
get rotation() {
return this._rotation;
}
/**
* Sets the scale of the entity.
* @param value - The scale of the entity.
*/
set scale(value) {
this._scale = value;
if (this.entity) {
this.entity.setLocalScale(this._scale);
}
}
/**
* Gets the scale of the entity.
* @returns The scale of the entity.
*/
get scale() {
return this._scale;
}
/**
* Sets the tags of the entity.
* @param value - The tags of the entity.
*/
set tags(value) {
this._tags = value;
if (this.entity) {
this.entity.tags.clear();
this.entity.tags.add(this._tags);
}
}
/**
* Gets the tags of the entity.
* @returns The tags of the entity.
*/
get tags() {
return this._tags;
}
static get observedAttributes() {
return [
'enabled',
'name',
'position',
'rotation',
'scale',
'tags',
'onpointerenter',
'onpointerleave',
'onpointerdown',
'onpointerup',
'onpointermove'
];
}
attributeChangedCallback(name: string, _oldValue: string, newValue: string) {
switch (name) {
case 'enabled':
this.enabled = newValue !== 'false';
break;
case 'name':
this.name = newValue;
break;
case 'position':
this.position = parseVec3(newValue);
break;
case 'rotation':
this.rotation = parseVec3(newValue);
break;
case 'scale':
this.scale = parseVec3(newValue);
break;
case 'tags':
this.tags = newValue.split(',').map(tag => tag.trim());
break;
case 'onpointerenter':
case 'onpointerleave':
case 'onpointerdown':
case 'onpointerup':
case 'onpointermove':
if (newValue) {
const eventName = name.substring(2);
// Use Function.prototype.bind to avoid new Function
const handler = (event: Event) => {
try {
const handlerStr = this.getAttribute(eventName) || '';
/* eslint-disable-next-line no-new-func */
new Function('event', handlerStr).call(this, event);
} catch (e) {
console.error('Error in event handler:', e);
}
};
this.addEventListener(eventName, handler);
}
break;
}
}
addEventListener(type: string, listener: EventListener, options?: boolean | AddEventListenerOptions) {
if (!this._listeners[type]) {
this._listeners[type] = [];
}
this._listeners[type].push(listener);
super.addEventListener(type, listener, options);
if (type.startsWith('pointer')) {
this.dispatchEvent(new CustomEvent(`${type}:connect`, { bubbles: true }));
}
}
removeEventListener(type: string, listener: EventListener, options?: boolean | EventListenerOptions) {
if (this._listeners[type]) {
this._listeners[type] = this._listeners[type].filter(l => l !== listener);
}
super.removeEventListener(type, listener, options);
if (type.startsWith('pointer')) {
this.dispatchEvent(new CustomEvent(`${type}:disconnect`, { bubbles: true }));
}
}
hasListeners(type: string): boolean {
return Boolean(this._listeners[type]?.length);
}
}
customElements.define('pc-entity', EntityElement);
export { EntityElement };