UNPKG

@woosh/meep-engine

Version:

Pure JavaScript game engine. Fully featured and production ready.

364 lines (267 loc) • 8.92 kB
import LabelView from "../../../src/view/common/LabelView.js"; import EmptyView from "../../../src/view/elements/EmptyView.js"; import { createFieldEditor } from "./createFieldEditor.js"; import { FieldDescriptor } from "./FieldDescriptor.js"; import { FieldValueAdapter } from "./FieldValueAdapter.js"; import { findNearestRegisteredType } from "./findNearestRegisteredType.js"; /** * @template T * @param {T|undefined} value */ function objectToClass(value) { if (value === null || value === undefined) { return; } const type = typeof value; switch (type) { case "object": const proto = Object.getPrototypeOf(value); if (proto === null) { return; } return proto.constructor; case "boolean": return Boolean; case "function": return Function; case "string": return String; case "number": return Number; default: throw new Error(`Unsupported type '${type}'`); } } /** * * @param {Object} object * @param {string} field_name * @returns {boolean} */ function isPublicField(object, field_name) { if (field_name.startsWith('_')) { // private return false; } return true; } /** * * @param {Object} object * @param {string} field_name * @returns {boolean} */ function isReservedField(object, field_name) { if (field_name.startsWith('@')) { return true; } return false; } /** * * @type {string[]} */ const reserved_prototype_names = [ "constructor" ]; const reserved_prototypes = [ Object, Object.prototype ]; /** * * @param {Object} object * @param {FieldDescriptor[]} result * @param {*} object_proto */ function extract_object_fields_reflection(object, result, object_proto) { const keys = Object.keys(object); for (let i = 0; i < keys.length; i++) { const key = keys[i]; if (!isPublicField(object, key)) { continue; } if (isReservedField(object, key)) { continue; } const field_type = objectToClass(object[key]); if (field_type === undefined) { continue; } const descriptor = new FieldDescriptor(); descriptor.name = key; descriptor.type = field_type; descriptor.adapter = new FieldValueAdapter(); result.push(descriptor); } // TODO exclude value fields that belong to the prototype, such as .isTransform if (!reserved_prototypes.includes(object_proto)) { const descriptors = Object.getOwnPropertyDescriptors(object_proto); for (let d_name in descriptors) { if (reserved_prototype_names.includes(d_name)) { // reserved name, skip continue; } const d = descriptors[d_name]; const adapter = new FieldValueAdapter(); if (d.get === undefined && d.value === undefined) { adapter.readable = false; } if (d.get !== undefined && d.set === undefined) { adapter.writable = false; } if (!adapter.readable) { continue; } let field_value; try { field_value = adapter.read(object, d_name); } catch (e) { console.warn(e); continue; } const field_type = objectToClass(field_value); if (field_type === undefined) { continue; } const descriptor = new FieldDescriptor(); descriptor.name = d_name; descriptor.type = field_type; descriptor.adapter = adapter; result.push(descriptor); } } } /** * * @param {Object} object * @param {Map<*, TypeEditor>} registry * @returns {FieldDescriptor[]} */ function extractObjectFields(object, registry) { const result = []; const object_proto = Object.getPrototypeOf(object); const Klass = object_proto.constructor; const editor = registry.get(Klass); if (!(editor?.schema?.additionalProperties === false)) { extract_object_fields_reflection(object, result, object_proto); } if (editor !== undefined) { const schema = editor.schema; if (schema !== undefined) { // has schema, apply const prop_keys = Object.keys(schema.properties); for (let i = 0; i < prop_keys.length; i++) { const key = prop_keys[i]; const property = schema.properties[key]; const field_index = result.findIndex(a => a.name === key); let field; if (field_index === -1) { // not in schema yet, build field = new FieldDescriptor(); field.name = key; field.adapter = new FieldValueAdapter(); result.push(field); } else { field = result[field_index]; } let actual_type = field.type; if (actual_type === undefined || actual_type === null) { let actual_value; try { actual_value = field.adapter.read(object, key); } catch (e) { // silent failure } if (actual_value !== undefined && actual_value !== null) { actual_type = objectToClass(actual_value); } } if (property.type) { // validate type if (actual_type !== undefined && actual_type !== null) { if (actual_type !== property.type) { console.error(`Schema violation at field '${key}', schema type is different from actual type`, schema, actual_type); } } field.type = property.type; } else if (actual_type !== undefined && field.type !== actual_type) { field.type = actual_type; } field.schema = property; if (property.transient === true) { // no point in editing transient result.splice(field_index, 1); } } } } return result; } const build_path = []; let build_path_offset = 0; /** * @param {Object} object * @param {Map<*, TypeEditor>} registry * @param {FieldDescriptor} [field_descriptor] */ export function buildObjectEditorFromRegistry(object, registry, field_descriptor) { const editor = findNearestRegisteredType(registry, object); let fd; if (field_descriptor === undefined) { fd = new FieldDescriptor(); fd.adapter = new FieldValueAdapter(); fd.adapter.read = () => object; fd.adapter.writable = false; if (editor.schema) { fd.schema = editor.schema; } } else { fd = field_descriptor; } return editor.build(null, fd, registry); } const MAX_DEPTH = 7; /** * @param {Object} object * @param {Map<*, TypeEditor>} registry */ export function createObjectEditor(object, registry) { if (build_path_offset > MAX_DEPTH) { // too deep return undefined; } const seen_index = build_path.indexOf(object); if (seen_index >= 0 && seen_index < build_path_offset) { // recursion return undefined; } build_path[build_path_offset++] = object; const vResult = new EmptyView({ classList: ['auto-object-editor'], css: {} }); const fields = extractObjectFields(object, registry); const field_views = []; for (let i = 0; i < fields.length; i++) { const field = fields[i]; let vField; try { vField = createFieldEditor(object, field, registry); } catch (e) { vField = new LabelView('ERROR'); console.warn(e); } if (vField === null) { continue; } field_views.push(vField); } build_path_offset--; if (field_views.length === 0) { // skip containers with no data return null; } vResult.addChildren(field_views); return vResult; }