UNPKG

onsight

Version:

Interactive, easy to use JavaScript game framework.

380 lines (317 loc) 15.7 kB
import { MathUtils } from '../utils/MathUtils.js'; import { SysUtils } from '../utils/SysUtils.js'; // REGISTRATION // registered() Returns class definition of a registered type // registeredTypes() Returns an array of all types registered with Component Manager // register() Registers a component type with the internal Component Manager // DATA // includeData() Checks if a schema item should be included in a component data structure // sanitizeData() Build default data of 'type', remove any properties that should not be included // stripData() Removes data from 'newData' if conditions from schema on 'newData' don't match 'oldData' /******************** PROPERTY TYPES ********************/ // // NOTE: See 'test' component for examples of all property types // // TYPE DESCRIPTION DEFAULT OPTIONS (DEFAULT) // // -- DATA TYPES -- // select Dropdown null // // number Number Box 0 min (-inf), max (inf), step (1), unit (''), label, precision (3) // int Whole Number 0 min (-inf), max (inf), step (1), unit (''), label // angle Degrees, converted to Radians 0 min (-inf), max (inf), step (10), unit ('°'), label, precision (3) // slider Number Slider 0 min (-inf), max (inf), step ('any'), precision (2) // // variable Number with +/- Number [ 0, 0 ] min[], max[], step[], unit[], precision[] // vector Number Array [ 0 ] size: array length (1), tint (false), aspect (false) - also min[], max[], step[], unit[], precision[], label[] // option Bool Array [ false ] size: array length (1), tint (false) // // boolean Option / Checkbox false // color Color Selector 0xffffff // string String / Multiline? '' rows (1) // key Keyboard Key '' // // -- UUID TYPES -- // asset Asset (asset.uuid) null class: (all) or (geometry, material, script, shape, texture, prefab) // // -- OBJECT TYPES -- // object Data object {} // // -- FORMATTING -- // divider Inserts horizontal rule --- Used for formatting / layout only // hidden Any --- Not visible in Editor // /******************** OPTIONS ********************/ // // INSPECTOR OPTIONS // hide Hide in Inspector if any of these conditions are met // alias Override a variable name to have custom string shown in Inspector // promode Only Show when 'promode' is enabled // rebuild Change of this property will cause a rebuild of the Inspector ('style' always rebuilds) // // DATA OPTIONS // type Must be provided, data type, see above // select Options for drop down selection box // if Only include property if these conditions are met // not Do not include property if these conditions are met // default Default value of item if none provided // // NUMERICAL OPTIONS (can be array values for numArrays / variable) // min Minimum allowed value // max Maximum allowed value // step Mouse wheel / arrow key step of number when being changed in Inspector. See special step values below // precision Decimal precision of value // unit Display unit of number type ('°', 'px', etc.) in Inspector // label Displays a decoration label for the number, i.e. 'X:' or 'H:'... Using this right aligns the input box! // // SPECIAL STEP VALUES (Number / Int / Angle / Slider) // 'any' Applies to Slider only. Totally smooth, slider can be any value // 'grid' Align step to current editor 'grid' setting // // VECTOR OPTIONS // ... Supports all "Numerical Options" // size Length of array // tint When true, number boxes are colorered (up to 4 boxes, axis colors) // aspect True/False, show "Aspect Lock" button? // // STRING OPTIONS // rows If value of > 1, specifies multiline text. Value describes desired default height when displayed. // // ASSET OPTIONS // class If left out, can be any asset in project. Otherwise specifies which asset type to use (geometry, texture, etc.) // const _registered = {}; class ComponentManager { /******************** TYPES */ static defaultValue(type) { switch (type) { case 'select': return null; case 'number': return 0; case 'int': return 0; case 'angle': return 0; case 'slider': return 0; case 'variable': return [ 0, 0 ]; case 'vector': return [ 0 ]; case 'option': return [ false ]; case 'boolean': return false; case 'color': return 0xffffff; case 'string': return ''; case 'key': return ''; case 'asset': return null; case 'object': return {}; case 'divider': return null; default: console.warn(`ComponentManager.defaultValue(): Unknown property type: '${type}'`); } return null; } /******************** REGISTRATION */ /** Returns class definition of a registered type */ static registered(type = '') { const ComponentClass = _registered[type]; if (!ComponentClass) console.warn(`ComponentManager.registered(): Component '${type}' not registered'`); return ComponentClass; } /** Returns an array of all types registered with Component Manager */ static registeredTypes() { return Object.keys(_registered); } /** Registers a component type with the internal Component Manager */ static register(type = '', ComponentClass) { // Check for Component Type type = type.toLowerCase(); if (_registered[type]) return console.warn(`ComponentManager.register(): Component '${type}' already registered`); if (!SysUtils.isObject(ComponentClass.config)) ComponentClass.config = {}; if (!SysUtils.isObject(ComponentClass.config.schema)) ComponentClass.config.schema = {}; // Ensure Property Default Values const schema = ComponentClass.config.schema; for (const key in schema) { const properties = Array.isArray(schema[key]) ? schema[key] : [ schema[key] ]; for (const property of properties) { if (property.type === undefined) { console.warn(`ComponentManager.register(): All schema properties require a 'type' value`); } else if (property.type === 'divider') { // NOTHING: 'divider' type is for formatting only! continue; } if (property.default === undefined) property.default = ComponentManager.defaultValue(property.type); if (property.proMode !== undefined) property.promode = property.proMode; } } // Add Constructor (sets type) class Component extends ComponentClass { constructor() { super(); // Prototype this.isComponent = true; this.type = type; // Properties this.attached = true; this.expanded = true; this.order = 0; this.tag = ''; // Owner this.entity = null; // Data this.backend = undefined; this.data = {}; } init(data = {}) { this.dispose(); if (typeof super.init === 'function') super.init(data); } dispose() { if (typeof super.dispose === 'function') super.dispose(); if (typeof this.backend === 'object' && typeof this.backend.dispose === 'function') this.backend.dispose(); this.backend = undefined; } attach() { this.attached = true; if (typeof super.attach === 'function') super.attach(); } detach() { this.attached = false; if (typeof super.detach === 'function') super.detach(); } /** Returns stored default schema (saved when Component was registered). Pass in starting data by 'key, value' pairs */ defaultData(/* key, value, key, value, etc. */) { const data = {}; // Set Schema Defaults (keep new data) for (let i = 0, l = arguments.length; i < l; i += 2) { data[arguments[i]] = arguments[i + 1]; } ComponentManager.sanitizeData(this.type, data); // Base Properties data.base = { isComponent: true, attached: this.attached, expanded: this.expanded, order: this.order, tag: this.tag, type: this.type, }; return data; } toJSON() { let data; if (this.data && this.data.style) { data = this.defaultData('style', this.data.style); } else { data = this.defaultData(); } // Copy existing 'data' properties for (const key in data) { if (this.data[key] !== undefined) { // Make sure to save 'texture' as UUID only if (this.data[key] && this.data[key].isTexture) { data[key] = this.data[key].uuid; // All other data } else { data[key] = structuredClone(this.data[key]); } } } return data; } } // Register Component _registered[type] = Component; } /******************** DATA */ /** Checks if a schema item should be included in a component data structure, optionally check a second data structure */ static includeData(item, data1, data2 = undefined) { // Check all 'if' keys, ex: bevelSize: { type: 'number', if: { style: [ 'shape' ], bevelEnabled: [ true ] } } } for (const key in item.if) { const conditions = Array.isArray(item.if[key]) ? item.if[key] : [ item.if[key] ]; let check1 = false, check2 = false; for (const condition of conditions) { check1 = check1 || (data1[key] === condition); check2 = check2 || (data2 === undefined) ? true : (data2[key] === condition); } if (!check1 || !check2) return false; } // Check all 'not' keys, EXAMPLE: height: { type: 'number', not: { style: [ 'shape' ] } }, for (const key in item.not) { const conditions = Array.isArray(item.not[key]) ? item.not[key] : [ item.not[key] ]; let check1 = false, check2 = false; for (const condition of conditions) { check1 = check1 || (data1[key] === condition); check2 = check2 || (data2 === undefined) ? false : (data2[key] === condition); } if (check1 || check2) return false; } return true; } /** Build default data of 'type', remove any properties that should not be included */ static sanitizeData(type, data) { if (!data || typeof data !== 'object') data = {}; const ComponentClass = ComponentManager.registered(type); if (!ComponentClass || !ComponentClass.config || !ComponentClass.config.schema) return; const schema = ComponentClass.config.schema; if (!SysUtils.isObject(schema)) return; // PARSE KEYS for (const schemaKey in schema) { // List of properties with different 'if' conditions const itemArray = Array.isArray(schema[schemaKey]) ? schema[schemaKey] : [ schema[schemaKey] ]; // Process each item in property array, include first item that is allowed (matches 'if', 'not', etc.) let itemToInclude = undefined; for (const item of itemArray) { // FORMATTING ONLY if (item.type === 'divider') continue; // PROCESS 'IF' / 'NOT' if (!ComponentManager.includeData(item, data)) continue; // INCLUDE itemToInclude = item; break; } // Make sure we have the property if (itemToInclude !== undefined) { if (data[schemaKey] === undefined) { if (Array.isArray(itemToInclude.default)) { data[schemaKey] = [...itemToInclude.default]; } else if (typeof itemToInclude.default === 'object') { data[schemaKey] = structuredClone(itemToInclude.default); } else { data[schemaKey] = itemToInclude.default; } } if (MathUtils.isNumber(data[schemaKey])) { const min = itemToInclude['min'] ?? -Infinity; const max = itemToInclude['max'] ?? Infinity; if (data[schemaKey] < min) data[schemaKey] = min; if (data[schemaKey] > max) data[schemaKey] = max; } // Make sure we don't have the property } else { delete data[schemaKey]; } } // next schemaKey } /** Removes properties from 'newData' if conditions from schema on 'newData' don't match 'oldData' */ static stripData(type, oldData, newData) { const ComponentClass = ComponentManager.registered(type); if (!ComponentClass || !ComponentClass.config || !ComponentClass.config.schema) return; const schema = ComponentClass.config.schema; if (!SysUtils.isObject(schema)) return; // PARSE KEYS for (const schemaKey in schema) { let matchedConditions = false; // List of properties with different 'if' conditions const itemArray = Array.isArray(schema[schemaKey]) ? schema[schemaKey] : [ schema[schemaKey] ]; // Process each item, check if that we satisfy 'if' condition for (const item of itemArray) { // FORMATTING ONLY if (item.type === 'divider') continue; // PROCESS 'IF' / 'NOT' if (!ComponentManager.includeData(item, oldData, newData)) continue; // MATCHED matchedConditions = true; break; } // Both data sets matched same condition if (matchedConditions !== true) { delete newData[schemaKey]; } } } } export { ComponentManager };