// VVVV.js -- Visual Web Client Programming // (c) 2011 Matthias Zauner // VVVV.js is freely distributable under the MIT license. // Additional authors of sub components are mentioned at the specific code locations. VVVV.MousePositions = {'_all': {'x': 0.0, 'y': 0.0, 'wheel': 0.0, 'lb': 0.0, 'mb': 0.0, 'rb': 0.0}} /** * @const * @property {Integer} Input 0 * @property {Integer} Output 1 * @property {Integer} Configuration 2 */ var PinDirection = { Input : 0,Output : 1,Configuration : 2 }; /** * The default pin type, used if no further specified * @memberof VVVV.PinTypes * @mixin * @property {String} typeName Generic * @property {Boolean} reset_on_disconnect true * @property {String} defaultValue '0' */ VVVV.PinTypes.Generic = { typeName: "Generic", reset_on_disconnect: true, defaultValue: function() { return '0' } } /** * Value Pin Type * @memberof VVVV.PinTypes * @mixin * @property {String} typeName Value * @property {Boolean} reset_on_disconnect false * @property {String} defaultValue 0 * @property {Boolean} primitive true */ VVVV.PinTypes.Value = { typeName: "Value", reset_on_disconnect: false, defaultValue: function() { return 0 }, primitive: true } /** * String Pin Type * @memberof VVVV.PinTypes * @mixin * @property {String} typeName String * @property {Boolean} reset_on_disconnect false * @property {String} defaultValue '' * @property {Boolean} primitive true */ VVVV.PinTypes.String = { typeName: "String", reset_on_disconnect: false, defaultValue: function() { return '' }, primitive: true } /** * Enum Pin Type * @memberof VVVV.PinTypes * @mixin * @property {String} typeName Enum * @property {Boolean} reset_on_disconnect false * @property {String} defaultValue '' * @property {Boolean} primitive true */ VVVV.PinTypes.Enum = { typeName: "Enum", reset_on_disconnect: false, defaultValue: function() { return '' }, primitive: true } /** * Contains various unsorted helper methods * @namespace */ VVVV.Helpers = { /** * Translates a verbous operator (from the nodename) to a symbol * @param {String} l The verbous operator * @return {String} the operator symbol */ translateOperators: function(l) { l = l.replace("Add", "+"); l = l.replace("Subtract", "-"); l = l.replace("Multiply", "*"); l = l.replace("Divide", "/"); l = l.replace("EQ", "="); l = l.replace("GT", ">"); l = l.replace("GTE", ">="); l = l.replace("LT", "<"); l = l.replace("LTE", "<="); return l; }, /** * Translates a relative path to an absolute one (usable by the browser) and replaces variables %VVVV% and %PAGE% * @param {String} path the relative path * @param {VVVV.Core.Patch} patch the patch which the above path is relative to * @return {String} the absolute path, usable by the browser */ prepareFilePath: function(path, patch) { path = path.replace(/\\/g, '/'); if (path.match(/^%VVVV%/)) // VVVV.js system path return path.replace('%VVVV%', VVVV.Root); if (path.match(/^%PAGE%/)) // hosting HTML page path return path.replace('%PAGE%', location.pathname); if (path.match(/^(\/|.+:\/\/)/)) // path starting with / or an URL protocol (http://, ftp://, ..) return path; if (patch) return patch.getAbsolutePath()+path; else return path; }, /** * Helper function that helps creating a dynamic number of input pins * see e.g. Add (Value) for usage * @param {VVVV.Core.Node} node the node the pins should be added to/removed from * @param {Array} pins the array which holds the VVVV.Core.Pin objects; will be modified by the function * @param {Integer} count the desired number of pins * @param {Function} create_callback the function which actually creates the pin */ dynamicPins: function(node, pins, count, create_callback) { var currentCount = pins.length; for (var i=currentCount; i<count; i++) { pins[i] = create_callback.call(node, i); } for (var i=count; i<pins.length; i++) { node.removeInputPin(pins[i].pinname); } pins.length = count; node.parentPatch.afterUpdate(); } } /** @namespace */ VVVV.Core = { /** * @class * @constructor * @param {String} pinname Pin Name * @param {String} direction see {@link PinDirection} * @param {Array} init_values the array of initial values * @param {VVVV.Core.Node} node the node this pin is attached to * @param {Object} [type] the PinType, default is {@link VVVV.PinTypes.Generic), see {@link VVVV.PinTypes} */ Pin: function(pinname,direction, init_values, node, type) { /** see {@link PinDirection} */ this.direction = direction; /** @member */ this.pinname = pinname; /** @member */ this.links = []; /** @member */ this.values = []; /** @member */ this.node = node; /** @member */ this.changed = true; this.active = false; this.reset_on_disconnect = false; /** if the pin is a subpatch's input pin, the slavePin is the corresponding IOBox input pin INSIDE the subpatch */ this.slavePin = undefined; /** if the pin is a subpatch's output pin, the masterPin is the corresponding IOBox output pin INSIDE the subpatch */ this.masterPin = undefined; /** contains a row of named callback functions, each fired if the pin's connection has changed */ this.connectionChangedHandlers = {}; /** contains the options used if the pin is of type {@link VVVV.PinTypes.Enum} */ this.enumOptions = []; /** * retreives pin's slices * @param {Integer} i the slice/bin number * @param {Integer} [binSize] the bin size, default is 1 * @return if binSize is 1, the value of the slice is returned; if binSize is > 1, an array with the slice values is returned */ this.getValue = function(i, binSize) { if (!binSize || binSize==1) return this.values[i%this.values.length]; var ret = []; for (var j=0; j<binSize; j++) { ret.push(this.values[(i*binSize+j)%this.values.length]); } return ret; } /** * set a pin's slice value; if an output pin, it also sets the values of connected input pins. If the pin is a subpatch input pin, it also sets the slavePin inside the subpatch * @param {Integer} i the slice number * @param v the value to set * @param {Boolean} [stopPropagation] default is false; if true, the function does not update slavePins to avoid infinite loops; this parameter should not be used in node implementations */ this.setValue = function(i, v, stopPropagation) { stopPropagation = stopPropagation || false this.values[i] = v; this.changed = true; this.node.dirty = true; var that = this; if (this.direction==PinDirection.Output) { var linkCount = this.links.length; for (var j=0; j<linkCount; j++) { this.links[j].toPin.setValue(i, v); } } if (this.slavePin && !stopPropagation) { this.slavePin.setValue(i, v); } if (this.direction==PinDirection.Input && this.masterPin && !this.isConnected()) this.masterPin.setValue(i, v, true); if (this.node.isIOBox && this.pinname=='Descriptive Name') { if (this.node.parentPatch.domInterface) this.node.parentPatch.domInterface.connect(this.node); else if (this.node.parentPatch.parentPatch) this.node.registerInterfacePin(); } } /** * used to mark a pin as changed without actually using {@link VVVV.Core.Pin#setValue} */ this.markPinAsChanged = function() { this.changed = true; this.node.dirty = true; var that = this; if (this.direction==PinDirection.Output) { var linkCount = this.links.length; for (var i=0; i<linkCount; i++) { this.links[i].toPin.markPinAsChanged(); } } if (this.slavePin) { this.slavePin.markPinAsChanged(); } } /** * used to find out if a pin has changed since last evaluation * @return {Boolean} true if changed, false if not changed */ this.pinIsChanged = function() { var ret = this.changed; this.changed = false; return ret; } /** * used do find out if a pin is connected * @return true, if there are incoming or outgoing links to or from this pin (and its masterPin, if preset) */ this.isConnected = function() { return (this.links.length > 0 || (this.masterPin && this.masterPin.isConnected())); } /** * @return {Integer} the number of slices */ this.getSliceCount = function() { return this.values.length; } /** * sets the number of slices; also sets the slice number of connected downstream pins and the slavePin if present; absolutely necessary if the slice number decreases * @param {Integer} len the slice count */ this.setSliceCount = function(len) { if (this.values.length==len) return; this.values.length = len; this.changed = true; this.node.dirty = true; if (this.direction==PinDirection.Output) { var linkCount = this.links.length; for (var i=0; i<linkCount; i++) { this.links[i].toPin.setSliceCount(len); } } if (this.slavePin) { this.slavePin.setSliceCount(len); } } /** * used to change the pin's type during runtime. Also sets the value to the new pin type's default value * @param {Object} newType the new type, see {@link VVVV.PinTypes} */ this.setType = function(newType) { if (newType.typeName == this.typeName) return; var that = this; delete this.connectionChangedHandlers['nodepin']; delete this.connectionChangedHandlers['webglresource']; _(newType.connectionChangedHandlers).each(function(handler, key) { that.connectionChangedHandlers[key] = newType.connectionChangedHandlers[key]; }); this.typeName = newType.typeName; this.defaultValue = newType.defaultValue; if (this.direction == PinDirection.Input && this.defaultValue) { this.setValue(0, this.defaultValue()); this.setSliceCount(1); } if (newType.reset_on_disconnect!=undefined) this.reset_on_disconnect = newType.reset_on_disconnect; } if (type==undefined) { type = VVVV.PinTypes.Generic; this.unvalidated = true; } this.setType(type); if (init_values && init_values.length>0) // override PinType's default value with values from constructor, if it isn't [] this.values = init_values.slice(0); // use slice(0) to create a copy of the array this.reset = function() { if (this.defaultValue) { this.setValue(0, this.defaultValue()); this.setSliceCount(1); } else this.values = init_values.slice(0); this.markPinAsChanged(); } /** * called when the pin gets connected or disconnected; subsequently calls the callbacks registered in {@link VVVV.Core.Pin#connectionChangedHandlers} */ this.connectionChanged = function() { var that = this; _(this.connectionChangedHandlers).each(function(handler) { that.f = handler; that.f(); }); } }, /** * @class * @constructor * @param {Integer} id the Node ID * @param {String} nodename the Node Name * @param {VVVV.Core.Patch} [parentPatch] the {@link VVVV.Core.Patch} the node is nested */ Node: function(id, nodename, parentPatch) { /** the nodename; might be e.g. a name in format NodeName (Category), SomeShader.fx or a path/to/a/subpatch.v4p */ this.nodename = nodename; /** the node ID */ this.id = id; /** X position in pixels inside the parent patch */ this.x = 0; /** Y position in pixels inside the parent patch */ this.y = 0; /** Node width (in a weird unit); use {@link VVVV.Core.Node.getWidth()} for pixel value */ this.width = 0; /** Node width (in a weird unit); use {@link VVVV.Core.Node.getHeight()} for pixel value */ this.height = 0; /** flag indicating whether a node is an IOBox */ this.isIOBox = false; /** flag indicating whether a node is a shader node */ this.isShader = false; /** the number of subsequent resources (subpatches, shaders, 3rd party libs, etc.) that are currently being loaded. Is 0 if nothing is pending */ this.resourcesPending = 0; /** flag indicating if this node should automatically output nil on all output pins, if a nil value is on any input pin */ this.auto_nil = true; this.setupObject = function() { // had to put this into a method to allow Patch to "derive" from Node. Really have to understand this javascript prototype thing some day ... this.inputPins = {}; this.outputPins = {}; this.invisiblePins = {} ; this.defaultPinValues = {}; }; this.setupObject(); /** a flag indicating if this node should evaluate each frame, no matter if it's marked dirty or not */ this.auto_evaluate = false; this.delays_output = false; /** a flag indicating if any of this node's input pins has changed */ this.dirty = true; /** the patch containing this node */ this.parentPatch = parentPatch; if (parentPatch) this.parentPatch.nodeMap[id] = this; /** * saves a pin value coming from the patch XML for later use * @param {String} pinname the pin's name * @param {Array} value the array of values (slices) */ this.addDefault = function(pinname, value) { this.defaultPinValues[pinname] = value; } /** * Creates a new input pin and adds it to the node. If pin values from the XML have been registered through {@link VVVV.Core.Node.addDefault}, * these values are assigned * @param {String} pinname the new pin's name * @param {Array} value the default spread * @param {Object} type see {@link VVVV.PinTypes} * @return {VVVV.Core.Pin} the new {@link VVVV.Core.Pin} */ this.addInputPin = function(pinname, value, type) { var pin = new VVVV.Core.Pin(pinname,PinDirection.Input, value, this, type); this.inputPins[pinname] = pin; if (this.parentPatch) this.parentPatch.pinMap[this.id+'_in_'+pinname] = pin; this.applyPinValuesFromXML(pinname); return pin; } /** * Creates a new output pin and adds it to the node. * @param {String} pinname the new pin's name * @param {Array} value the default spread * @param {Object} type see {@link VVVV.PinTypes} * @return {VVVV.Core.Pin} the new {@link VVVV.Core.Pin} */ this.addOutputPin = function(pinname, value, type) { var pin = new VVVV.Core.Pin(pinname,PinDirection.Output, value, this, type); this.outputPins[pinname] = pin; if (this.parentPatch) this.parentPatch.pinMap[this.id+'_out_'+pinname] = pin; return pin; } /** * deletes an input pin and all incoming links * @param pinname the name of the pin to delete */ this.removeInputPin = function(pinname) { if (!this.inputPins[pinname]) return; var l = this.inputPins[pinname].links[0]; if (l) { l.fromPin.connectionChanged(); l.destroy(); } delete this.inputPins[pinname]; this.dirty = true; } /** * deletes an output pin and all outgoing links * @param pinname the name of the pin to delete */ this.removeOutputPin = function(pinname) { if (!this.outputPins[pinname]) return; var n = this.outputPins[pinname].links.length; for (var i=0; i<n; i++) { var l = this.outputPins[pinname].links[i]; l.toPin.connectionChanged(); l.destroy(); } delete this.outputPins[pinname]; this.dirty = true; } /** * Creates a new invisible/config pin and adds it to the node. If pin values from the XML have been registered through {@link VVVV.Core.Node.addDefault}, * these values are assigned * @param {String} pinname the new pin's name * @param {Array} value the default spread * @param {Object} type see {@link VVVV.PinTypes} * @return {VVVV.Core.Pin} the new {@link VVVV.Core.Pin} */ this.addInvisiblePin = function(pinname, value, type) { var pin = new VVVV.Core.Pin(pinname,PinDirection.Configuration, value, this, type); this.invisiblePins[pinname] = pin; this.parentPatch.pinMap[this.id+'_inv_'+pinname] = pin; if (this.defaultPinValues[pinname] != undefined) { pin.values = this.defaultPinValues[pinname]; } return pin; } /** * Helper to get the type of IOBox (e.g. Value Advanced, String, Color) * @return {String} the type of IOBox */ this.IOBoxType = function() { var match = /^IOBox \((.*)\)/.exec(this.nodename); if (match && match.length>1) return match[1]; return ""; } /** * Returns the input pin of the IOBox which is represented by the IOBox label * @return {VVVV.Core.Pin} the pin represented by the IOBox label, see {@link VVVV.Core.Pin} */ this.IOBoxInputPin = function() { switch (this.IOBoxType()) { case "Value Advanced": return this.inputPins["Y Input Value"]; case "String": return this.inputPins["Input String"]; case "Color": return this.inputPins["Color Input"]; case "Node": return this.inputPins["Input Node"]; } return undefined; } /** * Returns the output pin of the IOBox which is represented by the IOBox label * @return {VVVV.Core.Pin} the pin represented by the IOBox label, see {@link VVVV.Core.Pin} */ this.IOBoxOutputPin = function() { switch (this.IOBoxType()) { case "Value Advanced": return this.outputPins["Y Output Value"]; case "String": return this.outputPins["Output String"]; case "Color": return this.outputPins["Color Output"]; case "Node": return this.outputPins["Output Node"]; } return undefined; } /** * Returns the number of visible rows of an IOBox. This is basically a convenience method for getting the value of the "Rows" pin * @return {Integer} the number of visible rows */ this.IOBoxRows = function() { if (this.invisiblePins["Rows"]) return this.invisiblePins["Rows"].getValue(0); else return 1; } /** * Tells, if a node is a comment node. Reverse engineering revealed that this is the case, if a String IOBox has no output * pins. Maybe better ask someone who actually knows. * @return {Boolean} true, if the node is a comment, false otherwise. */ this.isComment = function() { return this.isIOBox && _(this.outputPins).size()==0 } /** * Returns the text shown inside a node box in the editor. In case of an IOBox this is the result of {@link VVVV.Core.Node.IOBoxInputPin}; * in case of a subpatch this is "|| SubPatchName" (the .v4p extension stripped); in case of a normal node, this is the node name. * @return {String} the node's representative label */ this.label = function() { if (this.isIOBox) { if (this.IOBoxInputPin().getValue(0)) return this.IOBoxInputPin().getValue(0).toString(); return ''; } if (this.isSubpatch) { return "||"+this.nodename.match(/([^\/]+)\.v4p$/)[1]; } var label = this.nodename.replace(/\s\(.+\)/, ''); var label = VVVV.Helpers.translateOperators(label); return label; } /** * Returns the node with in pixels, used for displaying the patch * @return {Integer} the node width in pixels */ this.getWidth = function() { var ret; if (this.width==100 || this.width==0) { if (this.isIOBox) ret = 60; else ret = Math.max(18, (this.label().length+2)*6); } else ret = this.width/15; ret = Math.max(ret, (_(this.inputPins).size()-1)*12+4); return ret; } /** * Returns the node height in pixels, used for displaying the patch * @return {Integer} the node height in pixels */ this.getHeight = function() { if (this.isIOBox && this.height==100) return 18 * this.IOBoxRows(); if (this.height==100 || this.isSubpatch) return 18; else return Math.max(18, this.height/15); } /** * Returns all nodes which are connected to a node's input pins * @return {Array} an Array of {@link VVVV.Core.Node} objects */ this.getUpstreamNodes = function() { var ret = []; _(this.inputPins).each(function(p) { if (p.links.length>0) ret.push(p.links[0].fromPin.node); }); return ret; } /** * Returns all nodes which are connected to a node's output pins * @return {Array} an Array of {@link VVVV.Core.Node} objects */ this.getDownstreamNodes = function() { var ret = []; _(this.outputPins).each(function(p) { for (var j=0; j<p.links.length; j++) { ret.push(p.links[j].toPin.node); } }); return ret; } /** * Finds all nodes with a certain name, the node's data eventually flows into. * @param {String} name the name of the node to search for * @result {Array} an Array of {@link VVVV.Core.Node} objects matching the search */ this.findDownstreamNodes = function(name) { var ret = []; _(this.outputPins).each(function(p) { for (var j=0; j<p.links.length; j++) { if (p.links[j].toPin.node.nodename==name) ret.push(p.links[j].toPin.node); else { if (p.links[j].toPin.slavePin) { // enter subpatch ret = ret.concat(p.links[j].toPin.slavePin.node.findDownstreamNodes(name)); } else if (p.links[j].toPin.node.isIOBox && p.links[j].toPin.node.IOBoxOutputPin().slavePin) { // leave subpatch ret = ret.concat(p.links[j].toPin.node.IOBoxOutputPin().slavePin.node.findDownstreamNodes(name)); } else ret = ret.concat(p.links[j].toPin.node.findDownstreamNodes(name)); } } }); return ret; } /** * Tells, if a node has any nil inputs * @return true, if any of the input pins are true, false otherwise */ this.hasNilInputs = function() { var result = false _(this.inputPins).each(function(p) { if (p.getSliceCount()==0) result = true; }); return result; } /** * Returns the maximum number of slices of a node's input pins * @return the maximum number of slices */ this.getMaxInputSliceCount = function() { var ret = 0; _(this.inputPins).each(function(p) { if (p.getSliceCount()>ret) ret = p.values.length; }); return ret; } /** * Applies values from the patch XML to an input pin, if present * @param {String} pinname the name of the pin */ this.applyPinValuesFromXML = function(pinname) { if (!this.inputPins[pinname]) return; var pin = this.inputPins[pinname]; var values = this.defaultPinValues[pinname]; if (values != undefined) { // this checks for the case when complex input pins have a value of "||" when not connected. // this should not override the default value set by the node with "" if (!pin.reset_on_disconnect || values.length>1 || values[0]!="") { for (var i=0; i<values.length; i++) { if (pin.values[i]!=values[i]) pin.setValue(i, values[i]); } pin.setSliceCount(values.length); } } } /** * Called, if an IOBox's Descriptive Name inside a subpatch changes, this method creates and updates the subpatch's in and output * pins. Subsequently triggers connection changed events for the IOBox's input and output pins. */ this.registerInterfacePin = function() { var that = this; if (this.isIOBox) { if (this.parentPatch.parentPatch && this.invisiblePins["Descriptive Name"].getValue(0)!="") { var pinname = this.invisiblePins["Descriptive Name"].getValue(0); this.IOBoxInputPin().connectionChangedHandlers['subpatchpins'] = function() { if (this.links.length>0 && this.masterPin) { if (VVVV_ENV=='development') console.log('deleting '+pinname+' input pin because node has input connection...'); for (var i=0; i<this.masterPin.links.length; i++) { this.masterPin.links[i].destroy(); } that.parentPatch.removeInputPin(pinname); this.masterPin = undefined; } if (that.IOBoxOutputPin().links.length==0) { if (!that.IOBoxOutputPin().slavePin) { if (VVVV_ENV=='development') console.log('interfacing output pin detected: '+pinname); var pin = that.parentPatch.outputPins[pinname]; if (pin==undefined) var pin = that.parentPatch.addOutputPin(pinname, that.IOBoxOutputPin().values); pin.setType(VVVV.PinTypes[that.IOBoxOutputPin().typeName]); that.IOBoxOutputPin().slavePin = pin; pin.masterPin = that.IOBoxOutputPin(); } else if (that.IOBoxOutputPin().slavePin.pinname!=pinname) { // rename subpatch pin if (VVVV_ENV=='development') console.log('renaming '+that.IOBoxOutputPin().slavePin.pinname+" to "+pinname); that.parentPatch.outputPins[pinname] = that.parentPatch.outputPins[that.IOBoxOutputPin().slavePin.pinname]; that.parentPatch.removeOutputPin(that.IOBoxOutputPin().slavePin.pinname); that.IOBoxOutputPin().slavePin.pinname = pinname; } } this.node.parentPatch.parentPatch.afterUpdate(); } this.IOBoxInputPin().connectionChanged(); this.IOBoxOutputPin().connectionChangedHandlers['subpatchpins'] = function() { if (this.links.length>0 && this.slavePin) { if (VVVV_ENV=='development') console.log('deleting '+pinname+' output pin because node '+that.id+' has output connection...'); for (var i=0; i<this.slavePin.links.length; i++) { this.slavePin.links[i].destroy(); } that.parentPatch.removeOutputPin(pinname); this.slavePin = undefined; } if (that.IOBoxInputPin().links.length==0) { if (!that.IOBoxInputPin().masterPin) { if (VVVV_ENV=='development') console.log('interfacing input pin detected: '+pinname); var pin = that.parentPatch.inputPins[pinname]; if (pin==undefined) { if (VVVV_ENV=='development') console.log('creating new input pin at parent patch, using IOBox values'); var pin = that.parentPatch.addInputPin(pinname, that.IOBoxInputPin().values); } else { slicecount = pin.getSliceCount(); for (var i=0; i<slicecount; i++) { that.IOBoxInputPin().setValue(i, pin.getValue(i)); } if (slicecount>0) that.IOBoxInputPin().setSliceCount(pin.getSliceCount()); } var savedValues = pin.values.slice(); pin.setType(VVVV.PinTypes[that.IOBoxInputPin().typeName]); if ((pin.unvalidated && VVVV.PinTypes[pin.typeName].primitive) || pin.isConnected()) { pin.values = savedValues; pin.unvalidated = false; } pin.slavePin = that.IOBoxInputPin(); that.IOBoxInputPin().masterPin = pin; } else if (that.IOBoxInputPin().masterPin.pinname!=pinname) { // rename subpatch pin console.log('renaming '+that.IOBoxInputPin().masterPin.pinname+" to "+pinname); that.parentPatch.inputPins[pinname] = that.parentPatch.inputPins[that.IOBoxInputPin().masterPin.pinname]; that.parentPatch.removeInputPin(that.IOBoxInputPin().masterPin.pinname); that.IOBoxInputPin().masterPin.pinname = pinname; } } this.node.parentPatch.parentPatch.afterUpdate(); } this.IOBoxOutputPin().connectionChanged(); } } } /** * Method called immediatly after node creation for setting up common node settings */ this.setup = function() { //Add descriptive name for all nodes this.addInvisiblePin("Descriptive Name",[""], VVVV.PinTypes.String); } /** * Method called AFTER a node's pins have been created and populated with values from patch XML, and BEFORE node links are created. * This method should be overwritten by any Node implementation and is useful for e.g. creating dynamic number of input pins and * other initialising code which should run before first call of {@link VVVV.Core.Node.evaluate}. * @abstract */ this.initialize = function() { } /** * Method called each frame, if a node is marked dirty or {@link VVVV.Core.Node.auto_evaluate} is true. This method should * be overwritten by any Node implementation and usually holds the node's main logic. * @abstract */ this.evaluate = function() { var that = this; _(this.outputPins).each(function(p) { p.setValue(0, "not calculated"); }); } /** * Method called when a node is being deleted. Should be overwritten by any Node implementation to free resources and gracefully * shut itself down * @abstract */ this.destroy = function() { if (this.isIOBox) { if (this.IOBoxInputPin().masterPin) { this.parentPatch.removeInputPin(this.IOBoxInputPin().masterPin.pinname); this.parentPatch.parentPatch.afterUpdate(); } if (this.IOBoxOutputPin().slavePin) { this.parentPatch.removeOutputPin(this.IOBoxOutputPin().slavePin.pinname); this.parentPatch.parentPatch.afterUpdate(); } } } /** * Creates the XML code representing the node and its pins. Called by {@link VVVV.Core.Patch.toXML} on serializing a patch and * directly by the editor when nodes are being copied to clipboard * @return {String} the node's XML code */ this.serialize = function() { var $node = $("<NODE>"); $node.attr("id", this.id); $node.attr("nodename", this.nodename); $node.attr("systemname", this.nodename); if (this.shaderFile) { $node.attr("filename", this.shaderFile.replace(".vvvvjs.fx", ".fx").replace("%VVVV%/effects", "%VVVV%/lib/nodes/effects")); } if (this.isSubpatch) { $node.attr("filename", this.nodename); $node.attr("systemname", this.nodename.match("(.*)\.v4p$")[1]) } if (this.isIOBox) $node.attr("componentmode", "InABox"); else $node.attr("componentmode", "Hidden"); var $bounds = $("<BOUNDS>"); if (this.isIOBox) $bounds.attr("type", "Box"); else $bounds.attr("type", "Node"); $bounds.attr("left", parseInt(this.x * 15)); $bounds.attr("top", parseInt(this.y * 15)); $bounds.attr("width", parseInt(this.width)); $bounds.attr("height", parseInt(this.height)); $node.append($bounds); var that = this; _(this.inputPins).each(function(p) { var $pin = $("<PIN>"); $pin.attr("pinname", p.pinname); $pin.attr("visible", "1"); if ((!p.isConnected() || p.masterPin) && VVVV.PinTypes[p.typeName].primitive && that.defaultPinValues[p.pinname]) { $pin.attr("values", _(that.defaultPinValues[p.pinname]).map(function(v) { return "|"+v.toString().replace(/\|/g, "||")+"|"; }).join(",")); } $node.append($pin); }) _(this.invisiblePins).each(function(p) { var $pin = $("<PIN>"); $pin.attr("pinname", p.pinname); $pin.attr("visible", "0"); if (VVVV.PinTypes[p.typeName].primitive) { $pin.attr("values", _(p.values).map(function(v) { return "|"+v.toString().replace(/\|/g, "||")+"|"; }).join(",")); } $node.append($pin); }) return $node; } }, /** * @class * @constructor * @param {VVVV.Core.Pin} fromPin the output pin which is the source of the connection * @param {VVVV.Core.Pin} toPin the input pin which is the destination of the connection */ Link: function(fromPin, toPin) { this.fromPin = fromPin; this.toPin = toPin; this.fromPin.links.push(this); this.toPin.links.push(this); /** * deletes resources associated with a link */ this.destroy = function() { if (this.toPin.reset_on_disconnect) this.toPin.reset(); else this.toPin.node.defaultPinValues[this.toPin.pinname] = this.toPin.values.slice(0); this.fromPin.links.splice(this.fromPin.links.indexOf(this), 1); this.toPin.links.splice(this.toPin.links.indexOf(this), 1); this.fromPin.node.parentPatch.linkList.splice(this.fromPin.node.parentPatch.linkList.indexOf(this),1); } /** * Returns the XML string representing the link. Used for saving the patch and copying to clipboard */ this.serialize = function() { // calling it LONK instead of LINK here, because jquery does not make a closing tag for LINK elements // renaming it to LINK later ... var $link = $("<LONK>"); $link.attr("srcnodeid", this.fromPin.node.id); $link.attr("srcpinname", this.fromPin.pinname); $link.attr("dstnodeid", this.toPin.node.id); $link.attr("dstpinname", this.toPin.pinname); return $link; } }, /** * @class * @constructor * @param {String} ressource either a path/to/some/patch.v4p or VVVV XML code * @param {Function} success_handler called after the patch (and all sub components) has completely loaded and is ready * @param {Function} error_handler called if an error occured, most likey because the .v4p file was not found * @param {VVVV.Core.Patch} [parentPatch] the parent patch, if it's a subpatch * @param {Integer} id the patch's ID in the parent patch, if it's a subpatch */ Patch: function(ressource, success_handler, error_handler, parentPatch, id) { this.ressource = ressource; this.vvvv_version = "45_26.1"; /** the diameter of the patch / the maximum X and Y coordinates of all nodes in a patch */ this.boundingBox = {width: 0, height: 0}; /** @member*/ this.windowWidth = 500; /** @member */ this.windowHeight = 500; /** a hash table containing the pins of all nodes inside a patch, indexed with [node_id]_[in|out|inv]_[pinname] */ this.pinMap = {}; /** a hash table containing all nodes inside a patch, indexed with the node ID */ this.nodeMap = {}; /** an array containing all nodes inside a patch */ this.nodeList = []; /** an array containing all links inside a patch */ this.linkList = []; /** The {@link VVVV.MainLoop} Object running this patch */ this.mainloop = undefined; this.success = success_handler; this.error = error_handler; this.editor = undefined; this.setupObject(); if (parentPatch) this.parentPatch = parentPatch; if (id) this.id = id; var print_timing = false; /** * Returns the patch's absolute path, usable for the browser * @return {String} the absolute path */ this.getAbsolutePath = function() { var path = this.getRelativePath(); if (this.parentPatch) path = this.parentPatch.getAbsolutePath()+path; return path; } /** * Returns a patch's relative path, as it is specified in the paret patch * @return {String} the patch's path relative to its parent patch */ this.getRelativePath = function() { var match = this.nodename.match(/(.*\/)?[^/]+\.v4p/); return match[1] || ''; } /** * Called when a patch is deleted. Deletes all containing nodes. */ this.destroy = function() { for (var i=0; i<this.nodeList.length; i++) { this.nodeList[i].destroy(); delete this.nodeMap[this.nodeList[i].id]; delete this.nodeList[i]; } } /** * Creates an array of slices out of pin value string coming from a patch XML * @param {String} v the pin value string from the XML */ function splitValues(v) { if (v==undefined) return []; if (this.vvvv_version<="45_26") { // legacy code if (/\|/.test(v)) separator = "|"; else separator = ","; return v.split(separator).filter(function(d,i) { return d!=""}); } var result = []; var currSlice = ''; var insideValue = false; var len = v.length; for (var i=0; i<len; i++) { if (v[i]==',' && !insideValue) { result.push(currSlice); currSlice = ''; } else if (v[i]=='|') { if (v[i+1]!='|' || i+1==v.length-1) insideValue = !insideValue; else currSlice += v[++i]; } else currSlice += v[i]; } result.push(currSlice); return result; } if (this.vvvv_version<="45_26") { var oldLinks = {}; var newLinks = {}; var oldNodes = {}; var newNodes = {}; } var thisPatch = this; /** * Takes a patch XML string, parses it, and applies it to the patch. This method is called once by the constructor, passing the complete patch code, and * frequently by an editor, passing in XML snippets. This is the only method you should use to manipulate a patch. * @param {String} xml VVVV Patch XML * @param {Function} ready_callback called after the XML code has been completely processed, and the patch is fully loaded and ready again */ this.doLoad = function(xml, ready_callback) { var p = this; do { p.dirty = true; } while (p=p.parentPatch); var version_match = /^<!DOCTYPE\s+PATCH\s+SYSTEM\s+"(.+)\\(.+)\.dtd/.exec(xml); if (version_match) thisPatch.vvvv_version = version_match[2].replace(/[a-zA-Z]+/, '_'); // this is kind of a hacky way to determine, if the incoming XML is the complete patch, or a patch change var syncmode = 'diff'; if (/\s<PATCH/.test(xml) || thisPatch.vvvv_version<="45_26") { syncmode = 'complete'; if (VVVV_ENV=='development') console.log('complete: '+this.nodename); } var $windowBounds = $(xml).find('bounds[type="Window"]').first(); if ($windowBounds.length>0) { thisPatch.windowWidth = $windowBounds.attr('width')/15; thisPatch.windowHeight = $windowBounds.attr('height')/15; } if (syncmode=='complete') newNodes = {}; var nodesLoading = 0; $(xml).find('node').each(function() { // in case of renaming a node, delete the old one first if ($(this).attr('createme')=='pronto' && thisPatch.nodeMap[$(this).attr('id')]!=undefined) { var n = thisPatch.nodeMap[$(this).attr('id')]; if (VVVV_ENV=='development') console.log("node renamed, so deleting node "+n.id+' / '+n.nodename); _(n.inputPins).each(function(p) { _(p.links).each(function (link) { link.destroy(); link.fromPin.connectionChanged(); }); }) _(n.outputPins).each(function(p) { _(p.links).each(function (link) { link.destroy(); link.toPin.connectionChanged(); link.toPin.markPinAsChanged(); }); }) thisPatch.nodeList.splice(thisPatch.nodeList.indexOf(n),1); delete thisPatch.nodeMap[n.id]; } var $bounds; if ($(this).attr('componentmode')=="InABox") $bounds = $(this).find('bounds[type="Box"]').first(); else $bounds = $(this).find('bounds[type="Node"]').first(); var nodeExists = thisPatch.nodeMap[$(this).attr('id')]!=undefined; if (!nodeExists) { var nodename = $(this).attr('systemname')!="" ? $(this).attr('systemname') : $(this).attr('nodename'); if (nodename==undefined) return; if (VVVV.NodeLibrary[nodename.toLowerCase()]!=undefined) { var n = new VVVV.NodeLibrary[nodename.toLowerCase()]($(this).attr('id'), thisPatch); // load 3rd party libs, if required for this node if (VVVV.NodeLibrary[nodename.toLowerCase()].requirements) { thisPatch.resourcesPending++; // pause patch evaluation _(VVVV.NodeLibrary[nodename.toLowerCase()].requirements).each(function(libname) { if (VVVV.LoadedLibs[libname]===undefined) VVVV.loadScript(VVVV.ThirdPartyLibs[libname], function() { thisPatch.resourcesPending--; // resume patch evaluation }); }); } } else if (/.fx$/.test($(this).attr('filename'))) { var n = new VVVV.Nodes.GenericShader($(this).attr('id'), thisPatch); n.isShader = true; n.shaderFile = $(this).attr('filename').replace(/\\/g, '/').replace(/\.fx$/, '.vvvvjs.fx').replace('lib/nodes/', ''); n.nodename = nodename; } else { if (/.v4p$/.test($(this).attr('filename'))) { thisPatch.resourcesPending++; var that = this; var n = new VVVV.Core.Patch($(this).attr('filename'), function() { thisPatch.resourcesPending--; if (VVVV_ENV=='development') console.log(this.nodename+'invoking update links') updateLinks(xml); if (thisPatch.editor) thisPatch.editor.addPatch(this); if (this.auto_evaluate) { var p = thisPatch; do { p.auto_evaluate = true; } while (p = p.parentPatch); } this.setMainloop(thisPatch.mainloop); thisPatch.afterUpdate(); if (thisPatch.resourcesPending<=0 && ready_callback) { ready_callback(); ready_callback = undefined; } }, function() { thisPatch.resourcesPending--; this.not_implemented = true; VVVV.onNotImplemented(nodename); updateLinks(xml); thisPatch.afterUpdate(); if (thisPatch.resourcesPending<=0 && ready_callback) { ready_callback(); ready_callback = undefined; } }, thisPatch, $(that).attr('id') ); n.isSubpatch = true; if (thisPatch.editor && !n.editor) thisPatch.editor.addPatch(n); thisPatch.nodeMap[n.id] = n; } else { var n = new VVVV.Core.Node($(this).attr('id'), nodename, thisPatch); n.not_implemented = true; if (syncmode=='diff' && VVVV.Config.auto_undo == true) thisPatch.editor.sendUndo(); VVVV.onNotImplemented(nodename); } } if (VVVV_ENV=='development' && syncmode!='complete') console.log(thisPatch.nodename+': inserted new node '+n.nodename); } else n = thisPatch.nodeMap[$(this).attr('id')]; if (n.auto_evaluate) { // as soon as the patch contains a single auto-evaluate node, it is also an auto evaluating subpatch var p = thisPatch; do { p.auto_evaluate = true; } while (p = p.parentPatch); } if ($(this).attr('deleteme')=='pronto') { if (VVVV_ENV=='development') console.log('removing node '+n.id); if (n.isSubpatch) { if (n.editor) n.editor.removePatch(n); var subpatches = n.getSubPatches(); subpatches.push(n); var path; for (var i=0; i<subpatches.length; i++) { path = VVVV.Helpers.prepareFilePath(subpatches[i].nodename, subpatches[i].parentPatch); VVVV.Patches[path].splice(VVVV.Patches[path].indexOf(n), 1); if (VVVV.Patches[path].length == 0) delete VVVV.Patches[path]; } } thisPatch.nodeList.splice(thisPatch.nodeList.indexOf(n),1); n.destroy(); delete thisPatch.nodeMap[n.id]; } if ($bounds.length>0) { if ($bounds.attr('left')) { n.x = $bounds.attr('left')/15; n.y = $bounds.attr('top')/15; thisPatch.boundingBox.width = Math.max(thisPatch.boundingBox.width, n.x+100); thisPatch.boundingBox.height = Math.max(thisPatch.boundingBox.height, n.y+100); } if ($bounds.attr('width')) { n.width = $bounds.attr('width'); n.height = $bounds.attr('height'); } } if (/^iobox/.test(n.nodename.toLowerCase())) n.isIOBox = true; //To add anything which relates to all nodes if (!nodeExists) n.setup(); var that = this; // PINS $(this).find('pin').each(function() { var pinname = $(this).attr('pinname'); var values = splitValues($(this).attr('values')); //Get all defaults from xml if (values!=undefined) { if (values.length > 0) n.addDefault(pinname, values); } // if the output pin already exists (because the node created it), skip if (n.outputPins[pinname]!=undefined) return; // the input pin already exists (because the node created it), don't add it, but set values, if present in the xml if (n.inputPins[pinname]!=undefined) { if (!n.inputPins[pinname].isConnected()) { n.applyPinValuesFromXML(pinname); } return; } // the input pin already exists (because the node created it), don't add it, but set values, if present in the xml if (n.invisiblePins[pinname]!=undefined) { if (values!=undefined) { for (var i=0; i<values.length; i++) { if (n.invisiblePins[pinname].values[i]!=values[i]) n.invisiblePins[pinname].setValue(i, values[i]); } n.invisiblePins[pinname].setSliceCount(values.length); } return; } //Check for non implemented nodes if (($(this).attr('visible')==1 && $(this).attr('pintype')!='Configuration') || n.isSubpatch) { if ($(this).attr('pintype')=="Output" || $(xml).find('link[srcnodeid='+n.id+']').filter("link[srcpinname='"+pinname.replace(/[\[\]]/,'')+"']").length > 0) { if (n.outputPins[pinname] == undefined) { //Add as output list if not already there n.addOutputPin(pinname, values); } } else { if (n.inputPins[pinname] == undefined && n.invisiblePins[pinname] == undefined) { //Add as intput is neither in invisible/input list n.addInputPin(pinname, values); } } } else { if (n.inputPins[pinname] == undefined && n.invisiblePins[pinname] == undefined) { //Add as invisible pin n.addInvisiblePin(pinname, values); } } }); //Initialize node if (!nodeExists) { n.initialize(); thisPatch.nodeList.push(n); } if (syncmode=='complete') newNodes[n.id] = n; }); if (syncmode=='complete') { _(oldNodes).each(function(n, id) { if (newNodes[id]==undefined) { if (VVVV_ENV=='development') console.log('removing node '+n.id); thisPatch.nodeList.splice(thisPatch.nodeList.indexOf(n),1); delete thisPatch.nodeMap[n.id]; } }); oldNodes = {}; _(newNodes).each(function(n, id) { oldNodes[id] = n; }); } if (this.resourcesPending===0) updateLinks(xml); function updateLinks(xml) { if (syncmode=='complete') newLinks = {}; // first delete marked links $(xml).find('link[deleteme="pronto"]').each(function() { var link = false; for (var i=0; i<thisPatch.linkList.length; i++) { if (thisPatch.linkList[i].fromPin.node.id==$(this).attr('srcnodeid') && thisPatch.linkList[i].fromPin.pinname==$(this).attr('srcpinname') && thisPatch.linkList[i].toPin.node.id==$(this).attr('dstnodeid') && thisPatch.linkList[i].toPin.pinname==$(this).attr('dstpinname')) { link = thisPatch.linkList[i]; } } if (!link) return; if (VVVV_ENV=='development') console.log('removing '+link.fromPin.pinname+' -> '+link.toPin.pinname); var fromPin = link.fromPin; var toPin = link.toPin; link.destroy(); fromPin.connectionChanged(); toPin.connectionChanged(); toPin.markPinAsChanged(); }); $(xml).find('link[deleteme!="pronto"]').each(function() { var srcPin = thisPatch.pinMap[$(this).attr('srcnodeid')+'_out_'+$(this).attr('srcpinname')]; var dstPin = thisPatch.pinMap[$(this).attr('dstnodeid')+'_in_'+$(this).attr('dstpinname')]; // add pins which are neither defined in the node, nor defined in the xml, but only appeare in the links (this is the case with shaders) if (srcPin==undefined && thisPatch.nodeMap[$(this).attr('srcnodeid')]) srcPin = thisPatch.nodeMap[$(this).attr('srcnodeid')].addOutputPin($(this).attr('srcpinname'), undefined); if (dstPin==undefined && thisPatch.nodeMap[$(this).attr('dstnodeid')]) dstPin = thisPatch.nodeMap[$(this).attr('dstnodeid')].addInputPin($(this).attr('dstpinname'), undefined); if (srcPin && dstPin) { var link = false; for (var i=0; i<thisPatch.linkList.length; i++) { if (thisPatch.linkList[i].fromPin.node.id==srcPin.node.id && thisPatch.linkList[i].fromPin.pinname==srcPin.pinname && thisPatch.linkList[i].toPin.node.id==dstPin.node.id && thisPatch.linkList[i].toPin.pinname==dstPin.pinname) { link = thisPatch.linkList[i]; } } if (!link) { link = new VVVV.Core.Link(srcPin, dstPin); srcPin.connectionChanged(); dstPin.connectionChanged(); thisPatch.linkList.push(link); for (var i=0; i<srcPin.values.length; i++) { dstPin.setValue(i, srcPin.getValue(i)); } dstPin.setSliceCount(srcPin.getSliceCount()); } if (syncmode=='complete') newLinks[srcPin.node.id+'_'+srcPin.pinname+'-'+dstPin.node.id+'_'+dstPin.pinname] = link; } }); if (syncmode=='complete') { _(oldLinks).each(function(l, key) { if (newLinks[key]==undefined) { if (VVVV_ENV=='development') console.log('removing '+l.fromPin.pinname+' -> '+l.toPin.pinname); var fromPin = l.fromPin; var toPin = l.toPin; l.destroy(); fromPin.connectionChanged(); toPin.connectionChanged(); toPin.markPinAsChanged(); if (toPin.reset_on_disconnect) toPin.reset(); } }); oldLinks = {}; _(newLinks).each(function(l, key) { oldLinks[key] = l; }); } } if (this.resourcesPending<=0 && ready_callback) { ready_callback(); ready_callback = undefined; } } /** * Recursively fetches and returns all subpatches inside a patch * @return {Array} an array of {@link VVVV.Core.Patch} objects */ this.getSubPatches = function() { var ret = []; for (var i=0; i<this.nodeList.length; i++) { if (this.nodeList[i].isSubpatch) { ret.push(this.nodeList[i]); ret = ret.concat(this.nodeList[i].getSubPatches()); } } return ret; } /** * Sets the {@link VVVV.Core.MainLoop} object of the patch and all containing subpatches * @param {VVVV.Core.MainLoop} ml */ this.setMainloop = function(ml) { this.mainloop = ml; for (var i=0; i<this.nodeList.length; i++) { if (this.nodeList[i].isSubpatch) { this.nodeList[i].setMainloop(ml); } } } /** * Called always after the patch has been evaluated * @abstract */ this.afterEvaluate = function() { } /** * Called always after the patch has been modified using {@link VVVV.Core.Patch.doLoad} * @abstract */ this.afterUpdate = function() { } /** * Returns the VVVV XML string representing the patch, ready to be saved * @return {String} */ this.toXML = function() { var $patch = $("<PATCH>"); var $bounds = $("<BOUNDS>"); $bounds.attr("type", "Window"); $bounds.attr("width", parseInt(this.windowWidth * 15)); $bounds.attr("height", parseInt(this.windowHeight * 15)); $patch.append($bounds); var boundTypes = ["Node", "Box"]; for (var i=0; i<this.nodeList.length; i++) { var n = this.nodeList[i]; $patch.append(n.serialize()); } for (var i=0; i<this.linkList.length; i++) { var l = this.linkList[i]; $patch.append(l.serialize()); } var xml = '<!DOCTYPE PATCH SYSTEM "http://vvvv.org/versions/vvvv45beta28.1.dtd" >\r\n '+$patch.wrapAll('<d></d>').parent().html(); xml = xml.replace(/<patch/g, "<PATCH"); xml = xml.replace(/<\/patch>/g, "\n </PATCH>"); xml = xml.replace(/<node/g, "\n <NODE"); xml = xml.replace(/<\/node>/g, "\n </NODE>"); xml = xml.replace(/<bounds/g, "\n <BOUNDS"); xml = xml.replace(/<\/bounds>/g, "\n </BOUNDS>"); xml = xml.replace(/<pin/g, "\n <PIN"); xml = xml.replace(/<\/pin>/g, "\n </PIN>"); xml = xml.replace(/<lonk/g, "\n <LINK"); xml = xml.replace(/<\/lonk>/g, "\n </LINK>"); return xml; } /** * Evaluates the patch once. Is called by the patch's {@link VVVV.Core.MainLoop} each frame, and should not be called directly */ this.evaluate = function() { if (this.resourcesPending>0) // this.resourcesPending is >0 when thirdbarty libs or subpatches are loading at the moment return; if (print_timing) { var nodeProfiles = {}; var start = new Date().getTime(); var elapsed = 0; } var invalidNodes = {}; var terminalNodes = {} for (var i=0; i<this.nodeList.length; i++) { if (this.nodeList[i].getDownstreamNodes().length==0 || this.nodeList[i].auto_evaluate || this.nodeList[i].delays_output) { terminalNodes[this.nodeList[i].id] = this.nodeList[i]; } invalidNodes[this.nodeList[i].id] = this.nodeList[i]; } if (print_timing) console.log('building node maps: '+(new Date().getTime() - start)+'ms') function evaluateSubGraph(node) { //console.log("starting with "+node.nodename+" ("+node.id+")"); var upstreamNodes = node.getUpstreamNodes(); _(upstreamNodes).each(function(upnode) { if (invalidNodes[upnode.id]!=undefined && !upnode.delays_output) { evaluateSubGraph(upnode); } }); if (node.dirty || node.auto_evaluate || node.isSubpatch) { if (print_timing) var start = new Date().getTime(); if (node.auto_nil && !node.isSubpatch && node.hasNilInputs()) { _(node.outputPins).each(function(outPin) { outPin.setSliceCount(0); }); } else { try { node.evaluate(); } catch (e) { console.log('VVVV.Js / Error evaluating '+node.nodename+': '+e.message); } node.dirty = false; _(node.inputPins).each(function(inPin) { inPin.changed = false; }); } if (print_timing) { if (!nodeProfiles[node.nodename]) nodeProfiles[node.nodename] = {count: 0, dt: 0}; elapsed = new Date().getTime() - start; nodeProfiles[node.nodename].count++; nodeProfiles[node.nodename].dt += elapsed; console.log(node.nodename+' / '+node.id+': '+elapsed+'ms') } } delete invalidNodes[node.id]; return true; } _(terminalNodes).each(function(n, id, index) { //console.log('starting anew '+n.nodename); if (invalidNodes[n.id]!=undefined) evaluateSubGraph(n); }); if (print_timing) { _(nodeProfiles).each(function(p, nodename) { console.log(p.count+'x '+nodename+': '+p.dt+'ms'); }); var start = new Date().getTime(); } this.afterEvaluate(); if (print_timing) console.log('patch rendering: '+(new Date().getTime() - start)+'ms') print_timing = false; } $(window).keydown(function(e) { // ctrl + alt + T to print execution times if (e.which==84 && e.altKey && e.ctrlKey) print_timing = true; }); // actually load the patch, depending on the type of resource if (/\.v4p[^<>\s]*$/.test(ressource)) { this.nodename = ressource; var that = this; var path = ressource; if (this.parentPatch) path = VVVV.Helpers.prepareFilePath(ressource, this.parentPatch) if (!VVVV.Patches[path]) { $.ajax({ url: path, type: 'get', dataType: 'text', success: function(r) { that.doLoad(r, function() { VVVV.Patches[path] = VVVV.Patches[path] || []; VVVV.Patches[path].push(that); if (that.success) that.success(); that.afterUpdate(); }); }, error: function() { if (that.error) that.error(); } }); } else { that.doLoad(VVVV.Patches[path][0].toXML(), function() { VVVV.Patches[path].push(that); if (that.success) that.success(); that.afterUpdate(); }); } } else { this.doLoad(ressource, function() { if (this.success) this.success(); }); } // bind the #-shortcuts function checkLocationHash() { var match = window.location.hash.match('#([^\/]+)\/'+thisPatch.ressource+'$'); if (match) { console.log('launching editor ...'); var ed = new VVVV.Editors[match[1]](); ed.enable(thisPatch); } } checkLocationHash(); $(window).bind('hashchange', function() { checkLocationHash(); }); } } VVVV.Core.Patch.prototype = new VVVV.Core.Node();