/* The MIT License (MIT) Copyright (c) 2014-2020 Nikolai Suslov and the Krestianstvo.org project contributors. (https://github.com/NikolaySuslov/livecodingspace/blob/master/LICENSE.md) Virtual World Framework Apache 2.0 license (https://github.com/NikolaySuslov/livecodingspace/blob/master/licenses/LICENSE_VWF.md) */ /// vwf/model/object.js is a backstop property store. /// /// @module vwf/model/object /// @requires vwf/model /// @requires vwf/configuration import { Fabric } from '/core/vwf/fabric.js'; class VWFObject extends Fabric{ constructor(module) { console.log("Object constructor"); super(module, "Model"); } factory(){ let _self_ = this; return this.load( this.module, { // == Module Definition ==================================================================== // -- initialize --------------------------------------------------------------------------- initialize: function() { this.objects = {}; // maps id => { property: value, ... } this.creatingNode( undefined, 0 ); // global root // TODO: to allow vwf.children( 0 ), vwf.getNode( 0 ); is this the best way, or should the kernel createNode( global-root-id /* 0 */ )? }, // == Model API ============================================================================ // -- creatingNode ------------------------------------------------------------------------- creatingNode: function( nodeID, childID, childExtendsID, childImplementsIDs, childSource, childType, childIndex, childName, callback /* ( ready ) */ ) { // The kernel calls vwf/model/object's `creatingNode` multiple times: once at the start // of `createChild` so that we can claim our spot in the parent's children array before // doing any async operations, a second time after loading the prototype and behaviors, // then a third time in the normal order as the last driver. var parent = nodeID != 0 && this.objects[nodeID]; var child = this.objects[childID]; if ( ! child ) { // First time: initialize the node. child = this.objects[childID] = { id: childID, prototype: undefined, behaviors: undefined, source: childSource, type: childType, uri: parent ? undefined : childIndex, name: childName, properties: {}, methods: {}, events: {}, parent: undefined, children: [], sequence: 0, // counter for child ID assignments prng: parent ? // pseudorandom number generator, seeded by ... new Alea( JSON.stringify( parent.prng.state ), childID ) : // ... the parent's prng and the child ID, or new Alea( vwf.configuration["random-seed"], childID ), // ... the global seed and the child ID // TODO: The 'patches' object is in the process of moving to the kernel // Those objects that are double-commented out have already moved // Those that are single-commented out have yet to move. // Change list for patchable objects. This comment shows the structure of the // object, but it is created later dynamically as needed // patches: { // // root: true, // node is the root of the component -- moved to kernel's node registry // // descendant: true, // node is a descendant still within the component -- moved to kernel's node registry // internals: true, // random, seed, or sequence has changed // // properties: true, // placeholder for a property change list -- moved to kernel's node registry // // methods: [], // array of method names for methods that changed -- moved to kernel's node registry // }, // END TODO initialized: false, }; // Connect to the parent. if ( parent ) { child.parent = parent; parent.children[childIndex] = child; } // Create the `patches` field for tracking changes if the node is patchable (if it's // the root or a descendant in a component). if ( child.uri ) { child.patches = { /* root: true */ }; } else if ( parent && ! parent.initialized && parent.patches ) { child.patches = { /* descendant: true */ }; } } else if ( ! child.prototype ) { // Second time: fill in the prototype and behaviors. child.prototype = childExtendsID && this.objects[childExtendsID]; child.behaviors = ( childImplementsIDs || [] ).map( function( childImplementsID ) { return this.objects[childImplementsID]; }, this ); } else { // Third time: ignore since nothing is new. } }, // -- initializingNode --------------------------------------------------------------------- initializingNode: function( nodeID, childID, childExtendsID, childImplementsIDs, childSource, childType, childIndex, childName ) { this.objects[childID].initialized = true; }, // -- deletingNode ------------------------------------------------------------------------- deletingNode: function( nodeID ) { var child = this.objects[nodeID]; var object = child.parent; if ( object ) { var index = object.children.indexOf( child ); if ( index >= 0 ) { object.children.splice( index, 1 ); } child.parent = undefined; } delete this.objects[nodeID]; }, // -- addingChild -------------------------------------------------------------------------- addingChild: function( nodeID, childID, childName ) { // ... doesn't validate arguments or check for moving to/from 0 // TODO: not for global anchor node 0 var object = this.objects[nodeID]; var child = this.objects[childID]; child.parent = object; object.children.push( child ); }, // -- removingChild ------------------------------------------------------------------------ removingChild: function( nodeID, childID ) { // ... doesn't validate arguments or check for moving to/from 0 var object = this.objects[nodeID]; var child = this.objects[childID]; child.parent = undefined; object.children.splice( object.children.indexOf( child ), 1 ); }, // TODO: creatingProperties, initializingProperties // -- settingProperties -------------------------------------------------------------------- settingProperties: function( nodeID, properties ) { var object = this.objects[nodeID]; if ( ! object ) return; // TODO: patch until full-graph sync is working; drivers should be able to assume that nodeIDs refer to valid objects var node_properties = object.properties; for ( var propertyName in properties ) { // TODO: since undefined values don't serialize to json, interate over node_properties (has-own only) instead and set to undefined if missing from properties? if ( ! node_properties.hasOwnProperty( propertyName ) ) { this.kernel.setProperty( nodeID, propertyName, properties[propertyName] ); } // TODO: this needs to be handled in vwf.js for setProperties() the way it's now handling setProperty() create vs. initialize vs. set node_properties[propertyName] = properties[propertyName]; } return node_properties; }, // -- gettingProperties -------------------------------------------------------------------- gettingProperties: function( nodeID, properties ) { var object = this.objects[nodeID]; if ( ! object ) return; return this.objects[nodeID].properties; }, // -- creatingProperty --------------------------------------------------------------------- creatingProperty: function( nodeID, propertyName, propertyValue ) { return this.initializingProperty( nodeID, propertyName, propertyValue ); }, // -- initializingProperty ----------------------------------------------------------------- initializingProperty: function( nodeID, propertyName, propertyValue ) { return this.settingProperty( nodeID, propertyName, propertyValue ); }, // TODO: deletingProperty // -- settingProperty ---------------------------------------------------------------------- settingProperty: function( nodeID, propertyName, propertyValue ) { var object = this.objects[nodeID]; return object.properties[propertyName] = propertyValue; }, // -- gettingProperty ---------------------------------------------------------------------- gettingProperty: function( nodeID, propertyName, propertyValue ) { var object = this.objects[nodeID]; if ( ! object ) return; return this.objects[nodeID].properties[propertyName]; }, // -- creatingMethod ----------------------------------------------------------------------- creatingMethod: function( nodeID, methodName, methodParameters, methodBody ) { return this.settingMethod( nodeID, methodName, { parameters: methodParameters, body: methodBody } ); }, // -- settingMethod ------------------------------------------------------------------------ settingMethod: function( nodeID, methodName, methodHandler ) { return this.objects[nodeID].methods[methodName] = methodHandler; }, // -- gettingMethod ------------------------------------------------------------------------ gettingMethod: function( nodeID, methodName ) { return this.objects[nodeID].methods[methodName]; }, // -- addingEventListener ------------------------------------------------------------------ addingEventListener: function( nodeID, eventName, eventListenerID, eventHandler, eventContextID, eventPhases ) { if ( ! this.objects[ nodeID ].events[ eventName ] ) { this.objects[ nodeID ].events[ eventName ] = {}; } return this.settingEventListener( nodeID, eventName, eventListenerID, _self_.utility.merge( eventHandler, { context: eventContextID, phases: eventPhases } ) ) ? true : undefined; }, // -- removingEventListener ---------------------------------------------------------------- removingEventListener: function( nodeID, eventName, eventListenerID ) { var listeners = this.objects[ nodeID ].events[ eventName ]; if ( listeners && listeners[ eventListenerID ] ) { delete listeners[ eventListenerID ]; return true; } return undefined; }, // -- settingEventListener ----------------------------------------------------------------- settingEventListener: function( nodeID, eventName, eventListenerID, eventListener ) { return this.objects[ nodeID ].events[ eventName ][ eventListenerID ] = eventListener; }, // -- gettingEventListener ----------------------------------------------------------------- gettingEventListener: function( nodeID, eventName, eventListenerID ) { return this.objects[ nodeID ].events[ eventName ][ eventListenerID ]; }, // -- flushingEventListeners --------------------------------------------------------------- flushingEventListeners: function( nodeID, eventName, eventContextID ) { var listeners = this.objects[ nodeID ].events[ eventName ]; Object.keys( listeners ).forEach( function( eventListenerID ) { if ( listeners[ eventListenerID ].context === eventContextID ) { delete listeners[ eventListenerID ]; } } ); }, // == Special Model API ==================================================================== // The kernel delegates the corresponding API calls exclusively to vwf/model/object without // calling any other models. // -- random ------------------------------------------------------------------------------- random: function( nodeID ) { var object = this.objects[nodeID]; object.initialized && object.patches && ( object.patches.internals = true ); return object.prng(); }, // -- seed --------------------------------------------------------------------------------- seed: function( nodeID, seed ) { var object = this.objects[nodeID]; object.initialized && object.patches && ( object.patches.internals = true ); object.prng = new Alea( seed ); }, // -- intrinsics --------------------------------------------------------------------------- intrinsics: function( nodeID, result ) { var object = this.objects[nodeID]; result = result || {}; // TODO: extends and implements IDs result.source = object.source; result.type = object.type; return result; }, // -- uri ---------------------------------------------------------------------------------- uri: function( nodeID ) { var node = this.objects[ nodeID ]; if ( node ) { return node.uri; } else { this.logger.warnx( "Could not find uri of nonexistent node: '" + nodeID + "'" ); } }, // -- name --------------------------------------------------------------------------------- name: function( nodeID ) { return this.objects[nodeID].name || ""; }, // -- prototype ---------------------------------------------------------------------------- prototype: function( nodeID ) { // TODO: not for global anchor node 0 var object = this.objects[nodeID]; return object.prototype && object.prototype.id; }, // -- behaviors ---------------------------------------------------------------------------- behaviors: function( nodeID ) { // TODO: not for global anchor node 0 var object = this.objects[nodeID]; if ( ! object ) return; var behaviors = this.objects[nodeID].behaviors; if ( behaviors ) { return behaviors.map( function( behavior ) { return behavior.id; } ); } else { this.logger.warnx( "Node '" + nodeID + "' does not have a valid behaviors array" ); } }, // -- parent ------------------------------------------------------------------------------- parent: function( nodeID, initializedOnly ) { var object = this.objects[ nodeID ]; if ( object ) { return ( ! initializedOnly || object.initialized ) ? ( object.parent && object.parent.id || 0 ) : undefined; } else { this.logger.error( "Cannot find node: '" + nodeID + "'" ); } }, // -- children ----------------------------------------------------------------------------- children: function( nodeID, initializedOnly ) { if ( nodeID === undefined ) { this.logger.errorx( "children", "cannot retrieve children of nonexistent node"); return; } var node = this.objects[ nodeID ]; if ( node ) { return node.children.map( function( child ) { return ( ! initializedOnly || child.initialized ) ? child.id : undefined; } ); } else { this.logger.error( "Cannot find node: " + nodeID ); } }, // == Special utilities ==================================================================== // The kernel depends on these utility functions but does not expose them directly in the // public API. // -- properties --------------------------------------------------------------------------- properties: function( nodeID ) { return this.objects[nodeID].properties; }, // -- internals ---------------------------------------------------------------------------- internals: function( nodeID, internals ) { var object = this.objects[nodeID]; if ( !object ) { this.logger.errorx( "internals: object does not exist with id = '" + nodeID + "'" ); return; } if ( internals ) { // set if ( internals.sequence !== undefined ) { object.sequence = internals.sequence; object.initialized && object.patches && ( object.patches.internals = true ); } if ( internals.random !== undefined ) { _self_.merge( object.prng.state, internals.random ); object.initialized && object.patches && ( object.patches.internals = true ); } } else { // get internals = {}; internals.sequence = object.sequence; internals.random = object.prng.state; // TODO: tag as Alea data } return internals; }, // -- sequence ----------------------------------------------------------------------------- sequence: function( nodeID ) { var object = this.objects[nodeID]; object.initialized && object.patches && ( object.patches.internals = true ); return object && ++object.sequence; }, // -- patches ------------------------------------------------------------------------------ patches: function( nodeID ) { return this.objects[nodeID].patches; }, // -- exists ------------------------------------------------------------------------------- exists: function( nodeID ) { return !! this.objects[nodeID]; }, // -- initialized -------------------------------------------------------------------------- initialized: function( nodeID ) { return this.objects[nodeID].initialized; }, } ); } /// Merge fields from the `source` objects into `target`. merge( target /* [, source1 [, source2 ... ] ] */ ) { for ( var index = 1; index < arguments.length; index++ ) { var source = arguments[index]; Object.keys( source ).forEach( function( key ) { target[key] = source[key]; } ); } return target; } } export { VWFObject as default }