playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
241 lines (240 loc) • 8.39 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { Debug } from "../../core/debug.js";
import { EventHandler } from "../../core/event-handler.js";
import { Color } from "../../core/math/color.js";
import { Vec2 } from "../../core/math/vec2.js";
import { Vec3 } from "../../core/math/vec3.js";
import { Vec4 } from "../../core/math/vec4.js";
class ComponentSystem extends EventHandler {
/**
* Create a new ComponentSystem instance.
*
* @param {AppBase} app - The application managing this system.
*/
constructor(app) {
super();
/**
* The id type of the ComponentSystem.
*
* @type {string}
* @readonly
*/
__publicField(this, "id");
/**
* A list of option names accepted by {@link ComponentSystem#addComponent} that are not settable
* properties of the component itself - for example keys the system consumes to build derived
* state (such as `aabbCenter`) or deprecated aliases. Used only by debug-build validation to
* avoid false-positive warnings; subclasses that accept such options should override this.
*
* @type {string[]}
* @ignore
*/
__publicField(this, "extraDataProperties", []);
/**
* Cache of option names already validated as acceptable by {@link ComponentSystem#addComponent},
* lazily populated the first time each option is seen. Debug builds only.
*
* @type {Set<string>|null}
* @ignore
*/
__publicField(this, "_validProps", null);
this.app = app;
this.store = {};
this.schema = [];
}
/**
* Create new {@link Component} and component data instances and attach them to the entity.
*
* @param {Entity} entity - The Entity to attach this component to.
* @param {object} [data] - The source data with which to create the component.
* @returns {Component} Returns a Component of type defined by the component system.
* @example
* const entity = new Entity(app);
* app.systems.model.addComponent(entity, { type: 'box' });
* // entity.model is now set to a ModelComponent
* @ignore
*/
addComponent(entity, data = {}) {
const component = new this.ComponentType(this, entity);
const componentData = this.DataType ? new this.DataType() : {};
this.store[entity.guid] = {
entity,
data: componentData
};
entity[this.id] = component;
entity.c[this.id] = component;
Debug.call(() => validateComponentOptions(this, component, data));
this.initializeComponentData(component, data);
this.fire("add", entity, component);
return component;
}
/**
* Remove the {@link Component} from the entity and delete the associated component data.
*
* @param {Entity} entity - The entity to remove the component from.
* @example
* app.systems.model.removeComponent(entity);
* // entity.model === undefined
* @ignore
*/
removeComponent(entity) {
const id = this.id;
const record = this.store[entity.guid];
const component = entity.c[id];
component.fire("beforeremove");
this.fire("beforeremove", entity, component);
delete this.store[entity.guid];
entity[id] = void 0;
delete entity.c[id];
this.fire("remove", entity, record.data);
}
/**
* Create a clone of component. This creates a copy of all component data variables.
*
* @param {Entity} entity - The entity to clone the component from.
* @param {Entity} clone - The entity to clone the component into.
* @returns {Component} The newly cloned component.
* @ignore
*/
cloneComponent(entity, clone) {
const src = this.store[entity.guid];
return this.addComponent(clone, src.data);
}
/**
* Called during {@link addComponent} to initialize the component data in the store. This can
* be overridden by derived Component Systems and either called by the derived System or
* replaced entirely.
*
* @param {Component} component - The component being initialized.
* @param {object} data - The data block used to initialize the component.
* @param {Array<string | {name: string, type: string}>} [properties] - The array of property
* descriptors to initialize from the data block. A descriptor can be either a plain property
* name, or an object specifying the name and type. This is a legacy path for external
* schema-based components - when omitted, the enabled state is initialized from the data
* block instead. Callers that handle the enabled state themselves pass an empty array.
* @ignore
*/
initializeComponentData(component, data = {}, properties) {
if (properties) {
for (let i = 0, len = properties.length; i < len; i++) {
const descriptor = properties[i];
let name, type;
if (typeof descriptor === "object") {
name = descriptor.name;
type = descriptor.type;
} else {
name = descriptor;
type = void 0;
}
let value = data[name];
if (value !== void 0) {
if (type !== void 0) {
value = convertValue(value, type);
}
component[name] = value;
} else if (component.data && name in component.data) {
component[name] = component.data[name];
}
}
} else if (data.enabled !== void 0) {
component.enabled = data.enabled;
}
if (component.enabled && component.entity.enabled) {
component.onEnable();
}
}
/**
* Searches the component schema for properties that match the specified type.
*
* @param {string} type - The type to search for.
* @returns {string[]|object[]} An array of property descriptors matching the specified type.
* @ignore
*/
getPropertiesOfType(type) {
const matchingProperties = [];
const schema = this.schema || [];
schema.forEach((descriptor) => {
if (descriptor && typeof descriptor === "object" && descriptor.type === type) {
matchingProperties.push(descriptor);
}
});
return matchingProperties;
}
destroy() {
this.off();
}
}
function convertValue(value, type) {
if (!value) {
return value;
}
switch (type) {
case "rgb":
if (value instanceof Color) {
return value.clone();
}
return new Color(value[0], value[1], value[2]);
case "rgba":
if (value instanceof Color) {
return value.clone();
}
return new Color(value[0], value[1], value[2], value[3]);
case "vec2":
if (value instanceof Vec2) {
return value.clone();
}
return new Vec2(value[0], value[1]);
case "vec3":
if (value instanceof Vec3) {
return value.clone();
}
return new Vec3(value[0], value[1], value[2]);
case "vec4":
if (value instanceof Vec4) {
return value.clone();
}
return new Vec4(value[0], value[1], value[2], value[3]);
case "boolean":
case "number":
case "string":
return value;
case "entity":
return value;
// Entity fields should just be a string guid
default:
throw new Error(`Could not convert unhandled type: ${type}`);
}
}
function isSettableProperty(obj, key) {
let settable = false;
Debug.call(() => {
for (let proto = obj; proto && proto !== Object.prototype; proto = Object.getPrototypeOf(proto)) {
const descriptor = Object.getOwnPropertyDescriptor(proto, key);
if (descriptor) {
settable = descriptor.set !== void 0 || descriptor.writable === true;
return;
}
}
});
return settable;
}
function validateComponentOptions(system, component, data) {
Debug.call(() => {
const valid = system._validProps ?? (system._validProps = /* @__PURE__ */ new Set(["enabled", ...system.extraDataProperties]));
for (const key of Object.keys(data)) {
if (valid.has(key)) {
continue;
}
if (isSettableProperty(component, key)) {
valid.add(key);
} else {
Debug.warnOnce(`addComponent: ignoring unknown option '${key}' passed to the '${system.id}' component - check for a typo.`);
}
}
});
}
export {
ComponentSystem
};