'use strict';
// Create a global clipboard that can be shared between all instances of the editor tool.
window.wcPlayEditorClipboard = {
bounds: {
top: 0,
left: 0,
width: 0,
height: 0
},
nodes: []
};
/**
* Undo manager by WebCabin
* @see https://github.com/WebCabin/wcUndoManager
* @external wcUndoManager
*/
/**
* Provides a visual interface for editing a Play script. Requires HTML5 canvas.
* @class
* @param {external:jQuery~Object|external:jQuery~Selector|external:domNode} container - The container element.
* @param {wcPlayEditor~Options} [options] - Custom options.
*/
function wcPlayEditor(container, options) {
this.$container = $(container);
this.$typeButton = [];
this.$typeArea = [];
this._paletteSize = 0;
this._chainStyle = 1;
this._chainStyleMax = 1;
this._showingSelector = false;
this._menu = null;
this._size = {x: 0, y: 0};
this._engine = null;
this._parent = null;
this._nodeLibrary = [];
this._eventHandlers = {
onBeforeSave: null,
onSaved: null,
onBeforeLoad: null,
onLoaded: null,
onBeforeImport: null,
onImported: null
};
// this._nodeDrawCount = 0;
// this._chainDrawCount = 0;
this._font = {
breadcrumbs: {size: 10, family: 'Arial', weight: 'bold'},
title: {size: 13, family: 'Arial', weight: 'bold'},
titleDesc: {size: 10, family: 'Arial', weight: 'italic'},
details: {size: 9, family: 'Arial', weight: 'bold'},
links: {size: 9, family: 'Arial'},
property: {size: 9, family: 'Arial', weight: 'italic'},
value: {size: 9, family: 'Arial', weight: 'bold'},
initialValue: {size: 9, family: 'Arial', weight: 'bold italic'},
propertyHeader: {size: 8, family: 'Arial', weight: 'bold italic'}
};
this._drawStyle = {
palette: {
spacing: 10, // Spacing between nodes in the palette view.
scale: 0.7, // Scale to draw nodes within the palette view.
width: 0 // The pixel width of the palette view.
},
palettePopup: {
padding: 5, // Padding limit between the edge of the canvas and the popup window.
searchOffset: 44, // Pixel offset between top of window to the center of the search field.
width: 300,
height: 400
},
node: {
radius: 7, // The radius to draw node corners.
margin: 14 // The pixel space between the property text and the edge of the node border.
},
title: {
spacing: 5, // The pixel space between the title text and the bar that separates the properties.
wrapL: ' ', // The left string to wrap around the title text.
wrapR: ' ', // The right string to wrap around the title text.
placeholder: ' ', // A placeholder label if there is no title name.
nameWrapL: ' (', // The left string to wrap around the name portion of the title text.
nameWrapR: ') ', // The right string to wrap around the name portion of the title text.
details: ' [?] ' // The text to display for the detail popup button for the node.
},
links: {
length: 10, // Length of each link 'nub'
width: 7, // Width of each link 'nub'
spacing: 7, // The pixel space between the text of adjacent links.
padding: 5, // The pixel space between the link and its text.
margin: 7 // The pixel space between the link text and the edge of the node border.
},
property: {
spacing: 5, // The pixel space between adjacent properties.
strLen: 20, // The maximum character length a property value can display.
longStrLen: 43, // The maximum character length a property value can display when in expanded view.
minLength: 30, // The minimum length the property value can be.
valueWrapL: ' [', // The left string to wrap around a property value.
valueWrapR: '] ', // The right string to wrap around a property value.
initialWrapL: ' ', // The left string to wrap around a property initial value.
initialWrapR: ' ', // The right string to wrap around a property initial value.
highlightColor: 'rgba(255, 255, 255, 0.5)',
normalColor: 'rgba(255, 255, 255, 0.5)',
headerColor: 'rgba(255, 255, 255, 0.2)',
highlightBorder: -1,
normalBorder: 1,
headerBorder: -1,
headerSpace: 5
}
};
// Update properties.
this._lastUpdate = 0;
// Control properties.
this._viewportCamera = {x: 0, y: 0, z: 1};
this._viewportBounds = {left: 0, top: 0, width: 0, height: 0};
this._viewportMovingNode = false;
this._viewportMoving = false;
this._viewportMoved = false;
this._paletteMoving = false;
this._autoScrollDirection = {x: 0, y: 0};
this._autoScrollInterval = 0;
this._autoScrollNodes = false;
this._mouse = {x: 0, y: 0};
this._mouseInViewport = false;
this._highlightRect = null;
this._highlightNode = null;
this._selectedNode = null;
this._selectedNodes = [];
this._highlightTitle = false;
this._highlightDetails = false;
this._highlightDebugLog = false;
this._highlightBreakpoint = false;
this._highlightEntryLink = false;
this._highlightExitLink = false;
this._highlightInputLink = false;
this._highlightOutputLink = false;
this._highlightPropertyValue = false;
this._highlightPropertyInitialValue = false;
this._highlightViewport = false;
this._selectedEntryLink = false;
this._selectedExitLink = false;
this._selectedInputLink = false;
this._selectedOutputLink = false;
this._selectedNodeOrigins = [];
// this._draggingNodeData = null;
this._highlightCrumb = -1;
this._crumbBounds = [];
// Undo management is optional.
this._undoManager = null;
// Setup our options.
this._options = {
readOnly: false,
playable: true,
category: {
items: [],
isBlacklist: true
}
};
for (var prop in options) {
this._options[prop] = options[prop];
}
this.$top = $('<div class="wcPlayEditorTop" tabindex="1">');
this.$main = $('<div class="wcPlayEditorMain" tabindex="1" oncontextmenu="return false;">');
this.$palette = $('<div class="wcPlayPalette wcPlayNoHighlights" tabindex="1">');
this.$paletteScroller = $('<div class="wcPlayPaletteScroller" tabindex="1">');
this.$paletteInner = $('<div class="wcPlayPaletteInner" tabindex="1">');
this.$viewport = $('<canvas class="wcPlayViewport" tabindex="1">');
this._viewportContext = this.$viewport[0].getContext('2d');
this.$palette.append(this.$paletteScroller);
this.$paletteScroller.append(this.$paletteInner);
// this.$main.append(this.$palette);
this.$main.append(this.$viewport);
this.$container.append(this.$top);
this.$container.append(this.$main);
this.$search = $(
'<div class="wcPlayEditorSearch wcPlayHidden">' +
'<span>Search</span>' +
'<input type="text"/>' +
'<i class="fa fa-chevron-up wcPlayEditorSearchPrev"/>' +
'<i class="fa fa-chevron-down wcPlayEditorSearchNext"/>' +
'</div>');
this.$hiddenFileLoader = $('<input type="file" id="wcPlayEditorHiddenFileLoader"/>');
this.$hiddenFileImporter = $('<input type="file" id="wcPlayEditorHiddenFileImporter"/>');
this.$container.append(this.$search);
this.onResized();
this.__setupMenu();
this.__setupControls();
this.$top.focus();
window.requestAnimationFrame(this.__update.bind(this));
}
wcPlayEditor.prototype = {
/**
* Retrieves whether unsaved changes exist in the current script.
* @function wcPlayEditor#isModified
* @returns {boolean} - Whether the script has unsaved changes.
*/
isModified: function() {
if (this._engine && this._engine._undoManager) {
return this._engine._undoManager.isModified();
}
return false;
},
/**
* Clears the modified state of the current script.
* Note, this is done automatically when saving the script,
* use this only under special circumstances.
* @function wcPlayEditor#clearModified
*/
clearModified: function() {
if (this._engine && this._engine._undoManager) {
this._engine._undoManager.clearModified();
}
},
/**
* Gets, or Sets the {@link wcPlay} engine that this renderer will render.
* @function wcPlayEditor#engine
* @param {wcPlay} [engine] - If supplied, will assign a new {@link wcPlay} engine to render.
* @returns {wcPlay} - The current {@link wcPlay} engine.
*/
engine: function(engine) {
if (engine !== undefined && engine !== this._engine) {
if (this._engine) {
var index = this._engine._editors.indexOf(this);
if (index > -1) {
this._engine._editors.splice(index, 1);
}
this._engine._undoManager = this._undoManager;
this._undoManager = null;
}
this._engine = engine;
this._parent = engine;
this.__setupPalette();
this.center();
if (this._engine) {
this._engine._editors.push(this);
this._undoManager = this._engine._undoManager;
if (!this._undoManager && window.wcUndoManager) {
this._undoManager = new wcUndoManager();
this._engine._undoManager = this._undoManager;
}
}
}
return this._engine;
},
/**
* Retrieves the menu instance.
* @function wcPlayEditor#menu
* @returns {wcMenu} - The wcMenu object.
*/
menu: function() {
return this._menu;
},
/**
* Positions the canvas view to the center of all nodes.
* @function wcPlayEditor#center
*/
center: function() {
if (this._parent) {
this.focus(this._parent._entryNodes.concat(this._parent._processNodes, this._parent._storageNodes, this._parent._compositeNodes));
}
},
/**
* Scrolls the canvas view until a given set of nodes are within view.
* @function wcPlayEditor#focus
* @param {wcNode} nodes - A list of nodes to focus the view on.
*/
focus: function(nodes) {
var MIN_WIDTH = 1000;
if (nodes.length) {
this.__updateNodes(nodes, 0, this._viewportContext);
this.__drawNodes(nodes, this._viewportContext);
var boundList = [];
var offsetList = [];
for (var i = 0; i < nodes.length; ++i) {
boundList.push(nodes[i]._meta.bounds.farRect);
offsetList.push(nodes[i].pos);
}
var focusRect = this.__expandRect(boundList, offsetList);
// Clamp the focus rect to a minimum size, so we can not zoom in too far.
if (focusRect.width < MIN_WIDTH) {
focusRect.left -= (MIN_WIDTH - focusRect.width)/2;
focusRect.width = MIN_WIDTH;
}
this.focusRect(focusRect);
}
},
/**
* Scrolls the canvas view and centers on a given bounding rectangle.
* @function wcPlayEditor#focusRect
* @param {wcPlayEditor~Rect} rect - The rectangle to focus on.
*/
focusRect: function(rect) {
var PADDING = 100;
var scaleX = this.$viewport.width()? (this.$viewport.width() / (rect.width + PADDING)): 1;
var scaleY = this.$viewport.height()? (this.$viewport.height() / (rect.height + PADDING)): 1;
this._viewportCamera.z = Math.min(scaleX, scaleY);
if (scaleX > scaleY) {
rect.left -= ((this.$viewport.width() / scaleY - (rect.width + PADDING))) / 2;
} else {
rect.top -= ((this.$viewport.height() / scaleX - (rect.height + PADDING))) / 2;
}
this._viewportCamera.x = -(rect.left - PADDING / 2) * this._viewportCamera.z;
this._viewportCamera.y = -(rect.top - PADDING / 2) * this._viewportCamera.z;
},
/**
* Triggers a previously bound event handler.
* @function wcPlayEditor#triggerEvent
* @param {string} eventName - The name of the event to trigger.
* @param {Object[]} args - Any parameter arguments you wish to send to the triggered event nodes.
* These parameters are based on the node being triggered.
*/
triggerEvent: function(eventName, args) {
if ({}.hasOwnProperty.call(this._eventHandlers, eventName)) {
if (this._eventHandlers[eventName]) {
this._eventHandlers[eventName].apply(this, args);
}
}
},
/**
* Outputs a log message.
* @function wcPlayEditor#log
* @param {...string} args - The log messages.
*/
log: function(args) {
/* eslint-disable no-console */
args = Array.prototype.slice.call(arguments);
args.splice(0, 0, 'wcPlayEditor:');
console.log.apply(console, args);
/* eslint-enable no-console */
},
/**
* Outputs an error message.
* @function wcPlayEditor#error
* @param {...string} args - The log messages.
*/
error: function(args) {
/* eslint-disable no-console */
args = Array.prototype.slice.call(arguments);
args.splice(0, 0, 'wcPlayEditor ERROR:');
if (console.error) {
console.error.apply(console, args);
} else {
console.log.apply(console, args);
}
/* eslint-enable no-console */
},
/**
* Binds an event handler for when we are about to save our script.
* @function wcPlayEditor#onBeforeSave
* @param {Function} func - Assigns the function to handle this event.
* @returns {boolean} - Success or failure.
*/
onBeforeSave: function(func) {
if (typeof func !== 'function') {
this.error('Failed to bind event handler for onBeforeSave, argument must be a function!');
return false;
}
this._eventHandlers.onBeforeSave = func;
return true;
},
/**
* Binds an event handler for when we are about to save our script.
* @function wcPlayEditor#onSaved
* @param {Function} func - Assigns the function to handle this event.
* @returns {boolean} - Success or failure.
*/
onSaved: function(func) {
if (typeof func !== 'function') {
this.error('Failed to bind event handler for onSaved, argument must be a function!');
return false;
}
this._eventHandlers.onSaved = func;
return true;
},
/**
* Binds an event handler for when we are about to save our script.
* @function wcPlayEditor#onBeforeLoad
* @param {Function} func - Assigns the function to handle this event.
* @returns {boolean} - Success or failure.
*/
onBeforeLoad: function(func) {
if (typeof func !== 'function') {
this.error('Failed to bind event handler for onBeforeLoad, argument must be a function!');
return false;
}
this._eventHandlers.onBeforeLoad = func;
return true;
},
/**
* Binds an event handler for when we are about to save our script.
* @function wcPlayEditor#onLoaded
* @param {Function} func - Assigns the function to handle this event.
* @returns {boolean} - Success or failure.
*/
onLoaded: function(func) {
if (typeof func !== 'function') {
this.error('Failed to bind event handler for onLoaded, argument must be a function!');
return false;
}
this._eventHandlers.onLoaded = func;
return true;
},
/**
* Binds an event handler for when we are about to save our script.
* @function wcPlayEditor#onBeforeImport
* @param {Function} func - Assigns the function to handle this event.
* @returns {boolean} - Success or failure.
*/
onBeforeImport: function(func) {
if (typeof func !== 'function') {
this.error('Failed to bind event handler for onBeforeImport, argument must be a function!');
return false;
}
this._eventHandlers.onBeforeImport = func;
return true;
},
/**
* Binds an event handler for when we are about to save our script.
* @function wcPlayEditor#onImported
* @param {Function} func - Assigns the function to handle this event.
* @returns {boolean} - Success or failure.
*/
onImported: function(func) {
if (typeof func !== 'function') {
this.error('Failed to bind event handler for onImported, argument must be a function!');
return false;
}
this._eventHandlers.onImported = func;
return true;
},
/**
* Event that is called to begin an undo manager group operation.
* @function wcPlayEditor#onBeginUndoGroup
* @param {string} description - The description of the undo event.
*/
onBeginUndoGroup: function(description) {
if (this._undoManager) {
this._undoManager.beginGroup(description);
}
},
/**
* Event that is called to end an undo manager group operation.
* @function wcPlayEditor#onEndUndoGroup
*/
onEndUndoGroup: function() {
if (this._undoManager) {
this._undoManager.endGroup();
}
},
/**
* Event that is called when the container view is resized.
* @function wcPlayEditor#onResized
*/
onResized: function() {
var width = this.$main.width();
var height= this.$main.height();
if (this._size.x !== width || this._size.y !== height || this._size.z !== this._paletteSize) {
this._size.x = width;
this._size.y = height;
this._size.z = this._paletteSize;
this.$palette.css('width', this._size.z).attr('width', this._size.z).attr('height', height);
this.$viewport.css('width', width - this._size.z).attr('width', width - this._size.z).attr('height', height);
}
this._viewportBounds.top = -this._viewportCamera.y / this._viewportCamera.z;
this._viewportBounds.left = -this._viewportCamera.x / this._viewportCamera.z;
this._viewportBounds.width = this._size.x / this._viewportCamera.z;
this._viewportBounds.height = this._size.y / this._viewportCamera.z;
},
/**
* Disconnects all chains attached to a node's entry link.
* @function wcPlayEditor#onDisconnectEntryChains
* @param {wcNode} node - The node to disconnect from.
* @param {string} linkName - The name of the link.
*/
onDisconnectEntryChains: function(node, linkName) {
var chains = node.listEntryChains(linkName);
if (chains.length) {
if (this._undoManager) {
this._undoManager.addEvent('Disconnected Entry Links for "' + node.category + '.' + node.type + '.' + linkName + '"',
{
id: node.id,
name: linkName,
chains: chains,
engine: this._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
for (var i = 0; i < this.chains.length; ++i) {
var targetNode = this.engine.nodeById(this.chains[i].outNodeId);
var targetName = this.chains[i].outName;
myNode.connectEntry(this.name, targetNode, targetName);
}
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
myNode.disconnectEntry(this.name);
});
}
}
node.disconnectEntry(linkName);
},
/**
* Disconnects all chains attached to a node's exit link.
* @function wcPlayEditor#onDisconnectExitChains
* @param {wcNode} node - The node to disconnect from.
* @param {string} linkName - The name of the link.
*/
onDisconnectExitChains: function(node, linkName) {
var chains = node.listExitChains(linkName);
if (chains.length) {
if (this._undoManager) {
this._undoManager.addEvent('Disconnected Exit Links for "' + node.category + '.' + node.type + '.' + linkName + '"',
{
id: node.id,
name: linkName,
chains: chains,
engine: this._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
for (var i = 0; i < this.chains.length; ++i) {
var targetNode = this.engine.nodeById(this.chains[i].inNodeId);
var targetName = this.chains[i].inName;
myNode.connectExit(this.name, targetNode, targetName);
}
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
myNode.disconnectExit(this.name);
});
}
}
node.disconnectExit(linkName);
},
/**
* Disconnects all chains attached to a node's input link.
* @function wcPlayEditor#onDisconnectInputChains
* @param {wcNode} node - The node to disconnect from.
* @param {string} linkName - The name of the link.
*/
onDisconnectInputChains: function(node, linkName) {
var chains = node.listInputChains(linkName);
if (chains.length) {
if (this._undoManager) {
this._undoManager.addEvent('Disconnected Property Input Links for "' + node.category + '.' + node.type + '.' + linkName + '"',
{
id: node.id,
name: linkName,
chains: chains,
engine: this._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
for (var i = 0; i < this.chains.length; ++i) {
var targetNode = this.engine.nodeById(this.chains[i].outNodeId);
var targetName = this.chains[i].outName;
myNode.connectInput(this.name, targetNode, targetName);
}
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
myNode.disconnectInput(this.name);
});
}
}
node.disconnectInput(linkName);
},
/**
* Disconnects all chains attached to a node's output link.
* @function wcPlayEditor#onDisconnectOutputChains
* @param {wcNode} node - The node to disconnect from.
* @param {string} linkName - The name of the link.
*/
onDisconnectOutputChains: function(node, linkName) {
var chains = node.listOutputChains(linkName);
if (chains.length) {
if (this._undoManager) {
this._undoManager.addEvent('Disconnected Property Output Links for "' + node.category + '.' + node.type + '.' + linkName + '"',
{
id: node.id,
name: linkName,
chains: chains,
engine: this._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
for (var i = 0; i < this.chains.length; ++i) {
var targetNode = this.engine.nodeById(this.chains[i].inNodeId);
var targetName = this.chains[i].inName;
myNode.connectOutput(this.name, targetNode, targetName);
}
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
myNode.disconnectOutput(this.name);
});
}
}
node.disconnectOutput(linkName);
},
/**
* Retrieve mouse or touch position.
* @function wcPlayEditor#__mouse
* @private
* @param {Object} event - The mouse event.
* @param {wcPlayEditor~Offset} [offset] - An optional screen offset to apply to the pos.
* @param {wcPlay~Coordinates} [translation] - An optional camera translation to apply to the pos.
* @returns {wcPlay~Coordinates} - The mouse position.
*/
__mouse: function(event, offset, translation) {
if (event.originalEvent && (event.originalEvent.touches || event.originalEvent.changedTouches)) {
var touch = event.originalEvent.touches[0] || event.originalEvent.changedTouches[0];
return {
x: touch.clientX - (offset? offset.left: 0) - (translation? translation.x: 0),
y: touch.clientY - (offset? offset.top: 0) - (translation? translation.y: 0),
gx: touch.clientX,
gy: touch.clientY,
which: 1
};
}
return {
x: event.pageX - (offset? offset.left: 0) - (translation? translation.x: 0),
y: event.pageY - (offset? offset.top: 0) - (translation? translation.y: 0),
gx: event.pageX,
gy: event.pageY,
which: event.which || 1
};
},
/**
* Assigns font data to the canvas.
* @function wcPlayEditor#__setCanvasFont
* @private
* @param {Object} font - The font data to assign (wcPlayEditor~_font object).
* @param {external:Canvas~Context} context - The canvas context.
*/
__setCanvasFont: function(font, context) {
context.font = (font.weight? font.weight + ' ': '') + (font.size + 'px ') + font.family;
},
/**
* Clamps a given string value to a specific number of characters and appends a '...' if necessary.
* @function wcPlayEditor#__clampString
* @private
* @param {string} str - The string to clamp.
* @param {number} len - The number of characters to allow.
* @returns {string} - A clamped string.
*/
__clampString: function(str, len) {
if (str.length > len) {
return str.substr(0, len) + '...';
}
return str;
},
/**
* Blends two colors together. Color strings can be in hex string {'#ffffff'} or rgb string {'rgb(250,250,250)'} formats.
* @function wcPlayEditor#__blendColors
* @private
* @param {string} c0 - The first color string.
* @param {string} c1 - The second color string.
* @param {number} p - A multiplier to blend the colors by.
*/
__blendColors: function(c0, c1, p) {
/* eslint-disable */
var n=p<0?p*-1:p, u=Math.round, w=parseInt, f=0, t=0, R=0, G=0, B=0, R1=0, G1=0, B1=0;
if (c0.length>7) {
f=c0.split(','), t=(c1?c1:p<0?'rgb(0,0,0)':'rgb(255,255,255)').split(','), R=w(f[0].slice(4)), G=w(f[1]), B=w(f[2]);
return 'rgb('+(u((w(t[0].slice(4))-R)*n)+R)+','+(u((w(t[1])-G)*n)+G)+','+(u((w(t[2])-B)*n)+B)+')';
} else {
f=w(c0.slice(1), 16), t=w((c1?c1:p<0?'#000000':'#FFFFFF').slice(1), 16), R1=f>>16, G1=f>>8&0x00FF, B1=f&0x0000FF;
return '#'+(0x1000000+(u(((t>>16)-R1)*n)+R1)*0x10000+(u(((t>>8&0x00FF)-G1)*n)+G1)*0x100+(u(((t&0x0000FF)-B1)*n)+B1)).toString(16).slice(1);
}
/* eslint-enable */
},
/**
* Retrieves a bounding rectangle that encloses all given rectangles.
* @function wcPlayEditor#__expandRect
* @private
* @param {wcPlayEditor~Rect[]} rects - A list of rectangles to expand from.
* @param {wcPlay~Coordinates[]} [offsets] - Optional offsets for each rectangle, must be the same size as rects param.
* @returns {wcPlayEditor~Rect} - A bounding rectangle that encloses all given rectangles.
*/
__expandRect: function(rects, offsets) {
var bounds = {
top: rects[0].top + (offsets? offsets[0].y: 0),
left: rects[0].left + (offsets? offsets[0].x: 0),
width: rects[0].width,
height: rects[0].height
};
var i = 0;
var offsetX = 0;
var offsetY = 0;
for (i = 1; i < rects.length; ++i) {
offsetX = 0;
offsetY = 0;
if (offsets) {
offsetX = offsets[i].x;
offsetY = offsets[i].y;
}
if ((rects[i].top + offsetY) < bounds.top) {
bounds.top = (rects[i].top + offsetY);
}
if ((rects[i].left + offsetX) < bounds.left) {
bounds.left = (rects[i].left + offsetX);
}
}
for (i = 0; i < rects.length; ++i) {
offsetX = 0;
offsetY = 0;
if (offsets) {
offsetX = offsets[i].x;
offsetY = offsets[i].y;
}
if ((rects[i].top + offsetY) + rects[i].height > bounds.top + bounds.height) {
bounds.height = ((rects[i].top + offsetY) + rects[i].height) - bounds.top;
}
if ((rects[i].left + offsetX) + rects[i].width > bounds.left + bounds.width) {
bounds.width = ((rects[i].left + offsetX) + rects[i].width) - bounds.left;
}
}
return bounds;
},
/**
* Tests whether a given point is within a bounding rectangle.
* @function wcPlayEditor#__inRect
* @private
* @param {wcPlay~Coordinates} pos - The position to test.
* @param {wcPlayEditor~Rect} rect - The bounding rectangle.
* @param {wcPlay~Coordinates} [offset] - An optional offset to apply to the rect.
* @param {wcPlay~Coordinates} [trans] - An optional camera translation to apply to the pos.
* @returns {boolean} - Whether there is a collision.
*/
__inRect: function(pos, rect, offset, trans) {
if (offset === undefined) {
offset = {
x: 0,
y: 0
};
}
if (trans === undefined) {
trans = {
x: 0,
y: 0,
z: 1
};
}
if ((pos.y - trans.y) / trans.z >= offset.y + rect.top &&
(pos.x - trans.x) / trans.z >= offset.x + rect.left &&
(pos.y - trans.y) / trans.z <= offset.y + rect.top + rect.height &&
(pos.x - trans.x) / trans.z <= offset.x + rect.left + rect.width) {
return true;
}
return false;
},
/**
* Tests whether a given rectangle is within a bounding rectangle.
* @function wcPlayEditor#__rectOnRect
* @private
* @param {wcPlayEditor~Rect} rectA - The first rectangle.
* @param {wcPlayEditor~Rect} rectB - The second rectangle.
* @param {wcPlay~Coordinates} [offsetA] - An optional offset to apply to the rectA.
* @returns {boolean} - Whether there is a collision.
*/
__rectOnRect: function(rectA, rectB, offsetA) {
if (offsetA === undefined) {
offsetA = {
x: 0,
y: 0
};
}
return !(rectB.left > offsetA.x + rectA.left + rectA.width ||
rectB.left + rectB.width < offsetA.x + rectA.left ||
rectB.top > offsetA.y + rectA.top + rectA.height ||
rectB.top + rectB.height < offsetA.y + rectA.top);
},
/**
* Draws a rounded rectangle.
* @function wcPlayEditor#__drawRoundedRect
* @private
* @param {wcPlayEditor~Rect} rect - The rectangle bounds to draw.
* @param {string} color - The color of the line.
* @param {number} lineWidth - The thickness of the line, -1 will fill the shape.
* @param {number} radius - The radius of the rounded corners.
* @param {external:Canvas~Context} context - The canvas context to render on.
* @param {wcPlay~Coordinates} [pos] - An option positional offset to draw the rect.
*/
__drawRoundedRect: function(rect, color, lineWidth, radius, context, pos) {
if (!pos) {
pos = {x: 0, y: 0};
}
context.save();
context.strokeStyle = color;
context.fillStyle = color;
context.lineWidth = (lineWidth > 0)? lineWidth: 1;
context.beginPath();
context.moveTo(pos.x + rect.left + radius, pos.y + rect.top);
context.arcTo(pos.x + rect.left + rect.width, pos.y + rect.top, pos.x + rect.left + rect.width, pos.y + rect.top + radius, radius);
context.arcTo(pos.x + rect.left + rect.width, pos.y + rect.top + rect.height, pos.x + rect.left + rect.width - radius, pos.y + rect.top + rect.height, radius);
context.arcTo(pos.x + rect.left, pos.y + rect.top + rect.height, pos.x + rect.left, pos.y + rect.top + rect.height - radius, radius);
context.arcTo(pos.x + rect.left, pos.y + rect.top, pos.x + rect.left + radius, pos.y + rect.top, radius);
context.closePath();
if (lineWidth === -1) {
context.fill();
} else {
context.stroke();
}
context.restore();
},
/**
* Renders a new frame.
* @function wcPlayEditor#__update
* @param {number} timestamp - The current timestamp.
* @private
*/
__update: function(timestamp) {
if (!this._lastUpdate) {
this._lastUpdate = timestamp;
}
var ONE_SECOND = 1000;
var elapsed = (timestamp - this._lastUpdate) / ONE_SECOND;
this._lastUpdate = timestamp;
this.onResized();
this._menu.update();
if (this._parent) {
// Render the palette.
// this.__drawPalette(elapsed);
// Setup viewport canvas.
this._viewportContext.clearRect(0, 0, this.$viewport.width(), this.$viewport.height());
this._viewportContext.save();
this._viewportContext.translate(this._viewportCamera.x, this._viewportCamera.y);
this._viewportContext.scale(this._viewportCamera.z, this._viewportCamera.z);
// Update nodes.
this.__updateNodes(this._parent._entryNodes, elapsed, this._viewportContext);
this.__updateNodes(this._parent._processNodes, elapsed, this._viewportContext);
this.__updateNodes(this._parent._storageNodes, elapsed, this._viewportContext);
this.__updateNodes(this._parent._compositeNodes, elapsed, this._viewportContext);
// Render the nodes in the main script.
this.__drawNodes(this._parent._entryNodes, this._viewportContext);
this.__drawNodes(this._parent._processNodes, this._viewportContext);
this.__drawNodes(this._parent._compositeNodes, this._viewportContext);
this.__drawNodes(this._parent._storageNodes, this._viewportContext);
// Render chains between nodes.
this.__drawChains(this._parent._entryNodes, this._viewportContext);
this.__drawChains(this._parent._processNodes, this._viewportContext);
this.__drawChains(this._parent._compositeNodes, this._viewportContext);
this.__drawChains(this._parent._storageNodes, this._viewportContext);
if (this._highlightRect) {
var MAX_RADIUS = 10;
var radius = Math.min(MAX_RADIUS, this._highlightRect.width/2, this._highlightRect.height/2);
this.__drawRoundedRect(this._highlightRect, 'rgba(0, 255, 255, 0.25)', -1, radius, this._viewportContext);
this.__drawRoundedRect(this._highlightRect, 'darkcyan', 2, radius, this._viewportContext);
}
// this.log('Draw count - Nodes: ' + this._nodeDrawCount + ', Chains: ' + this._chainDrawCount);
// this._nodeDrawCount = 0;
// this._chainDrawCount = 0;
// if (this._selectedNodes.length) {
// var boundList = [];
// var offsetList = [];
// for (var i = 0; i < this._selectedNodes.length; ++i) {
// var node = this._selectedNodes[i];
// boundList.push(node._meta.bounds.farRect);
// offsetList.push(node.pos);
// }
// this.__drawRoundedRect(this.__expandRect(boundList, offsetList), "red", -1, 10, this._viewportContext);
// }
this._viewportContext.restore();
// Draw breadcrumbs.
var scopes = [];
var scopeNames = [];
var scope = this._parent;
while (!(scope && scope.instanceOf('wcPlay'))) {
scopes.unshift(scope);
scopeNames.unshift(scope.type + (scope.name? ' (' + scope.name + ')': ''));
scope = scope._parent;
}
scopes.unshift(this._engine);
scopeNames.unshift('Root');
this._viewportContext.fillStyle = 'black';
this.__setCanvasFont(this._font.breadcrumbs, this._viewportContext);
this._crumbBounds = [];
var left = 2;
for (var i = 0; i < scopeNames.length; ++i) {
var w = this._viewportContext.measureText(scopeNames[i]).width;
var w2 = this._viewportContext.measureText(' / ').width;
var boundData = {
rect: {
top: 0,
left: left,
width: w + 6,
height: this._font.breadcrumbs.size + this._drawStyle.property.spacing
},
parent: scopes[i]
};
this._crumbBounds.push(boundData);
left += w + w2;
if (this._highlightCrumb === i) {
this.__drawRoundedRect(boundData.rect, 'rgba(0, 255, 255, 0.25)', -1, 3, this._viewportContext);
this.__drawRoundedRect(boundData.rect, 'darkcyan', 2, 3, this._viewportContext);
}
}
this._viewportContext.fillText(scopeNames.join(' / '), 5, this._font.breadcrumbs.size);
}
window.requestAnimationFrame(this.__update.bind(this));
},
/**
* Updates the status of a list of nodes.
* @function wcPlayEditor#__updateNodes
* @private
* @param {wcNode[]} nodes - The nodes to update.
* @param {number} elapsed - Elapsed time since last update.
* @param {external:Canvas~Context} context - The canvas context to render on.
*/
__updateNodes: function(nodes, elapsed, context) {
for (var i = 0; i < nodes.length; ++i) {
this.__updateNode(nodes[i], elapsed, context);
}
},
/**
* Updates the status of a node.
* @function wcPlayEditor#__updateNode
* @private
* @param {wcNode} node - The Node to update.
* @param {number} elapsed - Elapsed time since last update.
* @param {external:Canvas~Context} context - The canvas context to render on.
*/
__updateNode: function(node, elapsed, context) {
node.onDraw();
// Update flash state.
var self = this;
// Options:
// keepBroken - Keeps the flash state alive unless engine is paused.
// forceActive - Forces the flash state alive unconditionally.
// colorMul=1 - Additional multiplier to apply to the flash alpha.
function __updateFlash(meta, darkColor, lightColor, pauseColor, options) {
options = options || {};
if (meta.flash || options.forceActive) {
meta.flashDelta += elapsed * 10.0;
if (meta.flashDelta >= 1.0) {
meta.flashDelta = 1.0;
if (!meta.awake && (!meta.broken || (!options.keepBroken && !self._engine.paused()))) {
meta.flash = false;
}
}
} else if (meta.flashDelta > 0.0) {
meta.flashDelta -= elapsed * 5.0;
if (meta.flashDelta <= 0.0) {
meta.flashDelta = 0;
meta.broken = options.keepBroken? meta.broken: meta.broken - 1;
}
}
meta.color = self.__blendColors(darkColor, meta.broken? pauseColor: lightColor, meta.flashDelta * (options.colorMul || 1));
return meta.flash;
}
var blackColor = '#000000';
var whiteColor = '#FFFFFF';
var propColor = '#117711';
var propFlashColor = '#FFFF00';
var forceNodeActive = false;
var i = 0;
if (this._engine._queuedProperties.length === 0) {
for (i = 0; i < node.chain.entry.length; ++i) {
__updateFlash(node.chain.entry[i].meta, blackColor, propFlashColor, propFlashColor, {colorMul: 0.9});
}
for (i = 0; i < node.chain.exit.length; ++i) {
__updateFlash(node.chain.exit[i].meta, blackColor, propFlashColor, propFlashColor, {colorMul: 0.9});
}
} else {
// If any entry links are active, keep the node highlighted.
for (i = 0; i < node.chain.entry.length; ++i) {
if (node.chain.entry[i].meta.flash || node.chain.entry[i].meta.flashDelta > 0) {
forceNodeActive = true;
break;
}
}
}
for (i = 0; i < node.properties.length; ++i) {
__updateFlash(node.properties[i].inputMeta, propColor, propFlashColor, propFlashColor, {colorMul: 0.9});
__updateFlash(node.properties[i].outputMeta, propColor, propFlashColor, propFlashColor, {colorMul: 0.9});
}
var color = node.color;
if (this._highlightNode === node) {
color = this.__blendColors(node.color, whiteColor, 0.25);
}
__updateFlash(node._meta, color, whiteColor, whiteColor, {
keepBroken: true,
forceActive: forceNodeActive,
colorMul: 0.5
});
// Measure bounding areas for node, if it is dirty.
if (node._meta.dirty) {
node._meta.dirty = false;
node._meta.bounds = {
node: node
};
var entryBounds = this.__measureEntryLinkOuter(node, context);
var outerBounds = this.__measureOuter(node, context, entryBounds.height);
var exitBounds = this.__measureExitLinkOuter(node, context, entryBounds.height + outerBounds.height);
var bounds = this.__expandRect([entryBounds, outerBounds, exitBounds]);
bounds.top = outerBounds.top;
bounds.height = outerBounds.height;
bounds.propWidth = outerBounds.propWidth;
bounds.valueWidth = outerBounds.valueWidth;
bounds.initialWidth = outerBounds.initialWidth;
node._meta.bounds.entryOuter = entryBounds;
node._meta.bounds.exitOuter = exitBounds;
node._meta.bounds.centerOuter = outerBounds;
node._meta.bounds.rect = this.__expandRect([entryBounds, outerBounds, exitBounds]);
node._meta.bounds.inner = this.__expandRect([outerBounds]);
node._meta.bounds.inner.left = node._meta.bounds.rect.left;
node._meta.bounds.inner.width = node._meta.bounds.rect.width;
node._meta.bounds.rect.top -= 3;
node._meta.bounds.rect.left -= this._drawStyle.links.length + 3;
node._meta.bounds.rect.width += this._drawStyle.links.length * 2 + 6;
node._meta.bounds.rect.height += 6;
if (node.chain.entry.length) {
node._meta.bounds.inner.top -= this._drawStyle.links.padding + this._font.links.size;
node._meta.bounds.inner.height += this._drawStyle.links.padding + this._font.links.size;
} else {
node._meta.bounds.rect.top -= this._drawStyle.links.length;
node._meta.bounds.rect.height += this._drawStyle.links.length;
}
if (node.chain.exit.length) {
node._meta.bounds.inner.height += this._drawStyle.links.padding + this._font.links.size;
} else {
node._meta.bounds.rect.height += this._drawStyle.links.length;
}
var maxValueWidth = (node._meta.bounds.inner.width - this._drawStyle.node.margin*2 - bounds.propWidth);
if (maxValueWidth > bounds.valueWidth + bounds.initialWidth) {
var extra = (maxValueWidth - (bounds.valueWidth + bounds.initialWidth)) / 2;
bounds.valueWidth += extra;
bounds.initialWidth += extra;
}
// Now use our measurements to draw our node.
this.__measureCenter(node, context, bounds);
this.__measureEntryLinks(node, context, entryBounds.width);
this.__measureExitLinks(node, context, entryBounds.height + outerBounds.height, exitBounds.width);
node._meta.bounds.farRect = {
top: node._meta.bounds.inner.top - 30,
left: node._meta.bounds.inner.left - 30,
width: node._meta.bounds.inner.width + 60,
height: node._meta.bounds.inner.height + 60
};
// Add a collapse button to the node in the left margin of the title.
node._meta.bounds.debugLog = {
left: node._meta.bounds.inner.left + 4,
top: node._meta.bounds.inner.top + 4 + (node.chain.entry.length? this._font.links.size + this._drawStyle.links.padding: 0),
width: this._drawStyle.node.margin - 5,
height: this._font.title.size - 4
};
// Add breakpoint button to the node in the right margin of the title.
node._meta.bounds.breakpoint = {
left: node._meta.bounds.inner.left + node._meta.bounds.inner.width - this._drawStyle.node.margin + 2,
top: node._meta.bounds.inner.top + 4 + (node.chain.entry.length? this._font.links.size + this._drawStyle.links.padding: 0),
width: this._drawStyle.node.margin - 5,
height: this._font.title.size - 4
};
}
},
/**
* Retrieves the index for a node type.
* @function wcPlayEditor#__typeIndex
* @private
* @param {wcPlay.NODE} type - The node type.
* @returns {number} - The type index.
*/
__typeIndex: function(type) {
switch (type) {
case wcPlay.NODE.ENTRY: return 0;
default:
case wcPlay.NODE.PROCESS: return 1;
case wcPlay.NODE.STORAGE: return 2;
case wcPlay.NODE.COMPOSITE: return 3;
}
},
/**
* Initializes the file menu and toolbar.
* @function wcPlayEditor#__setupMenu
* @private
*/
__setupMenu: function() {
this._menu = new wcMenu(this.$top, {
outer: this.$main,
manualUpdate: true,
data: this,
version: 'v1.1.0'
});
// File -> New Script...
this._menu.addOption('File', 'New Script', {
hotkeys: 'Alt+N',
icon: 'fa fa-file-o fa-lg',
description: 'Start a new script...',
toolbarIndex: -1,
condition: function(editor) {
return !editor._options.readOnly;
},
onActivated: function(editor) {
if (editor._engine) {
editor._engine.clear();
if (editor._undoManager) {
editor._undoManager.clear();
}
editor._parent = editor._engine;
}
}
});
// File -> Open Script...
this._menu.addOption('File', 'Open Script...', {
hotkeys: 'Ctrl+O',
icon: 'fa fa-folder-open-o fa-lg',
description: 'Open a script file...',
toolbarIndex: -1,
condition: function(editor) {
return !editor._options.readOnly;
},
onActivated: function(editor) {
if (editor._engine) {
if (document.createEvent) {
var evt = document.createEvent('MouseEvents');
evt.initEvent('click', true, false);
editor.$container.prepend(editor.$hiddenFileLoader);
editor.$hiddenFileLoader[0].dispatchEvent(evt);
}
}
}
});
// File -> Save Script
this._menu.addOption('File', 'Save Script', {
hotkeys: 'Ctrl+S',
icon: 'fa fa-save fa-lg',
description: 'Save this script...',
toolbarIndex: -1,
condition: function(editor) {
return !editor._options.readOnly;
},
onActivated: function(editor) {
if (editor._engine) {
if (!saveAs) {
editor.error('ERROR: Attempted to save the script when external dependency "FileSaver" was not included.');
return;
}
editor.triggerEvent('onBeforeSave', []);
var savedData = editor._engine.save();
var blob;
try {
blob = new Blob([savedData], {type: 'text/plain'});
} catch (e) {
// Legacy support
var bb = new BlobBuilder();
bb.append(savedData);
blob = bb.getBlob('text/plain');
}
saveAs(blob, 'script.wcplay');
editor._engine._undoManager.clearModified();
editor.triggerEvent('onSaved', []);
}
}
});
// File -> Import...
this._menu.addOption('File', 'Import...', {
icon: 'fa fa-link fa-lg',
description: 'Import a script file as a Composite Node.',
toolbarIndex: -1,
condition: function(editor) {
return !editor._options.readOnly;
},
onActivated: function(editor) {
if (editor._engine) {
if (document.createEvent) {
var evt = document.createEvent('MouseEvents');
evt.initEvent('click', true, false);
editor.$container.prepend(editor.$hiddenFileImporter);
editor.$hiddenFileImporter[0].dispatchEvent(evt);
}
}
}
});
// Edit -> Undo
this._menu.addOption('Edit', 'Undo', {
hotkeys: 'Ctrl+Z',
icon: 'fa fa-undo fa-lg',
toolbarIndex: -1,
description: function(editor) {
return 'Undo ' + ((editor._undoManager && editor._undoManager.undoInfo()) || 'Event');
},
condition: function(editor) {
return editor._undoManager && editor._undoManager.canUndo();
},
onActivated: function(editor) {
if (editor._undoManager) {
editor._undoManager.undo();
}
}
});
// Edit -> Redo
this._menu.addOption('Edit', 'Redo', {
hotkeys: 'Ctrl+Y,Ctrl+Shift+Z',
icon: 'fa fa-undo fa-flip-horizontal fa-lg',
toolbarIndex: -1,
description: function(editor) {
return 'Redo ' + ((editor._undoManager && editor._undoManager.redoInfo()) || 'Event');
},
condition: function(editor) {
return editor._undoManager && editor._undoManager.canRedo();
},
onActivated: function(editor) {
if (editor._undoManager) {
editor._undoManager.redo();
}
}
});
function __copy(editor) {
wcPlayEditorClipboard.nodes = [];
var bounds = [];
var offsets = [];
for (var i = 0; i < editor._selectedNodes.length; ++i) {
var node = editor._selectedNodes[i];
var data = node.export();
bounds.push(node._meta.bounds.farRect);
offsets.push(node.pos);
wcPlayEditorClipboard.nodes.push(data);
}
wcPlayEditorClipboard.bounds = editor.__expandRect(bounds, offsets);
}
// Edit -> Cut
this._menu.addOption('Edit', 'Cut', {
hotkeys: 'Ctrl+X',
icon: 'fa fa-cut fa-lg',
toolbarIndex: -1,
description: 'Cut selected node(s) out of your script and into the clipboard.',
condition: function(editor) {
return editor._selectedNodes.length > 0;
},
onActivated: function(editor) {
__copy(editor, editor._engine);
editor._undoManager && editor._undoManager.beginGroup('Cut Nodes to clipboard');
for (var i = 0; i < editor._selectedNodes.length; ++i) {
editor.__onDestroyNode(editor._selectedNodes[i]);
editor._selectedNodes[i].destroy();
}
editor._selectedNodes = [];
editor._undoManager && editor._undoManager.endGroup();
}
});
// Edit -> Copy
this._menu.addOption('Edit', 'Copy', {
hotkeys: 'Ctrl+C',
icon: 'fa fa-copy fa-lg',
toolbarIndex: -1,
description: 'Copy selected node(s) to your clipboard.',
condition: function(editor) {
return editor._selectedNodes.length > 0;
},
onActivated: __copy
});
// Edit -> Paste
this._menu.addOption('Edit', 'Paste', {
hotkeys: 'Ctrl+V',
icon: 'fa fa-paste fa-lg',
toolbarIndex: -1,
description: 'Paste node(s) in clipboard into your script.',
condition: function(editor) {
return wcPlayEditorClipboard.nodes.length > 0;
},
onActivated: function(editor) {
var i = 0;
var mouse = {
x: editor._mouse.x,
y: editor._mouse.y
};
if (!editor._mouseInViewport) {
mouse.x = editor.$viewport.width()/2;
mouse.y = editor.$viewport.height()/2;
}
editor._selectedNode = null;
editor._selectedNodes = [];
var idMap = [];
var nodes = [];
var data = null;
var newNode = null;
editor._undoManager && editor._undoManager.beginGroup('Paste Nodes from clipboard');
var bounds = wcPlayEditorClipboard.bounds;
for (i = 0; i < wcPlayEditorClipboard.nodes.length; ++i) {
data = wcPlayEditorClipboard.nodes[i];
newNode = new window.wcPlayNodes[data.className](editor._parent, data.pos);
idMap[data.id] = newNode.id;
nodes.push(newNode);
}
for (i = 0; i < wcPlayEditorClipboard.nodes.length; ++i) {
data = wcPlayEditorClipboard.nodes[i];
newNode = nodes[i];
editor._selectedNodes.push(newNode);
if (!editor._selectedNode) {
editor._selectedNode = newNode;
}
newNode.import(data, idMap);
newNode.pos.x = (mouse.x - editor._viewportCamera.x) / editor._viewportCamera.z - bounds.width/2 + (data.pos.x - bounds.left);
newNode.pos.y = (mouse.y - editor._viewportCamera.y) / editor._viewportCamera.z - bounds.height/2 + (data.pos.y - bounds.top);
editor.__onCreateNode(newNode);
}
editor._undoManager && editor._undoManager.endGroup();
}
});
// Edit -> Delete
this._menu.addOption('Edit', 'Delete', {
hotkeys: 'Delete',
icon: 'fa fa-trash fa-lg',
toolbarIndex: -1,
description: 'Delete selected node(s).',
condition: function(editor) {
return editor._selectedNodes.length > 0;
},
onActivated: function(editor) {
if (editor._selectedNodes.length) {
editor._undoManager && editor._undoManager.beginGroup('Removed Nodes');
for (var i = 0; i < editor._selectedNodes.length; ++i) {
editor.__onDestroyNode(editor._selectedNodes[i]);
editor._selectedNodes[i].destroy();
}
editor._selectedNode = null;
editor._selectedNodes = [];
editor._undoManager && editor._undoManager.endGroup();
}
}
});
// Edit -> Create Node
this._menu.addOption('Edit', 'Create Node', {
hotkeys: 'N',
icon: 'fa fa-plus fa-lg',
toolbarIndex: -1,
description: 'Create a new node.',
condition: function(editor) {
return !editor._options.readOnly;
},
onActivated: function(editor) {
if (editor._mouse) {
editor.__drawPalettePopup(editor._mouse);
}
}
});
// Edit -> Create Composite
this._menu.addOption('Edit', 'Create Composite', {
hotkeys: 'C',
icon: 'fa fa-object-group fa-lg',
toolbarIndex: -1,
description: 'Combine all selected nodes into a new "Composite" Node.',
condition: function(editor) {
return !editor._options.readOnly && editor._selectedNodes.length > 0;
},
onActivated: function(editor) {
editor.__createComposite();
}
});
// Debugging -> Toggle Debug Mode
this._menu.addOption('Debugging', 'Toggle Debug Mode', {
icon: function(editor) {
if (editor._engine && editor._engine.debugging()) {
return 'fa fa-dot-circle-o fa-lg';
} else {
return 'fa fa-circle-o fa-lg';
}
},
toolbarIndex: -1,
description: 'Toggle debugging mode (does not break on breakpoints).',
onActivated: function(editor) {
if (editor._engine) {
editor._engine.debugging(!editor._engine.debugging());
editor._engine.paused(false);
}
}
});
// Debugging -> Toggle Silence Mode
this._menu.addOption('Debugging', 'Toggle Silence Mode', {
icon: function(editor) {
if (editor._engine && editor._engine.silent()) {
return 'fa fa-volume-off fa-lg';
} else {
return 'fa fa-volume-up fa-lg';
}
},
toolbarIndex: -1,
description: 'Toggle silent mode (disables console log messages).',
onActivated: function(editor) {
if (editor._engine) {
editor._engine.silent(!editor._engine.silent());
}
}
});
// Debugging -> Restart Script
this._menu.addOption('Debugging', 'Start Script', {
hotkeys: 'Shift+Enter',
icon: function(editor) {
if (editor._engine && editor._engine.isRunning()) {
return 'fa fa-stop fa-lg';
} else {
return 'fa fa-play fa-lg';
}
},
toolbarIndex: -1,
display: function(editor) {
if (editor._engine && editor._engine.isRunning()) {
return 'Stop Script';
} else {
return 'Start Script';
}
},
description: 'Starts or Stops execution of the script.',
condition: function(editor) {
return !editor._options.readOnly && editor._options.playable;
},
onActivated: function(editor) {
if (editor._engine) {
if (editor._engine.isRunning()) {
editor._engine.stop();
} else {
editor._engine.start();
}
}
}
});
// Debugging -> Pause/Continue Script
this._menu.addOption('Debugging', 'Pause Script', {
hotkeys: 'Return',
icon: 'fa fa-pause fa-lg',
toggle: function(editor) {
return editor._engine && editor._engine.paused();
},
display: function(editor) {
if (editor._engine && editor._engine.paused()) {
return 'Resume Script';
} else {
return 'Pause Script';
}
},
toolbarIndex: -1,
description: 'Pause or Continue the script.',
condition: function(editor) {
return !editor._options.readOnly && (editor._engine && editor._engine.isRunning());
},
onActivated: function(editor) {
if (editor._engine) {
if (editor._engine.paused() || editor._engine.stepping()) {
editor._engine.paused(false);
editor._engine.stepping(false);
} else {
editor._engine.stepping(true);
}
}
}
});
// Debugging -> Step Script
this._menu.addOption('Debugging', 'Step Script', {
hotkeys: 'Spacebar',
icon: 'fa fa-fast-forward fa-lg',
toolbarIndex: -1,
description: 'Perform a single script update.',
condition: function(editor) {
return !editor._options.readOnly && (editor._engine && editor._engine.isRunning());
},
onActivated: function(editor) {
if (editor._engine) {
editor._engine.paused(false);
editor._engine.stepping(true);
}
}
});
// View -> Create Composite
this._menu.addOption('View', 'Fit in View', {
hotkeys: 'F',
icon: 'fa fa-crosshairs fa-lg',
categoryIndex: 2,
toolbarIndex: -1,
description: 'Center view on selected node(s).',
onActivated: function(editor) {
if (editor._selectedNodes.length) {
editor.focus(editor._selectedNodes);
} else {
editor.center();
}
}
});
// View -> Exit Composite
this._menu.addOption('View', 'Exit Composite', {
hotkeys: 'O',
icon: 'fa fa-level-up fa-lg',
toolbarIndex: -1,
description: 'Step out of this Composite Node.',
condition: function(editor) {
return editor._parent && editor._parent.instanceOf('wcNodeCompositeScript');
},
onActivated: function(editor) {
var focusNode = editor._parent;
editor._parent = editor._parent._parent;
editor._selectedNode = focusNode;
editor._selectedNodes = [focusNode];
editor.focus(editor._selectedNodes);
}
});
// View -> Enter Composite
this._menu.addOption('View', 'Enter Composite', {
hotkeys: 'I',
icon: 'fa fa-level-down fa-lg',
toolbarIndex: -1,
description: 'Step in to this Composite Node.',
condition: function(editor) {
return (editor._selectedNodes.length === 1 && editor._selectedNodes[0].instanceOf('wcNodeCompositeScript'));
},
onActivated: function(editor) {
editor._parent = editor._selectedNodes[0];
editor._selectedNode = null;
editor._selectedNodes = [];
editor.center();
}
});
// View -> Chain Style
this._menu.addOption('View', 'Chain Style', {
hotkeys: 'V',
icon: 'fa fa-sitemap fa-lg',
toolbarIndex: -1,
description: 'Toggle the visual style of the chains.',
toggle: function(editor) {
return editor._chainStyle === 0;
},
onActivated: function(editor) {
editor._chainStyle += 1;
if (editor._chainStyle > editor._chainStyleMax) {
editor._chainStyle = 0;
}
}
});
var self = this;
var $field = this.$search.children('input');
var searchResults = [];
var searchParent = null;
var searchValue = '';
var searchIndex = 0;
function __searchNodes() {
// Search nodes and focus on them.
if (searchParent) {
searchResults = searchParent.nodesBySearch(searchValue);
if (searchResults.length > 0) {
self._parent = searchResults[searchIndex]._parent;
self.focus([searchResults[searchIndex]]);
self._selectedNodes = searchResults;
}
}
}
function __searchPrev() {
if (searchResults.length) {
searchIndex -= 1;
if (searchIndex < 0) {
searchIndex = searchResults.length-1;
}
self._parent = searchResults[searchIndex]._parent;
self.focus([searchResults[searchIndex]]);
self._selectedNodes = searchResults;
}
}
function __searchNext() {
if (searchResults.length) {
searchIndex += 1;
if (searchIndex >= searchResults.length) {
searchIndex = 0;
}
self._parent = searchResults[searchIndex]._parent;
self.focus([searchResults[searchIndex]]);
self._selectedNodes = searchResults;
}
}
this.$search.children('.wcPlayEditorSearchPrev').click(__searchPrev);
this.$search.children('.wcPlayEditorSearchNext').click(__searchNext);
this.$search.keydown(function(event) {
if (event.keyCode === 27) {
if (!self.$search.hasClass('wcPlayHidden')) {
self.$search.addClass('wcPlayHidden');
}
} else if (event.keyCode === 13 || event.keyCode === 40 || event.keyCode === 9) {
// Return, Down arrow, or Tab to cycle next item.
__searchNext();
event.stopPropagation();
event.preventDefault();
return true;
} else if (event.keyCode === 38) {
// Up arrow to cycle previous item.
__searchPrev();
event.stopPropagation();
event.preventDefault();
return true;
}
});
this.$search.keyup(function() {
// Re-perform the search when the search value has changed.
var val = $field.val().toLowerCase();
if (searchValue !== val) {
searchIndex = 0;
searchParent = self._parent;
searchValue = val;
__searchNodes();
}
});
// View -> Search...
this._menu.addOption('View', 'Search...', {
hotkeys: 'Ctrl+F',
icon: 'fa fa-search fa-lg',
toolbarIndex: -1,
description: 'Toggle the visual style of the chains.',
onActivated: function(editor) {
editor.$search.removeClass('wcPlayHidden');
$field.focus();
$field.select();
if (searchResults.length) {
__searchNodes();
}
}
});
// Help -> Documentation...
this._menu.addOption('Help', 'Documentation...', {
icon: 'fa fa-file-pdf-o fa-lg',
description: 'Open the documentation for wcPlay in another window.',
onActivated: function(editor) {
window.open('http://play.api.webcabin.org/', '_blank');
}
});
this._menu.addSpacer('File', 'Save Script');
this._menu.addSpacer('File', 'Import...');
this._menu.addSpacer('Edit', 'Redo');
this._menu.addSpacer('Edit', 'Delete');
this._menu.addSpacer('View', 'Enter Composite');
this._menu.addSpacer('View', 'Chain Style');
this._menu.addSpacer('Debugging', 'Toggle Silence Mode');
this._menu.addToolbarSpacer('File', 'Save Script');
this._menu.addToolbarSpacer('File', 'Import...');
this._menu.addToolbarSpacer('Edit', 'Redo');
this._menu.addToolbarSpacer('Edit', 'Delete');
this._menu.addToolbarSpacer('Edit', 'Create Composite');
this._menu.addToolbarSpacer('View', 'Enter Composite');
this._menu.addToolbarSpacer('View', 'Chain Style');
this._menu.addToolbarSpacer('Debugging', 'Toggle Silence Mode');
this._menu.addToolbarSpacer('Debugging', 'Step Script');
},
/**
* Initializes the palette view.
* @function wcPlayEditor#__setupPalette
* @private
*/
__setupPalette: function() {
if (!this._engine) {
return;
}
var nodeLibrary = this._engine.nodeLibrary().get();
// Compile our node listing.
this._nodeLibrary = [];
for (var i = 0; i < nodeLibrary.length; ++i) {
var data = nodeLibrary[i];
// Skip categories we are not showing.
if (data.className === 'wcNodeCompositeScript') {
continue;
} else {
var catIndex = this._options.category.items.indexOf(data.category);
if ((!this._options.category.isBlacklist && catIndex === -1) ||
(this._options.category.isBlacklist && catIndex > -1)) {
continue;
}
}
// Now create an instance of the node so we can extract further data from it.
var node = new window.wcPlayNodes[data.className](null);
data.entry = node.chain.entry.map(function(link) {
return {name: link.name, desc: ''};
});
data.exit = node.chain.exit.map(function(link) {
return {name: link.name, desc: ''};
});
data.input = node.properties.filter(function(prop) {
return prop.options.input;
}).map(function(prop) {
return {name: prop.name, desc: prop.options.description};
});
data.output = node.properties.filter(function(prop) {
return prop.options.output;
}).map(function(prop) {
return {name: prop.name, desc: prop.options.description};
});
data.desc = node._meta.description;
data.node = node;
data.id = this._nodeLibrary.length;
this._nodeLibrary.push(data);
// We need to update the node to get it's proper measurements.
this.__updateNode(node, 0, this._viewportContext);
}
// this._paletteSize = this._options.readOnly? 0: this._drawStyle.palette.width;
// this.onResized();
// if (this.$typeButton.length == 0) {
// // Create our top bar with buttons for each node type.
// this.$typeButton.push($('<button class="wcPlayEditorButton wcToggled" title="Show Entry Nodes.">Entry</button>'));
// this.$typeButton.push($('<button class="wcPlayEditorButton" title="Show Process Nodes.">Process</button>'));
// this.$typeButton.push($('<button class="wcPlayEditorButton" title="Show Storage Nodes.">Storage</button>'));
// this.$typeButton.push($('<button class="wcPlayEditorButton" title="Show Composite Nodes.">Composite</button>'));
// this.$palette.append(this.$typeButton[0]);
// this.$palette.append(this.$typeButton[1]);
// this.$palette.append(this.$typeButton[2]);
// this.$palette.append(this.$typeButton[3]);
// this.$typeArea.push($('<div class="wcPlayTypeArea">'));
// this.$typeArea.push($('<div class="wcPlayTypeArea wcPlayHidden">'));
// this.$typeArea.push($('<div class="wcPlayTypeArea wcPlayHidden">'));
// this.$typeArea.push($('<div class="wcPlayTypeArea wcPlayHidden">'));
// this.$paletteInner.append(this.$typeArea[0]);
// this.$paletteInner.append(this.$typeArea[1]);
// this.$paletteInner.append(this.$typeArea[2]);
// this.$paletteInner.append(this.$typeArea[3]);
// }
// // Empty out our current node library.
// if (this._nodeLibrary) {
// for (var cat in this._nodeLibrary) {
// for (var type in this._nodeLibrary[cat]) {
// var typeData = this._nodeLibrary[cat][type];
// typeData.$button.remove();
// typeData.$canvas.remove();
// typeData.$category.remove();
// for (var i = 0; i < typeData.nodes.length; ++i) {
// typeData.nodes[i].destroy();
// }
// }
// }
// this._nodeLibrary = {};
// }
// function __organize(data) {
// // Initialize the node category if it is new.
// if (!this._nodeLibrary.hasOwnProperty(data.category)) {
// this._nodeLibrary[data.category] = {};
// }
// // Further categorize the node by its type.
// if (!this._nodeLibrary[data.category].hasOwnProperty(data.nodeType)) {
// var typeData = {
// $category: $('<div class="wcPlayTypeCategory">'),
// $button: $('<button class="wcPlayCategoryButton" title="Toggle visibility of this category.">' + data.category + '</button>'),
// $canvas: $('<canvas class="wcPlayTypeCategoryArea">'),
// context: null,
// nodes: [],
// };
// typeData.context = typeData.$canvas[0].getContext('2d');
// typeData.$category.append(typeData.$button);
// typeData.$category.append(typeData.$canvas);
// this.$typeArea[this.__typeIndex(data.nodeType)].append(typeData.$category);
// (function __setupCollapseHandler(d) {
// d.$button.click(function() {
// if (!d.$button.hasClass('wcToggled')) {
// d.$button.addClass('wcToggled');
// d.$canvas.addClass('wcPlayHidden');
// } else {
// d.$button.removeClass('wcToggled');
// d.$canvas.removeClass('wcPlayHidden');
// }
// });
// })(typeData);
// this._nodeLibrary[data.category][data.nodeType] = typeData;
// }
// }
// // Initialize our node library.
// for (var i = 0; i < wcPlay.NODE_LIBRARY.length; ++i) {
// var data = wcPlay.NODE_LIBRARY[i];
// // Skip categories we are not showing.
// if (data.className !== 'wcNodeCompositeScript') {
// var catIndex = this._options.category.items.indexOf(data.category);
// if ((!this._options.category.isBlacklist && catIndex === -1) ||
// (this._options.category.isBlacklist && catIndex > -1)) {
// continue;
// }
// } else {
// continue;
// }
// __organize.call(this, data);
// // Now create an instance of the node.
// var node = new window.wcPlayNodes[data.className](null);
// this._nodeLibrary[data.category][data.nodeType].nodes.push(node);
// }
// // Load our imported composite nodes as well.
// var composites = this._engine.importedComposites();
// for (var i = 0; i < composites.length; ++i) {
// var node = composites[i];
// __organize.call(this, node);
// this._nodeLibrary[node.category][node.nodeType].nodes.push(node);
// }
// // Now draw each of our palette nodes once so we can configure the size of the canvases.
// for (var cat in this._nodeLibrary) {
// for (var type in this._nodeLibrary[cat]) {
// var typeData = this._nodeLibrary[cat][type];
// typeData.$canvas.attr('width', this.$paletteInner.width());
// var yPos = this._drawStyle.palette.spacing;
// var xPos = this.$paletteInner.width() / 2;
// for (var i = 0; i < typeData.nodes.length; ++i) {
// this.__updateNode(typeData.nodes[i], 0, typeData.context);
// typeData.nodes[i].pos.x = xPos;
// typeData.nodes[i].pos.y = yPos;
// this.__drawNode(typeData.nodes[i], typeData.context);
// yPos += (typeData.nodes[i]._meta.bounds.rect.height * this._drawStyle.palette.scale) + this._drawStyle.palette.spacing;
// }
// typeData.$canvas.attr('height', yPos);
// }
// }
// var self = this;
// this.$typeButton[0].click(function() {
// self.$typeButton[0].addClass('wcToggled');
// self.$typeButton[1].removeClass('wcToggled');
// self.$typeButton[2].removeClass('wcToggled');
// self.$typeButton[3].removeClass('wcToggled');
// self.$typeArea[0].removeClass('wcPlayHidden');
// self.$typeArea[1].addClass('wcPlayHidden');
// self.$typeArea[2].addClass('wcPlayHidden');
// self.$typeArea[3].addClass('wcPlayHidden');
// });
// this.$typeButton[1].click(function() {
// self.$typeButton[0].removeClass('wcToggled');
// self.$typeButton[1].addClass('wcToggled');
// self.$typeButton[2].removeClass('wcToggled');
// self.$typeButton[3].removeClass('wcToggled');
// self.$typeArea[0].addClass('wcPlayHidden');
// self.$typeArea[1].removeClass('wcPlayHidden');
// self.$typeArea[2].addClass('wcPlayHidden');
// self.$typeArea[3].addClass('wcPlayHidden');
// });
// this.$typeButton[2].click(function() {
// self.$typeButton[0].removeClass('wcToggled');
// self.$typeButton[1].removeClass('wcToggled');
// self.$typeButton[2].addClass('wcToggled');
// self.$typeButton[3].removeClass('wcToggled');
// self.$typeArea[0].addClass('wcPlayHidden');
// self.$typeArea[1].addClass('wcPlayHidden');
// self.$typeArea[2].removeClass('wcPlayHidden');
// self.$typeArea[3].addClass('wcPlayHidden');
// });
// this.$typeButton[3].click(function() {
// self.$typeButton[0].removeClass('wcToggled');
// self.$typeButton[1].removeClass('wcToggled');
// self.$typeButton[2].removeClass('wcToggled');
// self.$typeButton[3].addClass('wcToggled');
// self.$typeArea[0].addClass('wcPlayHidden');
// self.$typeArea[1].addClass('wcPlayHidden');
// self.$typeArea[2].addClass('wcPlayHidden');
// self.$typeArea[3].removeClass('wcPlayHidden');
// });
},
// /**
// * Draws each node in the palette view.
// * @function wcPlayEditor#__drawPalette
// * @private
// * @param {number} elapsed - Elapsed time since last update.
// */
// __drawPalette: function(elapsed) {
// for (var cat in this._nodeLibrary) {
// for (var type in this._nodeLibrary[cat]) {
// // Ignore types that are not visible.
// if (!this.$typeButton[this.__typeIndex(type)].hasClass('wcToggled')) continue;
// var typeData = this._nodeLibrary[cat][type];
// // Ignore categories that are not visible.
// if (typeData.$button.hasClass('wcToggled')) continue;
// var yPos = this._drawStyle.palette.spacing;
// var xPos = this.$paletteInner.width() / 2 / this._drawStyle.palette.scale;
// typeData.$canvas.attr('width', this.$paletteInner.width());
// typeData.context.clearRect(0, 0, typeData.$canvas.width(), typeData.$canvas.height());
// typeData.context.save();
// typeData.context.scale(this._drawStyle.palette.scale, this._drawStyle.palette.scale);
// for (var i = 0; i < typeData.nodes.length; ++i) {
// this.__updateNode(typeData.nodes[i], 0, typeData.context);
// typeData.nodes[i].pos.x = xPos;
// typeData.nodes[i].pos.y = yPos;
// this.__drawNode(typeData.nodes[i], typeData.context, true);
// yPos += typeData.nodes[i]._meta.bounds.rect.height + this._drawStyle.palette.spacing;
// }
// typeData.context.restore();
// }
// }
// },
/**
* Draws a list of nodes on the canvas.
* @function wcPlayEditor#__drawNodes
* @private
* @param {wcNode[]} nodes - The node to render.
* @param {external:Canvas~Context} context - The canvas context to render on.
*/
__drawNodes: function(nodes, context) {
for (var i = 0; i < nodes.length; ++i) {
this.__drawNode(nodes[i], context);
}
},
/**
* Draws a single node on the canvas at a given position.
* @function wcPlayEditor#__drawNode
* @private
* @param {wcNode} node - The node to render.
* @param {external:Canvas~Context} context - The canvas context to render on.
* @param {boolean} [isPalette] - If true, this node will be rendered for the palette view.
*/
__drawNode: function(node, context, isPalette) {
// Ignore drawing if the node is outside of view.
if (!isPalette && !this.__rectOnRect(node._meta.bounds.farRect, this._viewportBounds, node.pos)) {
node._meta.visible = false;
return;
}
node._meta.visible = true;
// this._nodeDrawCount += 1;
// Show an additional bounding rect around selected nodes.
if (this._selectedNodes.indexOf(node) > -1) {
this.__drawRoundedRect(node._meta.bounds.rect, 'rgba(0, 255, 255, 0.25)', -1, 10, context, node.pos);
this.__drawRoundedRect(node._meta.bounds.rect, 'darkcyan', 2, 10, context, node.pos);
}
// Now use our measurements to draw our node.
this.__drawCenter(node, context, isPalette);
this.__drawEntryLinks(node, context, node._meta.bounds.entryOuter.width);
this.__drawExitLinks(node, context, node._meta.bounds.entryOuter.height + node._meta.bounds.centerOuter.height, node._meta.bounds.exitOuter.width);
// Add a collapse button to the node in the left margin of the title.
context.save();
context.fillStyle = (this._highlightDebugLog && this._highlightNode === node? 'black': 'white');
context.strokeStyle = 'black';
context.lineWidth = 1;
context.fillRect(node.pos.x + node._meta.bounds.debugLog.left, node.pos.y + node._meta.bounds.debugLog.top, node._meta.bounds.debugLog.width, node._meta.bounds.debugLog.height);
context.strokeRect(node.pos.x + node._meta.bounds.debugLog.left, node.pos.y + node._meta.bounds.debugLog.top, node._meta.bounds.debugLog.width, node._meta.bounds.debugLog.height);
if (node._log) {
context.strokeStyle = 'red';
} else {
context.strokeStyle = (this._highlightDebugLog && this._highlightNode === node)? 'white': 'black';
}
context.lineWidth = 2;
context.beginPath();
context.moveTo(node.pos.x + node._meta.bounds.debugLog.left + 1, node.pos.y + node._meta.bounds.debugLog.top + 1);
context.lineTo(node.pos.x + node._meta.bounds.debugLog.left + node._meta.bounds.debugLog.width/2, node.pos.y + node._meta.bounds.debugLog.top + node._meta.bounds.debugLog.height/2);
context.lineTo(node.pos.x + node._meta.bounds.debugLog.left + 1, node.pos.y + node._meta.bounds.debugLog.top + node._meta.bounds.debugLog.height - 1);
context.moveTo(node.pos.x + node._meta.bounds.debugLog.left + node._meta.bounds.debugLog.width/2, node.pos.y + node._meta.bounds.debugLog.top + node._meta.bounds.debugLog.height - 2);
context.lineTo(node.pos.x + node._meta.bounds.debugLog.left + node._meta.bounds.debugLog.width, node.pos.y + node._meta.bounds.debugLog.top + node._meta.bounds.debugLog.height - 2);
context.stroke();
context.restore();
// Add breakpoint button to the node in the right margin of the title.
context.save();
context.fillStyle = (this._highlightBreakpoint && this._highlightNode === node? 'black': 'white');
context.fillRect(node.pos.x + node._meta.bounds.breakpoint.left, node.pos.y + node._meta.bounds.breakpoint.top, node._meta.bounds.breakpoint.width, node._meta.bounds.breakpoint.height);
if (node._break) {
context.strokeStyle = 'red';
} else {
context.strokeStyle = (this._highlightBreakpoint && this._highlightNode === node)? 'white': 'black';
}
context.fillStyle = 'red';
context.lineWidth = 2;
context.beginPath();
context.arc(
node.pos.x + node._meta.bounds.breakpoint.left + node._meta.bounds.breakpoint.width/2,
node.pos.y + node._meta.bounds.breakpoint.top + node._meta.bounds.breakpoint.height/2,
Math.min(node._meta.bounds.breakpoint.width/2-2,
node._meta.bounds.breakpoint.height/2-2),
0,
2 * Math.PI
);
node._break && context.fill();
context.stroke();
context.strokeStyle = 'black';
context.lineWidth = 1;
context.strokeRect(node.pos.x + node._meta.bounds.breakpoint.left, node.pos.y + node._meta.bounds.breakpoint.top, node._meta.bounds.breakpoint.width, node._meta.bounds.breakpoint.height);
context.restore();
// Increase the nodes border thickness when flashing.
if (node.isBroken()) {
this.__drawRoundedRect(node._meta.bounds.inner, '#CC0000', 5, this._drawStyle.node.radius, context, node.pos);
} else if (node._meta.flashDelta) {
this.__drawRoundedRect(node._meta.bounds.inner, 'yellow', 2, this._drawStyle.node.radius, context, node.pos);
}
},
/**
* Measures the space to render entry links for a node.
* @function wcPlayEditor#__measureEntryLinkOuter
* @private
* @param {wcNode} node - The node to measure.
* @param {external:Canvas~Context} context - The canvas context.
* @returns {wcPlayEditor~Rect} - A bounding rectangle.
*/
__measureEntryLinkOuter: function(node, context) {
var bounds = {
top: 0,
left: 0,
width: 0,
height: 0
};
this.__setCanvasFont(this._font.links, context);
var links = node.chain.entry;
for (var i = 0; i < links.length; ++i) {
bounds.width += context.measureText(links[i].name).width + this._drawStyle.links.spacing;
}
bounds.left -= bounds.width/2 + this._drawStyle.links.margin;
bounds.width += this._drawStyle.links.margin * 2;
if (node.chain.entry.length) {
bounds.height = this._font.links.size + this._drawStyle.links.padding + this._drawStyle.links.length;
}
return bounds;
},
/**
* Measures the space to render exit links for a node.
* @function wcPlayEditor#__measureExitLinkOuter
* @private
* @param {wcNode} node - The node to measure.
* @param {external:Canvas~Context} context - The canvas context.
* @param {number} offset - The top position to measure the links.
* @returns {wcPlayEditor~Rect} - A bounding rectangle.
*/
__measureExitLinkOuter: function(node, context, offset) {
var bounds = {
top: offset,
left: 0,
width: 0,
height: 0
};
this.__setCanvasFont(this._font.links, context);
var links = node.chain.exit;
for (var i = 0; i < links.length; ++i) {
bounds.width += context.measureText(links[i].name).width + this._drawStyle.links.spacing;
}
bounds.left -= bounds.width/2 + this._drawStyle.links.margin;
bounds.width += this._drawStyle.links.margin * 2;
if (node.chain.exit.length) {
bounds.height = this._font.links.size + this._drawStyle.links.padding + this._drawStyle.links.length;
}
return bounds;
},
/**
* Measures the space to render the center area for a node.
* @function wcPlayEditor#__measureOuter
* @private
* @param {wcNode} node - The node to measure.
* @param {external:Canvas~Context} context - The canvas context.
* @param {number} offset - The upper offset.
* @returns {wcPlayEditor~Rect} - A bounding rectangle. The height is only the amount of space rendered within the node bounds (links stick out).
*/
__measureOuter: function(node, context, offset) {
var bounds = {
top: offset,
left: 0,
width: 0,
height: this._font.title.size + this._drawStyle.title.spacing + this._drawStyle.links.padding
};
// Measure the title bar area.
this.__setCanvasFont(this._font.title, context);
bounds.width = context.measureText(this._drawStyle.title.wrapL + node.type + ': ' + this._drawStyle.title.wrapR).width;
this.__setCanvasFont(this._font.titleDesc, context);
bounds.width += context.measureText(this._drawStyle.title.nameWrapL + (node.name || this._drawStyle.title.placeholder) + this._drawStyle.title.nameWrapR).width;
if (node.description() || node.details()) {
this.__setCanvasFont(this._font.details, context);
bounds.width += context.measureText(this._drawStyle.title.details).width;
}
// Measure the node's viewport.
if (node._viewportSize) {
bounds.width = Math.max(bounds.width, node._viewportSize.x);
bounds.height += node._viewportSize.y + this._drawStyle.property.spacing;
}
// Measure the property headers.
bounds.height += this._font.property.size + this._drawStyle.property.spacing;
// Measure properties.
var propWidth = 0;
var valueWidth = 0;
var initialWidth = 0;
var joinedWidth = 0;
var props = node.properties;
for (var i = 0; i < props.length; ++i) {
bounds.height += this._font.property.size + this._drawStyle.property.spacing;
// Property name.
this.__setCanvasFont(this._font.property, context);
propWidth = Math.max(context.measureText(props[i].name + ': ').width, propWidth);
var showValue = this._engine.isRunning() && !props[i].options.linked;
this.__setCanvasFont(this._font.value, context);
var measuredValue = context.measureText(this._drawStyle.property.valueWrapL + this.__drawPropertyValue(node, props[i]) + this._drawStyle.property.valueWrapR).width;
this.__setCanvasFont(this._font.initialValue, context);
var measuredInitial = context.measureText(this._drawStyle.property.initialWrapL + this.__drawPropertyValue(node, props[i], true) + this._drawStyle.property.initialWrapR).width;
// Property value.
if (showValue) {
valueWidth = Math.max(measuredValue, this._drawStyle.property.minLength, valueWidth);
initialWidth = Math.max(measuredInitial, this._drawStyle.property.minLength, initialWidth);
} else {
joinedWidth = Math.max(measuredValue + measuredInitial, this._drawStyle.property.minLength, joinedWidth);
}
}
joinedWidth = Math.max(joinedWidth, valueWidth + initialWidth);
bounds.width = Math.max(propWidth + joinedWidth, bounds.width) + this._drawStyle.node.margin * 2;
bounds.left -= bounds.width/2;
bounds.propWidth = propWidth;
bounds.valueWidth = valueWidth;
bounds.initialWidth = initialWidth;
return bounds;
},
/**
* Measures the space to render the center area for a node.
* @function wcPlayEditor#__measureCenter
* @private
* @param {wcNode} node - The node to draw.
* @param {external:Canvas~Context} context - The canvas context.
* @param {wcPlayEditor~Rect} rect - The bounding area to draw in.
*/
__measureCenter: function(node, context, rect) {
var upper = node.chain.entry.length? this._font.links.size + this._drawStyle.links.padding: 0;
// var lower = node.chain.exit.length? this._font.links.size + this._drawStyle.links.padding: 0;
node._meta.bounds.center = rect;
node._meta.bounds.inputBounds = [];
node._meta.bounds.outputBounds = [];
node._meta.bounds.propertyBounds = [];
node._meta.bounds.valueBounds = [];
node._meta.bounds.initialBounds = [];
context.save();
// Measure the title bar area.
this.__setCanvasFont(this._font.title, context);
var titleTypeWidth = context.measureText(this._drawStyle.title.wrapL + node.type + ': ').width;
var titleWrapRWidth = context.measureText(this._drawStyle.title.wrapR).width;
this.__setCanvasFont(this._font.titleDesc, context);
var titleTextWidth = context.measureText(this._drawStyle.title.nameWrapL + (node.name || this._drawStyle.title.placeholder) + this._drawStyle.title.nameWrapR).width;
var titleDetailsWidth = 0;
if (node.description() || node.details()) {
this.__setCanvasFont(this._font.details, context);
titleDetailsWidth = context.measureText(this._drawStyle.title.details).width;
}
node._meta.bounds.titleBounds = {
top: rect.top,
left: rect.left + titleTypeWidth + (rect.width - (titleTypeWidth + titleWrapRWidth + titleTextWidth + titleDetailsWidth))/2,
width: titleTextWidth,
height: this._font.title.size + this._drawStyle.title.spacing - 1,
typeWidth: titleTypeWidth,
wrapRWidth: titleWrapRWidth,
textWidth: titleTextWidth
};
node._meta.bounds.detailsBounds = {
top: rect.top,
left: rect.left + rect.width - this._drawStyle.node.margin - titleDetailsWidth,
width: titleDetailsWidth,
height: this._font.details.size + this._drawStyle.title.spacing - 1
};
// Title Lower Bar
upper = this._font.title.size;
upper += this._drawStyle.title.spacing;
// Draw the node's viewport.
if (node._viewportSize) {
// Calculate the translation to make the viewport 0,0.
node._meta.bounds.viewportBounds = {
top: rect.top + upper,
left: rect.left + (rect.width/2 - node._viewportSize.x/2),
width: node._viewportSize.x,
height: node._viewportSize.y
};
upper += node._viewportSize.y + this._drawStyle.property.spacing;
}
// Measure the property headers.
upper += this._font.property.size + this._drawStyle.property.spacing;
var linkRect = null;
var longRect = null;
var props = node.properties;
for (var i = 0; i < props.length; ++i) {
upper += this._font.property.size;
// Property name.
var propertyBound = {
rect: {
top: rect.top + upper - this._font.property.size,
left: rect.left + this._drawStyle.node.margin,
width: rect.width - this._drawStyle.node.margin * 2,
height: this._font.property.size + this._drawStyle.property.spacing
},
name: props[i].name
};
node._meta.bounds.propertyBounds.push(propertyBound);
var showValue = this._engine.isRunning() && !props[i].options.linked;
// Initial property value.
var initialBound = {
rect: {
top: rect.top + upper - this._font.property.size,
left: rect.left + rect.width - this._drawStyle.node.margin - rect.initialWidth - (showValue? 0: rect.valueWidth),
width: rect.initialWidth + (showValue? 0: rect.valueWidth),
height: this._font.property.size + this._drawStyle.property.spacing
},
name: props[i].name
};
node._meta.bounds.initialBounds.push(initialBound);
// Property value.
var valueBound = {
rect: {
top: rect.top + upper - this._font.property.size,
left: rect.left + rect.width - this._drawStyle.node.margin - rect.valueWidth - rect.initialWidth,
width: (showValue? rect.valueWidth: 0),
height: this._font.property.size + this._drawStyle.property.spacing
},
name: props[i].name
};
node._meta.bounds.valueBounds.push(valueBound);
// Property input.
linkRect = {
top: rect.top + upper - this._font.property.size/3 - this._drawStyle.links.width/2 - 5,
left: rect.left - this._drawStyle.links.length,
width: this._drawStyle.links.length,
height: (props[i].options && props[i].options.input)? this._drawStyle.links.width + 10: 0
};
longRect = {
top: rect.top + upper - this._font.property.size/3 - this._drawStyle.links.width/2 - 5,
left: rect.left - this._drawStyle.links.length,
width: rect.width + this._drawStyle.links.length,
height: (props[i].options && props[i].options.input)? this._drawStyle.links.width + 10: 0
};
node._meta.bounds.inputBounds.push({
rect: linkRect,
longRect: longRect,
point: {
x: linkRect.left + linkRect.width/3 - 2,
y: linkRect.top + linkRect.height/2
},
name: props[i].name
});
// Property output.
linkRect = {
top: rect.top + upper - this._font.property.size/3 - this._drawStyle.links.width/2 - 5,
left: rect.left + rect.width,
width: this._drawStyle.links.length,
height: (props[i].options && props[i].options.output)? this._drawStyle.links.width + 10: 0
};
longRect = {
top: rect.top + upper - this._font.property.size/3 - this._drawStyle.links.width/2 - 5,
left: rect.left,
width: rect.width + this._drawStyle.links.length,
height: (props[i].options && props[i].options.output)? this._drawStyle.links.width + 10: 0
};
node._meta.bounds.outputBounds.push({
rect: linkRect,
longRect: longRect,
point: {
x: linkRect.left + linkRect.width + 1,
y: linkRect.top + linkRect.height/2
},
name: props[i].name
});
upper += this._drawStyle.property.spacing;
}
context.restore();
},
/**
* Draws the entry links of a node.
* @function wcPlayEditor#__measureEntryLinks
* @private
* @param {wcNode} node - The node to draw.
* @param {external:Canvas~Context} context - The canvas context.
* @param {number} width - The width of the area to draw in.
*/
__measureEntryLinks: function(node, context, width) {
node._meta.bounds.entryBounds = [];
var xPos = -width/2 + this._drawStyle.links.margin;
var yPos = this._drawStyle.links.length + this._font.links.size;
context.save();
this.__setCanvasFont(this._font.links, context);
var links = node.chain.entry;
for (var i = 0; i < links.length; ++i) {
// Link label
var w = context.measureText(links[i].name).width + this._drawStyle.links.spacing;
// Link connector
var rect = {
top: yPos - this._drawStyle.links.length - this._font.links.size,
left: xPos + w/2 - this._drawStyle.links.width/2,
width: this._drawStyle.links.width,
height: this._drawStyle.links.length
};
// Expand the bounding rect just a little so it is easier to click.
rect.left -= 5;
rect.width += 10;
var longRect = {
top: rect.top,
left: rect.left,
width: rect.width,
height: node._meta.bounds.center.height
};
node._meta.bounds.entryBounds.push({
rect: rect,
longRect: longRect,
point: {
x: rect.left + rect.width/2,
y: rect.top + rect.height/3 - 2
},
name: links[i].name
});
xPos += w;
}
context.restore();
},
/**
* Draws the exit links of a node.
* @function wcPlayEditor#__measureExitLinks
* @private
* @param {wcNode} node - The node to draw.
* @param {external:Canvas~Context} context - The canvas context.
* @param {number} offset - The upper offset.
* @param {number} width - The width of the area to draw in.
*/
__measureExitLinks: function(node, context, offset, width) {
node._meta.bounds.exitBounds = [];
var xPos = -width/2 + this._drawStyle.links.margin;
var yPos = offset + this._font.links.size;
context.save();
this.__setCanvasFont(this._font.links, context);
var links = node.chain.exit;
for (var i = 0; i < links.length; ++i) {
// Link label
var w = context.measureText(links[i].name).width + this._drawStyle.links.spacing;
// Link connector
var rect = {
top: yPos + this._drawStyle.links.padding,
left: xPos + w/2 - this._drawStyle.links.width/2,
width: this._drawStyle.links.width,
height: this._drawStyle.links.length
};
// Expand the bounding rect just a little so it is easier to click.
rect.left -= 5;
rect.width += 10;
var longRect = {
top: rect.top - node._meta.bounds.center.height + this._drawStyle.links.length,
left: rect.left,
width: rect.width,
height: node._meta.bounds.center.height
};
node._meta.bounds.exitBounds.push({
rect: rect,
longRect: longRect,
point: {
x: rect.left + rect.width/2,
y: rect.top + rect.height + 1
},
name: links[i].name
});
xPos += w;
}
context.restore();
},
/**
* Measures the space to render the center area for a node.
* @function wcPlayEditor#__drawCenter
* @private
* @param {wcNode} node - The node to draw.
* @param {external:Canvas~Context} context - The canvas context.
* @param {boolean} [isPalette] - If true, this node will be rendered for the palette view.
* @returns {wcPlayEditor~DrawPropertyData} - Contains bounding rectangles for various drawings.
*/
__drawCenter: function(node, context, isPalette) {
var upper = node.chain.entry.length? this._font.links.size + this._drawStyle.links.padding: 0;
var lower = node.chain.exit.length? this._font.links.size + this._drawStyle.links.padding: 0;
// Node background
context.save();
var left = node.pos.x + node._meta.bounds.center.left + node._meta.bounds.center.width/2;
var top = node.pos.y + node._meta.bounds.center.top + (node._meta.bounds.center.height)/2;
var gradient = context.createRadialGradient(left, top, 10, left, top, Math.max(node._meta.bounds.center.width, node._meta.bounds.center.height));
gradient.addColorStop(0, (node.enabled()? node._meta.color: '#555'));
gradient.addColorStop(1, 'white');
context.fillStyle = context.strokeStyle = gradient;
context.lineJoin = 'round';
var diameter = this._drawStyle.node.radius*2;
context.lineWidth = diameter;
context.fillRect(node.pos.x + node._meta.bounds.center.left + diameter/2, node.pos.y + node._meta.bounds.center.top - upper + diameter/2, node._meta.bounds.center.width - diameter, node._meta.bounds.center.height + upper + lower - diameter);
context.strokeRect(node.pos.x + node._meta.bounds.center.left + diameter/2, node.pos.y + node._meta.bounds.center.top - upper + diameter/2, node._meta.bounds.center.width - diameter, node._meta.bounds.center.height + upper + lower - diameter);
context.restore();
this.__drawRoundedRect({
left: node._meta.bounds.center.left,
top: node._meta.bounds.center.top - upper,
width: node._meta.bounds.center.width,
height: node._meta.bounds.center.height + upper + lower
}, node._meta.color, 3, this._drawStyle.node.radius, context, node.pos);
// Title Upper Bar
upper = 0;
if (node.chain.entry.length) {
context.strokeStyle = node._meta.color;
context.beginPath();
context.moveTo(node.pos.x + node._meta.bounds.center.left, node.pos.y + node._meta.bounds.center.top + upper);
context.lineTo(node.pos.x + node._meta.bounds.center.left + node._meta.bounds.center.width, node.pos.y + node._meta.bounds.center.top + upper);
context.stroke();
}
// Highlight title text.
if (!this._options.readOnly && !isPalette) {
if (this._highlightTitle && this._highlightNode === node) {
this.__drawRoundedRect(node._meta.bounds.titleBounds, this._drawStyle.property.highlightColor, this._drawStyle.property.highlightBorder, this._font.title.size/2, context, node.pos);
} else if (this._highlightNode === node) {
this.__drawRoundedRect(node._meta.bounds.titleBounds, this._drawStyle.property.normalColor, this._drawStyle.property.normalBorder, this._font.title.size/2, context, node.pos);
}
}
// Highlight details button.
if (!this._options.readOnly && node._meta.bounds.detailsBounds.width > 0 && !isPalette) {
if (this._highlightDetails && this._highlightNode === node) {
this.__drawRoundedRect(node._meta.bounds.detailsBounds, this._drawStyle.property.highlightColor, this._drawStyle.property.highlightBorder, this._font.title.size/2, context, node.pos);
} else if (this._highlightNode === node) {
this.__drawRoundedRect(node._meta.bounds.detailsBounds, this._drawStyle.property.normalColor, this._drawStyle.property.normalBorder, this._font.title.size/2, context, node.pos);
}
}
// Title Text
context.save();
upper += this._font.title.size;
context.fillStyle = 'black';
context.strokeStyle = 'black';
context.textAlign = 'left';
this.__setCanvasFont(this._font.title, context);
context.fillText(this._drawStyle.title.wrapL + node.type + ': ', node.pos.x + node._meta.bounds.titleBounds.left - node._meta.bounds.titleBounds.typeWidth, node.pos.y + node._meta.bounds.titleBounds.top + upper);
this.__setCanvasFont(this._font.titleDesc, context);
context.fillText(this._drawStyle.title.nameWrapL + (node.name || this._drawStyle.title.placeholder) + this._drawStyle.title.nameWrapR, node.pos.x + node._meta.bounds.titleBounds.left, node.pos.y + node._meta.bounds.titleBounds.top + upper);
context.textAlign = 'right';
this.__setCanvasFont(this._font.title, context);
context.fillText(this._drawStyle.title.wrapR, node.pos.x + node._meta.bounds.titleBounds.left, node.pos.y + node._meta.bounds.titleBounds.top + upper);
if (node.description() || node.details()) {
this.__setCanvasFont(this._font.details, context);
context.fillText(this._drawStyle.title.details, node.pos.x + node._meta.bounds.detailsBounds.left + node._meta.bounds.detailsBounds.width, node.pos.y + node._meta.bounds.detailsBounds.top + this._font.details.size);
}
context.restore();
// Title Lower Bar
upper += this._drawStyle.title.spacing;
// Draw the node's viewport.
if (node._meta.bounds.viewportBounds) {
context.save();
// Translate the canvas so 0,0 is the beginning of the viewport.
context.translate(node.pos.x + node._meta.bounds.viewportBounds.left, node.pos.y + node._meta.bounds.viewportBounds.top);
// Draw the viewport.
node.onViewportDraw(context, this._options.readOnly);
// Now revert the translation.
context.translate(-node.pos.x - node._meta.bounds.viewportBounds.left, -node.pos.y - node._meta.bounds.viewportBounds.top);
context.restore();
upper += node._viewportSize.y + this._drawStyle.property.spacing;
}
// Draw the property headers.
upper += this._font.property.size;
context.save();
// Display the Current and Initial column headers.
var headerBounds = {
top: node._meta.bounds.center.top + upper - this._font.property.size,
left: node._meta.bounds.center.left + this._drawStyle.node.margin,
height: this._font.property.size + this._drawStyle.property.spacing/2,
width: node._meta.bounds.center.width - this._drawStyle.node.margin*2
};
this.__drawRoundedRect(headerBounds, this._drawStyle.property.headerColor, this._drawStyle.property.headerBorder, this._font.property.size/2, context, node.pos);
if (node._meta.bounds.centerOuter.valueWidth) {
context.fillStyle = 'black';
context.textAlign = 'right';
this.__setCanvasFont(this._font.propertyHeader, context);
context.fillText('Initial', node.pos.x + node._meta.bounds.center.left + node._meta.bounds.center.width - this._drawStyle.node.margin - this._drawStyle.property.headerSpace, node.pos.y + node._meta.bounds.center.top + upper);
context.fillStyle = '#444444';
this.__setCanvasFont(this._font.propertyHeader, context);
context.fillText('Current', node.pos.x + node._meta.bounds.center.left + node._meta.bounds.center.width - this._drawStyle.node.margin - this._drawStyle.property.headerSpace - node._meta.bounds.center.initialWidth, node.pos.y + node._meta.bounds.center.top + upper);
} else {
context.fillStyle = 'black';
context.textAlign = 'right';
this.__setCanvasFont(this._font.propertyHeader, context);
context.fillText('Initial', node.pos.x + node._meta.bounds.center.left + node._meta.bounds.center.width - this._drawStyle.node.margin - this._drawStyle.property.headerSpace, node.pos.y + node._meta.bounds.center.top + upper);
}
upper += this._drawStyle.property.spacing;
var i = 0;
var a = 0;
var props = node.properties;
for (i = 0; i < props.length; ++i) {
upper += this._font.property.size;
// var propertyBound = null;
// for (var a = 0; a < node._meta.bounds.propertyBounds.length; ++a) {
// if (node._meta.bounds.propertyBounds[a].name === props[i].name) {
// propertyBound = node._meta.bounds.propertyBounds[a];
// break;
// }
// }
// Initial property value.
var initialBound = null;
for (a = 0; a < node._meta.bounds.initialBounds.length; ++a) {
if (node._meta.bounds.initialBounds[a].name === props[i].name) {
initialBound = node._meta.bounds.initialBounds[a];
break;
}
}
// Property value.
var valueBound = null;
for (a = 0; a < node._meta.bounds.valueBounds.length; ++a) {
if (node._meta.bounds.valueBounds[a].name === props[i].name) {
valueBound = node._meta.bounds.valueBounds[a];
break;
}
}
var showValue = this._engine.isRunning() && !props[i].options.linked;
// Highlight hovered values.
if (!this._options.readOnly && !isPalette && !props[i].options.readOnly) {
if (this._engine && showValue) {
if (this._highlightNode === node && this._highlightPropertyValue && this._highlightPropertyValue.name === props[i].name) {
this.__drawRoundedRect(valueBound.rect, this._drawStyle.property.highlightColor, this._drawStyle.property.highlightBorder, this._font.property.size/2, context, node.pos);
} else if (this._highlightNode === node) {
this.__drawRoundedRect(valueBound.rect, this._drawStyle.property.normalColor, this._drawStyle.property.normalBorder, this._font.property.size/2, context, node.pos);
}
}
if (this._highlightNode === node && this._highlightPropertyInitialValue && this._highlightPropertyInitialValue.name === props[i].name) {
this.__drawRoundedRect(initialBound.rect, this._drawStyle.property.highlightColor, this._drawStyle.property.highlightBorder, this._font.property.size/2, context, node.pos);
} else if (this._highlightNode === node) {
this.__drawRoundedRect(initialBound.rect, this._drawStyle.property.normalColor, this._drawStyle.property.normalBorder, this._font.property.size/2, context, node.pos);
}
}
context.fillStyle = 'black';
context.textAlign = 'left';
this.__setCanvasFont(this._font.property, context);
context.fillText(props[i].name + ': ', node.pos.x + node._meta.bounds.center.left + this._drawStyle.node.margin, node.pos.y + node._meta.bounds.center.top + upper);
context.fillStyle = 'black';
context.textAlign = 'right';
this.__setCanvasFont(this._font.initialValue, context);
context.fillText(this._drawStyle.property.initialWrapL + this.__drawPropertyValue(node, props[i], true, !showValue) + this._drawStyle.property.initialWrapR, node.pos.x + node._meta.bounds.center.left + node._meta.bounds.center.width - this._drawStyle.node.margin, node.pos.y + node._meta.bounds.center.top + upper);
if (this._engine && showValue) {
context.fillStyle = '#444444';
this.__setCanvasFont(this._font.value, context);
context.fillText(this._drawStyle.property.valueWrapL + this.__drawPropertyValue(node, props[i]) + this._drawStyle.property.valueWrapR, node.pos.x + node._meta.bounds.center.left + node._meta.bounds.center.width - this._drawStyle.node.margin - node._meta.bounds.center.initialWidth, node.pos.y + node._meta.bounds.center.top + upper);
}
// Property input.
var linkRect = null;
for (a = 0; a < node._meta.bounds.inputBounds.length; ++a) {
if (node._meta.bounds.inputBounds[a].name === props[i].name) {
linkRect = node._meta.bounds.inputBounds[a].rect;
break;
}
}
if (props[i].options && props[i].options.input) {
context.fillStyle = (this._highlightInputLink && this._highlightInputLink.name === props[i].name && this._highlightNode === node? 'cyan': props[i].inputMeta.color);
context.strokeStyle = 'black';
context.beginPath();
context.moveTo(node.pos.x + linkRect.left, node.pos.y + linkRect.top + 5);
context.lineTo(node.pos.x + linkRect.left + linkRect.width, node.pos.y + linkRect.top + 5);
context.lineTo(node.pos.x + linkRect.left + linkRect.width, node.pos.y + linkRect.top + linkRect.height - 5);
context.lineTo(node.pos.x + linkRect.left, node.pos.y + linkRect.top + linkRect.height - 5);
context.lineTo(node.pos.x + linkRect.left + linkRect.width/3, node.pos.y + linkRect.top + linkRect.height/2);
context.closePath();
context.stroke();
context.fill();
}
// Property output.
linkRect = null;
for (a = 0; a < node._meta.bounds.outputBounds.length; ++a) {
if (node._meta.bounds.outputBounds[a].name === props[i].name) {
linkRect = node._meta.bounds.outputBounds[a].rect;
break;
}
}
if (props[i].options && props[i].options.output) {
context.fillStyle = (this._highlightOutputLink && this._highlightOutputLink.name === props[i].name && this._highlightNode === node? 'cyan': props[i].outputMeta.color);
context.strokeStyle = 'black';
context.beginPath();
context.moveTo(node.pos.x + linkRect.left, node.pos.y + linkRect.top + 5);
context.lineTo(node.pos.x + linkRect.left + linkRect.width/2, node.pos.y + linkRect.top + 5);
context.lineTo(node.pos.x + linkRect.left + linkRect.width, node.pos.y + linkRect.top + linkRect.height/2);
context.lineTo(node.pos.x + linkRect.left + linkRect.width/2, node.pos.y + linkRect.top + linkRect.height - 5);
context.lineTo(node.pos.x + linkRect.left, node.pos.y + linkRect.top + linkRect.height - 5);
context.closePath();
context.stroke();
context.fill();
}
upper += this._drawStyle.property.spacing;
}
// Lower Bar
if (node.chain.exit.length) {
context.strokeStyle = node._meta.color;
context.beginPath();
context.moveTo(node.pos.x + node._meta.bounds.center.left, node.pos.y + node._meta.bounds.center.top + node._meta.bounds.center.height);
context.lineTo(node.pos.x + node._meta.bounds.center.left + node._meta.bounds.center.width, node.pos.y + node._meta.bounds.center.top + node._meta.bounds.center.height);
context.stroke();
}
context.restore();
},
/**
* Draws the entry links of a node.
* @function wcPlayEditor#__drawEntryLinks
* @private
* @param {wcNode} node - The node to draw.
* @param {external:Canvas~Context} context - The canvas context.
* @param {number} width - The width of the area to draw in.
*/
__drawEntryLinks: function(node, context, width) {
var xPos = node.pos.x - width/2 + this._drawStyle.links.margin;
var yPos = node.pos.y + this._drawStyle.links.length + this._font.links.size;
context.save();
this.__setCanvasFont(this._font.links, context);
var links = node.chain.entry;
for (var i = 0; i < links.length; ++i) {
// Link label
context.fillStyle = 'black';
var w = context.measureText(links[i].name).width + this._drawStyle.links.spacing;
context.fillText(links[i].name, xPos + this._drawStyle.links.spacing/2, yPos);
// Link connector
var rect = null;
for (var a = 0; a < node._meta.bounds.entryBounds.length; ++a) {
if (node._meta.bounds.entryBounds[a].name === links[i].name) {
rect = node._meta.bounds.entryBounds[a].rect;
break;
}
}
context.fillStyle = (this._highlightEntryLink && this._highlightEntryLink.name === links[i].name && this._highlightNode === node? 'cyan': links[i].meta.color);
context.strokeStyle = 'black';
context.beginPath();
context.moveTo(node.pos.x + rect.left + 5, node.pos.y + rect.top);
context.lineTo(node.pos.x + rect.left + rect.width/2, node.pos.y + rect.top + rect.height/3);
context.lineTo(node.pos.x + rect.left + rect.width - 5, node.pos.y + rect.top);
context.lineTo(node.pos.x + rect.left + rect.width - 5, node.pos.y + rect.top + rect.height);
context.lineTo(node.pos.x + rect.left + 5, node.pos.y + rect.top + rect.height);
context.closePath();
context.stroke();
context.fill();
xPos += w;
}
context.restore();
},
/**
* Draws the exit links of a node.
* @function wcPlayEditor#__drawExitLinks
* @private
* @param {wcNode} node - The node to draw.
* @param {external:Canvas~Context} context - The canvas context.
* @param {number} offset - An offset height.
* @param {number} width - The width of the area to draw in.
*/
__drawExitLinks: function(node, context, offset, width) {
var xPos = node.pos.x - width/2 + this._drawStyle.links.margin;
var yPos = node.pos.y + offset + this._font.links.size;
context.save();
this.__setCanvasFont(this._font.links, context);
var links = node.chain.exit;
for (var i = 0; i < links.length; ++i) {
// Link label
context.fillStyle = 'black';
var w = context.measureText(links[i].name).width + this._drawStyle.links.spacing;
context.fillText(links[i].name, xPos + this._drawStyle.links.spacing/2, yPos);
// Link connector
var rect = null;
for (var a = 0; a < node._meta.bounds.exitBounds.length; ++a) {
if (node._meta.bounds.exitBounds[a].name === links[i].name) {
rect = node._meta.bounds.exitBounds[a].rect;
break;
}
}
context.fillStyle = (this._highlightExitLink && this._highlightExitLink.name === links[i].name && this._highlightNode === node? 'cyan': links[i].meta.color);
context.strokeStyle = 'black';
context.beginPath();
context.moveTo(node.pos.x + rect.left + 5, node.pos.y + rect.top);
context.lineTo(node.pos.x + rect.left + rect.width - 5, node.pos.y + rect.top);
context.lineTo(node.pos.x + rect.left + rect.width - 5, node.pos.y + rect.top + rect.height/2);
context.lineTo(node.pos.x + rect.left + rect.width/2, node.pos.y + rect.top + rect.height);
context.lineTo(node.pos.x + rect.left + 5, node.pos.y + rect.top + rect.height/2);
context.closePath();
context.stroke();
context.fill();
xPos += w;
}
context.restore();
},
/**
* Draws connection chains for a list of nodes.
* @function wcPlayEditor#__drawChains
* @private
* @param {wcNode[]} nodes - A list of nodes to render chains for.
* @param {external:Canvas~Context} context - The canvas context.
*/
__drawChains: function(nodes, context) {
for (var i = 0; i < nodes.length; ++i) {
this.__drawNodeChains(nodes[i], context);
}
},
/**
* Draws connection chains for a single node.
* @function wcPlayEditor#__drawNodeChains
* @private
* @param {wcNode} node - A node to render chains for.
* @param {external:Canvas~Context} context - The canvas context.
*/
__drawNodeChains: function(node, context) {
var i = 0;
var a = 0;
var b = 0;
var targetNode = null;
var targetName = '';
var flash = false;
var highlight = false;
for (i = 0; i < node.chain.exit.length; ++i) {
var exitLink = node.chain.exit[i];
// Skip links that are not chained with anything.
if (!exitLink.links.length) {
continue;
}
var exitPoint = null;
// Find the corresponding meta data for this link.
for (a = 0; a < node._meta.bounds.exitBounds.length; ++a) {
if (node._meta.bounds.exitBounds[a].name === exitLink.name) {
exitPoint = node._meta.bounds.exitBounds[a].point;
break;
}
}
// Skip links that do not contain meta data (should not happen).
if (!exitPoint) {
this.error('Attempted to draw chains for an exit link that has no meta data.');
continue;
}
// Follow each chain to their entry links.
for (a = 0; a < exitLink.links.length; ++a) {
targetNode = exitLink.links[a].node;
targetName = exitLink.links[a].name;
var entryLink = null;
// Skip pairs of nodes that are not visible.
if (!node._meta.visible && !targetNode._meta.visible) {
continue;
}
for (b = 0; b < targetNode.chain.entry.length; ++b) {
if (targetNode.chain.entry[b].name === targetName) {
entryLink = targetNode.chain.entry[b];
break;
}
}
// The link for this chain was not found.
if (!entryLink) {
this.error('Attempted to chain an exit link to an entry link that was not found.');
continue;
}
// Find the corresponding meta data for this link.
var entryPoint;
for (b = 0; b < targetNode._meta.bounds.entryBounds.length; ++b) {
if (targetNode._meta.bounds.entryBounds[b].name === entryLink.name) {
entryPoint = targetNode._meta.bounds.entryBounds[b].point;
break;
}
}
// Could not find meta data for this link.
if (!entryPoint) {
this.error('Attempted to draw chains to an entry link that has no meta data.');
continue;
}
flash = (exitLink.meta.flashDelta > 0 && entryLink.meta.flashDelta > 0);
highlight =
(this._highlightNode === targetNode && this._highlightEntryLink && this._highlightEntryLink.name === entryLink.name) ||
(this._highlightNode === node && this._highlightExitLink && this._highlightExitLink.name === exitLink.name);
// Now we have both our links, lets chain them together!
this.__drawChain(node.pos, targetNode.pos, exitPoint, entryPoint, node._meta.bounds.rect, targetNode._meta.bounds.rect, context, flash, highlight);
}
}
for (i = 0; i < node.properties.length; ++i) {
var outputProp = node.properties[i];
// Skip properties with no output links.
if (!outputProp.outputs.length) {
continue;
}
// Find the corresponding meta data for this link.
var outputPoint;
for (a = 0; a < node._meta.bounds.outputBounds.length; ++a) {
if (node._meta.bounds.outputBounds[a].name === outputProp.name) {
outputPoint = node._meta.bounds.outputBounds[a].point;
break;
}
}
// Failed to find bounds for the output link.
if (!outputPoint) {
this.error('Attempted to draw chains for an output link that has no meta data.');
continue;
}
// Follow each chain to their input links.
for (a = 0; a < outputProp.outputs.length; ++a) {
targetNode = outputProp.outputs[a].node;
targetName = outputProp.outputs[a].name;
var inputProp = null;
// Skip pairs of nodes that are not visible.
if (!node._meta.visible && !targetNode._meta.visible) {
continue;
}
for (b = 0; b < targetNode.properties.length; ++b) {
if (targetNode.properties[b].name === targetName) {
inputProp = targetNode.properties[b];
}
}
// Failed to find the input property to link with.
if (!inputProp) {
this.error('Attempted to chain a property link to a property that was not found.');
continue;
}
// Find the corresponding meta data for this link.
var inputPoint;
for (b = 0; b < targetNode._meta.bounds.inputBounds.length; ++b) {
if (targetNode._meta.bounds.inputBounds[b].name === inputProp.name) {
inputPoint = targetNode._meta.bounds.inputBounds[b].point;
break;
}
}
// Failed to find the meta data for a property input link.
if (!inputPoint) {
this.error('Attempted to draw chains to a property input link that has no meta data.');
continue;
}
flash = (outputProp.outputMeta.flashDelta > 0 || inputProp.inputMeta.flashDelta > 0);
highlight =
(this._highlightNode === targetNode && this._highlightInputLink && this._highlightInputLink.name === inputProp.name) ||
(this._highlightNode === node && this._highlightOutputLink && this._highlightOutputLink.name === outputProp.name);
// Now we have both our links, lets chain them together!
this.__drawChain(node.pos, targetNode.pos, outputPoint, inputPoint, node._meta.bounds.rect, targetNode._meta.bounds.rect, context, flash, highlight, true);
}
}
// Draw a link to the mouse cursor if we are making a connection.
var targetPos = null;
var targetRect = null;
var targetOffset = null;
var point = null;
if (this._selectedNode === node && this._selectedEntryLink) {
highlight = false;
if (this._highlightNode && this._highlightExitLink) {
targetPos = this._highlightExitLink.point;
targetRect = this._highlightExitLink.rect;
targetOffset = this._highlightNode.pos;
highlight = true;
} else {
targetPos = {
x: (this._mouse.x - this._viewportCamera.x) / this._viewportCamera.z,
y: (this._mouse.y - this._viewportCamera.y) / this._viewportCamera.z
};
targetRect = {
left: targetPos.x,
top: targetPos.y,
width: 1,
height: 1
};
targetOffset = {x: 0, y: 0};
}
point = null;
for (i = 0; i < node._meta.bounds.entryBounds.length; ++i) {
if (node._meta.bounds.entryBounds[i].name === this._selectedEntryLink.name) {
point = node._meta.bounds.entryBounds[i].point;
}
}
this.__drawChain(targetOffset, node.pos, targetPos, point, targetRect, node._meta.bounds.rect, context, highlight);
}
if (this._selectedNode === node && this._selectedExitLink) {
targetPos = null;
targetRect = null;
targetOffset = null;
highlight = false;
if (this._highlightNode && this._highlightEntryLink) {
targetPos = this._highlightEntryLink.point;
targetRect = this._highlightEntryLink.rect;
targetOffset = this._highlightNode.pos;
highlight = true;
} else {
targetPos = {
x: (this._mouse.x - this._viewportCamera.x) / this._viewportCamera.z,
y: (this._mouse.y - this._viewportCamera.y) / this._viewportCamera.z
};
targetRect = {
left: targetPos.x,
top: targetPos.y,
width: 1,
height: 1
};
targetOffset = {x: 0, y: 0};
}
point = null;
for (i = 0; i < node._meta.bounds.exitBounds.length; ++i) {
if (node._meta.bounds.exitBounds[i].name === this._selectedExitLink.name) {
point = node._meta.bounds.exitBounds[i].point;
}
}
this.__drawChain(node.pos, targetOffset, point, targetPos, node._meta.bounds.rect, targetRect, context, highlight);
}
if (this._selectedNode === node && this._selectedInputLink) {
targetPos = null;
targetRect = null;
targetOffset = null;
highlight = false;
if (this._highlightNode && this._highlightOutputLink) {
targetPos = this._highlightOutputLink.point;
targetRect = this._highlightOutputLink.rect;
targetOffset = this._highlightNode.pos;
highlight = true;
} else {
targetPos = {
x: (this._mouse.x - this._viewportCamera.x) / this._viewportCamera.z,
y: (this._mouse.y - this._viewportCamera.y) / this._viewportCamera.z
};
targetRect = {
left: targetPos.x,
top: targetPos.y,
width: 1,
height: 1
};
targetOffset = {x: 0, y: 0};
}
point = null;
for (i = 0; i < node._meta.bounds.inputBounds.length; ++i) {
if (node._meta.bounds.inputBounds[i].name === this._selectedInputLink.name) {
point = node._meta.bounds.inputBounds[i].point;
}
}
this.__drawChain(targetOffset, node.pos, targetPos, point, targetRect, node._meta.bounds.rect, context, highlight, false, true);
}
if (this._selectedNode === node && this._selectedOutputLink) {
targetPos = null;
targetRect = null;
targetOffset = null;
highlight = false;
if (this._highlightNode && this._highlightInputLink) {
targetPos = this._highlightInputLink.point;
targetRect = this._highlightInputLink.rect;
targetOffset = this._highlightNode.pos;
highlight = true;
} else {
targetPos = {
x: (this._mouse.x - this._viewportCamera.x) / this._viewportCamera.z,
y: (this._mouse.y - this._viewportCamera.y) / this._viewportCamera.z
};
targetRect = {
left: targetPos.x,
top: targetPos.y,
width: 1,
height: 1
};
targetOffset = {x: 0, y: 0};
}
point = null;
for (i = 0; i < node._meta.bounds.outputBounds.length; ++i) {
if (node._meta.bounds.outputBounds[i].name === this._selectedOutputLink.name) {
point = node._meta.bounds.outputBounds[i].point;
}
}
this.__drawChain(node.pos, targetOffset, point, targetPos, node._meta.bounds.rect, targetRect, context, highlight, false, true);
}
},
/**
* Draws a chain between two connector links, you can flip the x and y axes to achieve either a flow or property chain orientation.
* @function wcPlayEditor#__drawChain
* @private
* @param {wcPlay~Coordinates} startOffset - The offset for the start position and rect.
* @param {wcPlay~Coordinates} endOffset - The offset for the end position and rect.
* @param {wcPlay~Coordinates} startPos - The start position (the exit link).
* @param {wcPlay~Coordinates} endPos - The end position (the entry link).
* @param {wcPlayEditor~Rect} startRect - The start node's bounding rect to avoid.
* @param {wcPlayEditor~Rect} endRect - The end node's bounding rect to avoid.
* @param {external:Canvas~Context} context - The canvas context.
* @param {boolean} [flash] - If true, will flash the link.
* @param {boolean} [highlight] - If true, the link will be highlighted.
* @param {boolean} [isProperty] - If true, will render property chain orientation.
*/
__drawChain: function(startOffset, endOffset, startPos, endPos, startRect, endRect, context, flash, highlight, isProperty) {
context.save();
context.lineWidth = 2;
context.lineCap = 'round';
context.lineJoin = 'round';
context.beginPath();
context.moveTo((startOffset.x + startPos.x), (startOffset.y + startPos.y));
// this._chainDrawCount += 1;
// Do some preparation to make the orientation invisible.
// function __lineTo(x, y) {
// if (isProperty) {
// context.lineTo(y, x);
// } else {
// context.lineTo(x, y);
// }
// }
function __arcTo(x1, y1, x2, y2, radius) {
if (isProperty) {
context.arcTo(y1, x1, y2, x2, radius);
} else {
context.arcTo(x1, y1, x2, y2, radius);
}
}
function __curveTo(x1, y1, x2, y2, x3, y3) {
if (isProperty) {
context.bezierCurveTo(y1, x1, y2, x2, y3, x3);
} else {
context.bezierCurveTo(x1, y1, x2, y2, x3, y3);
}
}
var start, end, startBounds, endBounds;
if (isProperty) {
start = {
x: startOffset.y + startPos.y,
y: startOffset.x + startPos.x
};
end = {
x: endOffset.y + endPos.y,
y: endOffset.x + endPos.x
};
startBounds = {
top: startRect.left + startOffset.x,
left: startRect.top + startOffset.y,
width: startRect.height,
height: startRect.width
};
endBounds = {
top: endRect.left + endOffset.x,
left: endRect.top + endOffset.y,
width: endRect.height,
height: endRect.width
};
context.strokeStyle = (highlight? 'cyan': (flash? '#CCCC00': '#33CC33'));
} else {
start = {
x: startOffset.x + startPos.x,
y: startOffset.y + startPos.y
};
end = {
x: endOffset.x + endPos.x,
y: endOffset.y + endPos.y
};
startBounds = {
top: startRect.top + startOffset.y,
left: startRect.left + startOffset.x,
width: startRect.width,
height: startRect.height
};
endBounds = {
top: endRect.top + endOffset.y,
left: endRect.left + endOffset.x,
width: endRect.width,
height: endRect.height
};
context.strokeStyle = (highlight? 'cyan': (flash? '#CCCC00': '#000000'));
}
var a = 0, midx = 0, midy = 0, leftx = 0, rightx = 0, sidex = 0, radius = 0, top = 0, bottom = 0, coreRadius = 15;
switch (this._chainStyle) {
// Squared chains
case 0:
// If the exit link is above the entry link
if (start.y < end.y) {
midx = (end.x + start.x) / 2;
midy = (end.y + start.y) / 2;
radius = Math.min(coreRadius, Math.abs(end.x - start.x)/2, Math.abs(end.y - start.y)/2);
__arcTo(start.x, midy, midx, midy, radius);
__arcTo(end.x, midy, end.x, end.y, radius);
}
// If the start rect is to the left side of the end rect.
else if (startBounds.left + startBounds.width < endBounds.left) {
midx = (endBounds.left + startBounds.left + startBounds.width) / 2 - 2;
midy = (end.y + start.y) / 2;
leftx = (midx + start.x) / 2;
rightx = (end.x + midx) / 2;
radius = Math.min(coreRadius, Math.abs(end.y - start.y)/4, Math.abs(midx - leftx), Math.abs(midx - rightx));
__arcTo(start.x, start.y + radius, leftx, start.y + radius, radius);
__arcTo(midx, start.y + radius, midx, midy, radius);
__arcTo(midx, end.y - radius, rightx, end.y - radius, radius);
__arcTo(end.x, end.y - radius, end.x, end.y, radius);
}
// If the start rect is to the right side of the end rect.
else if (startBounds.left > endBounds.left + endBounds.width) {
midx = (startBounds.left + endBounds.left + endBounds.width) / 2 + 2;
midy = (end.y + start.y) / 2;
leftx = (midx + end.x) / 2;
rightx = (start.x + midx) / 2;
radius = Math.min(coreRadius, Math.abs(end.y - start.y)/4, Math.abs(midx - leftx), Math.abs(midx - rightx));
__arcTo(start.x, start.y + radius, rightx, start.y + radius, radius);
__arcTo(midx, start.y + radius, midx, midy, radius);
__arcTo(midx, end.y - radius, leftx, end.y - radius, radius);
__arcTo(end.x, end.y - radius, end.x, end.y, radius);
}
// If the start link is below the end link. Makes a loop around the nodes.
else if (start.y > end.y && Math.abs(start.y - end.y) > this._drawStyle.links.length) {
a = start.x;
top = Math.min(startBounds.top - coreRadius, endBounds.top - coreRadius);
bottom = Math.max(startBounds.top + startBounds.height + coreRadius, endBounds.top + endBounds.height + coreRadius);
midy = (start.y + end.y) / 2;
// Choose left or right.
if (Math.abs(Math.min(startBounds.left, endBounds.left) - start.x) <= Math.abs(Math.max(startBounds.left + startBounds.width, endBounds.left + endBounds.width) - end.x)) {
// Left
a = Math.min(startBounds.left - coreRadius, endBounds.left - coreRadius);
bottom -= 2;
} else {
// Right
a = Math.max(startBounds.left + startBounds.width + coreRadius, endBounds.left + endBounds.width + coreRadius);
bottom += 2;
}
midx = (start.x + a) / 2;
radius = Math.min(coreRadius, Math.abs(a - (start.x))/2, Math.abs(a - (end.x))/2);
__arcTo(start.x, bottom, midx, bottom, radius);
__arcTo(a, bottom, a, midy, radius);
__arcTo(a, top, midx, top, radius);
__arcTo(end.x, top, end.x, end.y, radius);
}
break;
// Splined chains.
case 1:
// If the Exit link is right above the Entry link target.
if (start.y < end.y) {
midy = (start.y + end.y) / 2;
midx = (start.x + end.x) / 2;
__curveTo(start.x, midy, end.x, midy, end.x, end.y);
}
// If the start rect is to the left or right side of the end rect.
else if (startBounds.left + startBounds.width < endBounds.left || startBounds.left > endBounds.left + endBounds.width) {
radius = Math.abs(start.y - end.y) / 2;
top = endBounds.top - radius;
bottom = startBounds.top + startBounds.height + radius;
__curveTo(start.x, bottom, end.x, top, end.x, end.y);
}
// If the start link is below the end link. Makes a loop around the nodes.
else if (start.y > end.y && Math.abs(start.y - end.y) > this._drawStyle.links.length) {
sidex = start.x;
// Choose left or right.
if (Math.abs(Math.min(startBounds.left, endBounds.left) - start.x) <= Math.abs(Math.max(startBounds.left + startBounds.width, endBounds.left + endBounds.width) - end.x)) {
// Left
sidex = Math.min(startBounds.left, endBounds.left) - 15;
} else {
// Right
sidex = Math.max(startBounds.left + startBounds.width, endBounds.left + endBounds.width) + 15;
}
top = endBounds.top - 30;
bottom = Math.max(startBounds.top + startBounds.height + 30, endBounds.top + endBounds.height + 30);
midy = (start.y + end.y)/2;
__curveTo(start.x, bottom, sidex, bottom, sidex, midy);
__curveTo(sidex, top, end.x, top, end.x, end.y);
// top = endBounds.top - Math.abs(end.x - sidex)/2;
// bottom = Math.max(startBounds.top + startBounds.height + Math.abs(start.x - sidex)/2, endBounds.top + endBounds.height + Math.abs(end.x - sidex)/2);
// __curveTo(start.x, bottom, sidex, bottom, sidex, start.y);
// __lineTo(sidex, end.y);
// __curveTo(sidex, top, end.x, top, end.x, end.y);
}
break;
}
// Finish our line to the end position.
context.lineTo((endOffset.x + endPos.x), (endOffset.y + endPos.y));
context.stroke();
context.restore();
},
/**
* Draws the value of a property embedded on the node.
* @function wcPlayEditor#__drawPropertyValue
* @private
* @param {wcNode} node - The node that owns this property.
* @param {Object} property - The property data.
* @param {boolean} [initial] - Set true if the property being viewed is the initial value.
* @param {boolean} [expanded] - For initial values, if we are not displaying the current value then we should expand the display.
* @returns {string} - A string value to print as the value.
*
* @see {wcNode~PropertyOptions}
* @see {wcNode~PropertyDisplay}
*/
__drawPropertyValue: function(node, property, initial, expanded) {
var value = null;
if (initial) {
value = node.initialProperty(property.name);
} else {
value = node.property(property.name);
}
if (typeof property.options.display === 'function') {
value = property.options.display.call(node, value, initial);
} else {
// Handle custom display of certain property types.
switch (property.type) {
case wcPlay.PROPERTY.TOGGLE:
// Display toggle buttons as 'yes', 'no'
return (value? 'yes': 'no');
case wcPlay.PROPERTY.SELECT:
var noneValue = '';
if (property.options.hasOwnProperty('noneValue')) {
noneValue = property.options.noneValue;
}
// Display none only if we allow none.
if ((!property.options.hasOwnProperty('allowNone') || property.options.allowNone) && value == noneValue) {
return '<none>';
}
var items = property.options.items;
if ($.isFunction(items)) {
items = items.call(node);
}
if ($.isArray(items)) {
var found = false;
for (var i = 0; i < items.length; ++i) {
if (typeof items[i] === 'object') {
if (items[i].value == value) {
value = items[i].name;
found = true;
break;
}
} else if (typeof items[i] === 'string') {
if (items[i] == value) {
found = true;
break;
}
}
}
if (!found) {
value = '<unknown>';
}
}
break;
}
}
return this.__clampString(String(value), (expanded? this._drawStyle.property.longStrLen: this._drawStyle.property.strLen));
},
/**
* Draws the detail popup box for the node.
* @function wcPlayEditor#__drawDetailsPopup
* @param {wcNode} node - The node to draw for.
*/
__drawDetailsPopup: function(node) {
var displayTitle = node.type + ' Node';
var displayInfo = node.description();
if (displayInfo) {
displayInfo += '\n\n';
}
displayInfo += node.details();
var $blocker = $('<div class="wcPlayDetailsPopupBlocker">');
var $popup = $('<div class="wcPlayDetailsPopup">');
var $title = $('<h3>' + displayTitle + '</h3>');
var $info = $('<pre class="wcPlayDetailsPopupText">' + displayInfo + '</pre>');
$popup.append($title);
$popup.append($info);
$blocker.append($popup);
$('body').append($blocker);
$blocker.click(function() {
$(this).remove();
});
},
/**
* A callback handler when a node has been created.
* @typedef wcPlayEditor~OnPalettePopupFinished
* @param {wcNode} [node] - The node created, or null if the operation was cancelled.
*/
/**
* Draws the popup that allows the user to select a node type.
* @function wcPlayEditor#__drawPalettePopup
* @param {wcPlay~Coordinates} pos - The position to center the popup.
* @param {wcNode} [linkNode] - If supplied, the source node to link with.
* @param {string} [linkName] - If supplied, the name of the source link.
* @param {wcNode.LINK_TYPE} [linkType] - If supplying a node, this is the type of link you are attaching to.
* @param {wcPlayEditor~OnPalettePopupFinished} [onFinished] - A callback function to call when the popup has resolved.
*/
__drawPalettePopup: function(pos, linkNode, linkName, linkType, onFinished) {
// Populate the node list.
var $resultList = null;
function __searchList(key) {
var result = fuse.search(key);
// No results, just show the full listing.
if (!result.length && !key) {
result = self._nodeLibrary;
}
var $listContainer = $('<div id="wcPlayEditorPaletteList">');
var $list = $('<ul>');
$listContainer.append($list);
for (var i = 0; i < result.length; ++i) {
var data = result[i];
var links = null;
// Determine whether to filter a node based on available connection links.
if (connectLink) {
if (!data[connectLink].length) {
continue;
}
links = data[connectLink];
}
var link = '';
if (!links) {
link = ' class="wcSelectable"';
}
var describer = data.node.category;
var add = false;
var $item = $('<li id="wcNode-' + data.id + '"' + link + ' title="' + data.desc + '"><span class="wcMainLabel">' + data.displayName + '</span><span class="wcDescriber">' + describer + '</span></li>');
if (links) {
var $links = $('<ul>');
$item.append($links);
for (var a = 0; a < links.length; ++a) {
// Ensure the connection can actually be made.
var linkOptions = data.node.propertyOptions(links[a].name);
if (!options ||
((!options[linkType + 'Condition'] || options[linkType + 'Condition'].call(linkNode, data.node, links[a].name)) &&
(!linkOptions[connectLink + 'Condition'] || linkOptions[connectLink + 'Condition'].call(data.node, linkNode, linkName)))) {
add = true;
$links.append('<li id="wcNode-' + data.id + '-' + a + '" class="wcSelectable wcLinkItem" title="' + links[a].desc + '"><span class="wcPrefix">' + connectLink + ' -- </span><span class="wcMainLabel">' + links[a].name + '</span></li>');
}
}
} else {
add = true;
}
if (add) {
$list.append($item);
}
}
// Attempt to find the currently selected item.
var $selected = [];
if (current) {
$selected = $list.find('#'+current);
}
// Always select the first item.
$selected = $list.find('.wcSelectable').first();
current = null;
if ($selected.length) {
$selected.addClass('wcSelected');
current = $selected.attr('id');
}
if ($resultList) {
$resultList.remove();
}
$resultList = $listContainer;
$popup.append($resultList);
__ensureVisible($selected);
// Make all selection items clickable.
$('#wcPlayEditorPaletteList .wcSelectable').click(function() {
current = this.id;
__createNode(function() {
$blocker.click();
});
event.preventDefault();
return true;
});
}
function __ensureVisible($item) {
if ($item.length && $item[0].scrollIntoView) {
// Check if the item is visible.
var itemRect = $item[0].getBoundingClientRect();
var listRect = $resultList[0].getBoundingClientRect();
if (itemRect.top < listRect.top) {
if ($item.hasClass('wcLinkItem')) {
$item = $item.parents('li');
}
$item[0].scrollIntoView(true);
} else if (itemRect.bottom > listRect.bottom) {
$item[0].scrollIntoView(false);
}
}
}
function __createNode(cb) {
if (!current) {
cb();
return;
}
var id = current.split('-')[1];
var link = current.split('-')[2];
var data = self._nodeLibrary[id];
if (!data) {
return;
}
// Create an instance of the node and add it to the script.
var newNode = new window.wcPlayNodes[data.className](self._parent, {x: 0, y: 0});
var exportData = data.node.export(); // Export nodes default data set.
exportData.id = newNode.id;
// Position the new node.
exportData.pos.x = (pos.x - self._viewportCamera.x) / self._viewportCamera.z;
exportData.pos.y = (pos.y - self._viewportCamera.y) / self._viewportCamera.z;
// Calculate position based on link connector.
var bounds = null;
switch (linkType) {
case wcNode.LINK_TYPE.ENTRY:
bounds = data.node._meta.bounds.exitBounds;
break;
case wcNode.LINK_TYPE.EXIT:
bounds = data.node._meta.bounds.entryBounds;
break;
case wcNode.LINK_TYPE.INPUT:
bounds = data.node._meta.bounds.outputBounds;
break;
case wcNode.LINK_TYPE.OUTPUT:
bounds = data.node._meta.bounds.inputBounds;
break;
}
if (bounds) {
var bound = bounds.find(function(bound) {
return bound.name === data[connectLink][link].name;
});
if (bound) {
exportData.pos.x -= bound.point.x;
exportData.pos.y -= bound.point.y;
}
}
newNode.import(exportData, []);
// Connect nodes if possible.
switch (linkType) {
case wcNode.LINK_TYPE.ENTRY:
linkNode.connectEntry(linkName, newNode, data[connectLink][link].name);
break;
case wcNode.LINK_TYPE.EXIT:
linkNode.connectExit(linkName, newNode, data[connectLink][link].name);
break;
case wcNode.LINK_TYPE.INPUT:
linkNode.connectInput(linkName, newNode, data[connectLink][link].name);
break;
case wcNode.LINK_TYPE.OUTPUT:
linkNode.connectOutput(linkName, newNode, data[connectLink][link].name);
break;
}
self.__onCreateNode(newNode);
self._selectedNode = newNode;
self._selectedNodes = [newNode];
self.__updateNode(newNode, 0, self._viewportContext);
self.__drawNode(newNode, self._viewportContext);
cb();
}
if (!this._showingSelector) {
var self = this;
var current = null;
var createdNode = null;
this._showingSelector = true;
var connectLink = '';
var options = linkNode? linkNode.propertyOptions(linkName): {};
switch (linkType) {
case wcNode.LINK_TYPE.ENTRY: connectLink = wcNode.LINK_TYPE.EXIT; break;
case wcNode.LINK_TYPE.EXIT: connectLink = wcNode.LINK_TYPE.ENTRY; break;
case wcNode.LINK_TYPE.INPUT: connectLink = wcNode.LINK_TYPE.OUTPUT; break;
case wcNode.LINK_TYPE.OUTPUT: connectLink = wcNode.LINK_TYPE.INPUT; break;
}
var header = 'Create Node';
if (connectLink) {
header += ' and connect to an ' + connectLink;
}
var $blocker = $('<div class="wcPlayEditorBlocker">');
var $popup = $('<div id="wcPlayEditorPalettePopup"><label>' + header + ':</label></div>');
$popup.css('width', this._drawStyle.palettePopup.width);
$popup.css('height', this._drawStyle.palettePopup.height);
this.$main.append($blocker);
this.$main.append($popup);
$blocker.click(function() {
$(this).remove();
$popup.remove();
self._showingSelector = false;
onFinished && onFinished(createdNode);
});
// Node search input field with auto-focus.
var $input = $('<input type="text" id="wcPlayEditorPaletteInput">');
$popup.append($input);
$input.focus();
$input.select();
$input.keydown(function(event) {
// Stop the key presses from triggering hotkeys
event.stopPropagation();
var $selected = null;
var $next = null;
// Cancel on escape.
if (event.keyCode === 27) {
$blocker.click();
}
// Return to select the current item.
else if (event.keyCode === 13) {
// Create our node.
__createNode(function() {
$blocker.click();
});
event.preventDefault();
return true;
}
// Down arrow, or Tab to cycle next item.
else if (event.keyCode === 40 || (event.keyCode === 9 && !event.shiftKey)) {
if (current) {
$selected = $('#'+current);
if ($selected.length) {
$next = $selected.next('.wcSelectable');
// No more siblings, jump to first child of our parent instead.
if (!$next.length) {
$next = $selected.parents('li').next();
$next = $next.find('.wcSelectable').first();
}
if ($next.length) {
$next.addClass('wcSelected');
current = $next.attr('id');
$selected.removeClass('wcSelected');
__ensureVisible($next);
}
}
}
event.preventDefault();
return true;
}
// Up arrow, shift-tab to cycle previous item.
else if (event.keyCode === 38 || (event.keyCode === 9 && event.shiftKey)) {
if (current) {
$selected = $('#'+current);
if ($selected.length) {
$next = $selected.prev('.wcSelectable');
// No more siblings, jump to first child of our parent instead.
if (!$next.length) {
$next = $selected.parents('li').prev();
$next = $next.find('.wcSelectable').last();
}
if ($next.length) {
$next.addClass('wcSelected');
current = $next.attr('id');
$selected.removeClass('wcSelected');
__ensureVisible($next);
}
}
}
event.preventDefault();
return true;
}
return true;
});
var searchValue = '';
$input.keyup(function() {
// Re-perform the search when the search value has changed.
var val = $input.val().toLowerCase();
if (searchValue !== val) {
searchValue = val;
__searchList(val);
}
});
var fuse = new Fuse(this._nodeLibrary, {
caseSensitive: false,
shouldSort: true,
tokenize: false,
threshold: 0.4,
location: 0,
distance: 100,
maxPatternLength: 32,
keys: ['displayName', 'category', connectLink + '.name']
});
__searchList('');
}
if ($popup) {
// Clamp the popup position so it remains inside the canvas.
var viewWidth = this.$main.width();
var viewHeight = this.$main.height();
var left = pos.x - this._drawStyle.palettePopup.width/2;
var top = pos.y;
switch (linkType) {
case wcNode.LINK_TYPE.INPUT:
left = pos.x - this._drawStyle.palettePopup.width;
top = pos.y - this._drawStyle.palettePopup.searchOffset;
break;
case wcNode.LINK_TYPE.OUTPUT:
left = pos.x;
top = pos.y - this._drawStyle.palettePopup.searchOffset;
break;
case wcNode.LINK_TYPE.ENTRY:
top = pos.y - this._drawStyle.palettePopup.height;
break;
}
left = Math.min(Math.max(left, this._drawStyle.palettePopup.padding), viewWidth - this._drawStyle.palettePopup.width - this._drawStyle.palettePopup.padding);
top = Math.min(Math.max(top, this._drawStyle.palettePopup.padding), viewHeight - this._drawStyle.palettePopup.height - this._drawStyle.palettePopup.padding);
$popup.css('left', left + 'px');
$popup.css('top', top + 'px');
}
},
/**
* Draws the editor control for the title of the node.
* @function wcPlayEditor#__drawTitleEditor
* @private
* @param {wcNode} node - The node to draw for.
* @param {wcPlayEditor~BoundingData} bounds - The bounding data for the title.
*/
__drawTitleEditor: function(node, bounds) {
if (this._options.readOnly) {
return;
}
var self = this;
var cancelled = false;
var datalistProp = '';
var items = node.onNameEditSuggestion();
if (Array.isArray(items)) {
datalistProp = ' list="wcDynamicSuggestionList"';
var $dataList = $('<datalist id="wcDynamicSuggestionList"/>');
for (var i = 0; i < items.length; ++i) {
if (typeof items[i] === 'object') {
$dataList.append($('<option value="' + items[i].value + '">' + items[i].name + '</option>'));
} else {
$dataList.append($('<option value="' + items[i] + '">' + items[i] + '</option>'));
}
}
$('body').append($dataList);
}
var $control = $('<input type="text" maxlength="40"' + datalistProp + '/>');
$control.val(node.name);
$control.change(function() {
$('#wcDynamicSuggestionList').remove();
if (!cancelled) {
if (self._undoManager) {
self._undoManager.beginGroup('Title changed for Node "' + node.category + '.' + node.type + '"');
self._undoManager.addEvent('', {
id: node.id,
oldValue: node.name,
newValue: $control.val(),
engine: self._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
var oldName = myNode.name;
var newName = myNode.onNameChanging(oldName, this.oldValue);
if (newName === undefined) {
newName = this.oldValue;
}
myNode.name = newName;
myNode.onNameChanged(oldName, newName);
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
var oldName = myNode.name;
var newName = myNode.onNameChanging(oldName, this.newValue);
if (newName === undefined) {
newName = this.newValue;
}
myNode.name = newName;
myNode.onNameChanged(oldName, newName);
});
}
var oldName = node.name;
var newName = node.onNameChanging(oldName, $control.val());
if (newName === undefined) {
newName = $control.val();
}
node.name = newName;
node.onNameChanged(oldName, newName, self._undoManager);
self._undoManager && self._undoManager.endGroup();
}
});
var offset = {
top: 0,
left: this.$palette.width()
};
this.$main.append($control);
$control.addClass('wcPlayEditorControl');
$control.focus();
$control.select();
// Clicking away will close the editor control.
$control.blur(function() {
$('#wcDynamicSuggestionList').remove();
$(this).remove();
});
$control.keydown(function(event) {
event.stopPropagation();
});
$control.keyup(function(event) {
switch (event.keyCode) {
case 13: // Enter to confirm.
$control.blur();
break;
case 27: // Escape to cancel.
cancelled = true;
$control.blur();
break;
}
return false;
});
$control.css('top', offset.top + (node.pos.y + bounds.top) * this._viewportCamera.z + this._viewportCamera.y)
.css('left', offset.left + (node.pos.x + bounds.left) * this._viewportCamera.z + this._viewportCamera.x)
.css('width', Math.max(bounds.width * this._viewportCamera.z, 200))
.css('height', Math.max(bounds.height * this._viewportCamera.z, 15));
},
/**
* Draws the editor control for a property.
* @function wcPlayEditor#__drawPropertyEditor
* @private
* @param {wcNode} node - The node to draw for.
* @param {Object} property - The property data.
* @param {wcPlayEditor~BoundingData} bounds - The bounding data for this property.
* @param {boolean} [initial] - Set true if the property being changed is the initial value.
*/
__drawPropertyEditor: function(node, property, bounds, initial) {
if (this._options.readOnly || property.options.readOnly) {
return;
}
var $control = null;
var cancelled = false;
var enterConfirms = true;
var propFn = (initial? 'initialProperty': 'property');
var self = this;
function undoChange(name, oldValue, newValue) {
if (self._undoManager) {
self._undoManager.beginGroup('Property "' + name + '" changed for Node "' + node.category + '.' + node.type + '"');
self._undoManager.addEvent('', {
id: node.id,
name: name,
propFn: propFn,
oldValue: oldValue,
newValue: newValue,
engine: self._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
if (myNode) {
myNode[this.propFn](this.name, this.oldValue, true, true);
}
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
if (myNode) {
myNode[this.propFn](this.name, this.newValue, true, true);
}
});
}
}
function endChange() {
self._undoManager && self._undoManager.endGroup();
}
var $blocker = $('<div class="wcPlayEditorBlocker">');
var items = [];
var value = null;
var datalistProp = '';
var i = 0;
// Determine what editor to use for the property.
switch (property.type) {
case wcPlay.PROPERTY.TOGGLE:
// Toggles do not show an editor, instead, they just toggle their state.
var state = node[propFn](property.name);
undoChange(property.name, state, !state);
node[propFn](property.name, !state, true, true, this._undoManager);
endChange();
break;
case wcPlay.PROPERTY.NUMBER:
$control = $('<input type="number"' + (property.options.min? ' min="' + property.options.min + '"': '') + (property.options.max? ' max="' + property.options.max + '"': '') + (property.options.step? ' step="' + property.options.step + '"': '') + '>');
$control.val(parseFloat(node[propFn](property.name)));
$control.change(function() {
if (!cancelled) {
var min = $(this).attr('min') !== undefined? parseFloat($(this).attr('min')): -Infinity;
var max = $(this).attr('max') !== undefined? parseFloat($(this).attr('max')): Infinity;
value = Math.min(max, Math.max(min, parseFloat($control.val())));
undoChange(property.name, value, $control.val());
node[propFn](property.name, $control.val(), true, true, self._undoManager);
endChange();
// $blocker.click();
}
});
$control.keyup(function(event) {
if (event.keyCode === 13) {
$blocker.click();
}
});
break;
case wcPlay.PROPERTY.STRING:
case wcPlay.PROPERTY.DYNAMIC:
items = property.options.items;
if ($.isFunction(items)) {
items = items.call(node);
}
if (Array.isArray(items)) {
datalistProp = ' list="wcDynamicSuggestionList"';
var $dataList = $('<datalist id="wcDynamicSuggestionList"/>');
for (i = 0; i < items.length; ++i) {
if (typeof items[i] === 'object') {
$dataList.append($('<option value="' + items[i].value + '">' + items[i].name + '</option>'));
} else {
$dataList.append($('<option value="' + items[i] + '">' + items[i] + '</option>'));
}
}
$('body').append($dataList);
}
if (property.options.multiline) {
$control = $('<textarea' + (property.options.maxlength? ' maxlength="' + property.options.maxlength + '"': '') + datalistProp + '/>');
enterConfirms = false;
} else {
$control = $('<input type="text" maxlength="' + (property.options.maxlength || 524288) + '"' + datalistProp + '/>');
}
$control.val(node[propFn](property.name).toString());
$control.change(function() {
$('#wcDynamicSuggestionList').remove();
if (!cancelled) {
value = node[propFn](property.name);
undoChange(property.name, value, $control.val());
node[propFn](property.name, $control.val(), true, true, self._undoManager);
endChange();
$blocker.click();
}
});
break;
case wcPlay.PROPERTY.SELECT:
value = node[propFn](property.name);
$control = $('<select>');
items = property.options.items;
if ($.isFunction(items)) {
items = items.call(node);
}
if (Array.isArray(items)) {
var noneValue = '';
if (property.options.hasOwnProperty('noneValue')) {
noneValue = property.options.noneValue;
}
var found = false;
if (!property.options.hasOwnProperty('allowNone') || property.options.allowNone) {
$control.append($('<option value=""' + (noneValue == value? ' selected': '') + '><none></option>'));
if (noneValue == value) found = true;
}
for (i = 0; i < items.length; ++i) {
if (typeof items[i] === 'object') {
$control.append($('<option value="' + items[i].value + '"' + (items[i].value == value? ' selected': '') + '>' + items[i].name + '</option>'));
if (items[i].value == value) found = true;
} else {
$control.append($('<option value="' + items[i] + '"' + (items[i] == value? ' selected': '') + '>' + items[i] + '</option>'));
if (items[i] == value) found = true;
}
}
// We did not find the current item, prepend an 'unknown' entry and select that instead.
if (!found) {
$control.prepend($('<option value="' + value + '" selected><unknown></option>'));
}
} else {
this.error('Tried to display a Select type property when no selection list was provided.');
return;
}
$control.change(function() {
if (!cancelled) {
value = node[propFn](property.name);
var newValue = $control.val();
if (newValue == '' && property.options.hasOwnProperty('noneValue')) {
newValue = property.options.noneValue;
}
undoChange(property.name, value, newValue);
node[propFn](property.name, newValue, true, true, self._undoManager);
endChange();
$blocker.click();
}
});
break;
case wcPlay.PROPERTY.CUSTOM:
if (typeof property.options.onCreate === 'function') {
value = node[propFn](property.name);
$control = $(property.options.onCreate(node, property.name, value, initial, function(newValue) {
if (!cancelled) {
value = node[propFn](property.name);
undoChange(property.name, value, newValue);
node[propFn](property.name, newValue, true, true, self._undoManager);
endChange();
$blocker.click();
}
}));
}
break;
}
if ($control) {
var offset = {
top: 0,
left: this.$palette.width()
};
this.$main.append($blocker);
this.$main.append($control);
$control.addClass('wcPlayEditorControl');
$control.focus();
$control.select();
// Clicking away will close the editor control.
$blocker.click(function(event) {
$('#wcDynamicSuggestionList').remove();
event.stopPropagation();
$blocker.remove();
$control.remove();
});
$control.keydown(function(event) {
event.stopPropagation();
});
$control.keyup(function(event) {
switch (event.keyCode) {
case 13: // Enter to confirm.
if (enterConfirms || event.ctrlKey) {
$control.blur();
}
break;
case 27: // Escape to cancel.
cancelled = true;
$control.blur();
break;
}
return false;
});
$control.css('top', offset.top + (node.pos.y + bounds.rect.top) * this._viewportCamera.z + this._viewportCamera.y)
.css('left', offset.left + (node.pos.x + bounds.rect.left) * this._viewportCamera.z + this._viewportCamera.x)
.css('width', Math.max(bounds.rect.width * this._viewportCamera.z, 200))
.css('height', Math.max(bounds.rect.height * this._viewportCamera.z, 15));
}
},
/**
* Takes all selected nodes and generates a new Composite node to contain them.
* @function wcPlayEditor#__createComposite
* @private
*/
__createComposite: function() {
if (this._selectedNodes.length && this._parent) {
var i = 0;
var a = 0;
var b = 0;
this._undoManager && this._undoManager.beginGroup('Combined Nodes into Composite');
// Create undo events for removing the selected nodes.
for (i = 0; i < this._selectedNodes.length; ++i) {
this.__onDestroyNode(this._selectedNodes[i]);
// Now give this node a new ID so it is treated like a different node.
this._selectedNodes[i].id = this._engine.__nextNodeId();
}
var compNode = new wcPlayNodes.wcNodeCompositeScript(this._parent, {x: 0, y: 0}, this._selectedNodes);
// Calculate the bounding box of all moved nodes.
var boundList = [];
var offsetList = [];
var targetNode = null;
var targetName = null;
var linkName = null;
var linkNode = null;
var node = null;
for (i = 0; i < this._selectedNodes.length; ++i) {
node = this._selectedNodes[i];
boundList.push(node._meta.bounds.farRect);
offsetList.push(node.pos);
}
var bounds = this.__expandRect(boundList, offsetList);
for (i = 0; i < this._selectedNodes.length; ++i) {
node = this._selectedNodes[i];
// The node was already moved to the composite node, now remove it from the parent object.
this._parent.__removeNode(node);
// Find all chains that connect to an external node.
var entryChains = node.listEntryChains(undefined, this._selectedNodes);
var exitChains = node.listExitChains(undefined, this._selectedNodes);
var inputChains = node.listInputChains(undefined, this._selectedNodes);
var outputChains = node.listOutputChains(undefined, this._selectedNodes);
// External entry chains.
var createdLinks = [];
for (a = 0; a < entryChains.length; ++a) {
targetNode = this._engine.nodeById(entryChains[a].outNodeId);
targetName = entryChains[a].outName;
linkName = entryChains[a].inName;
node = this._engine.nodeById(entryChains[a].inNodeId);
// Make sure we only create one Composite Entry per link.
linkNode = null;
for (b = 0; b < createdLinks.length; ++b) {
if (createdLinks[b].name === linkName) {
linkNode = createdLinks[b].node;
break;
}
}
if (!linkNode) {
// Create a Composite Entry Node, this acts as a surrogate entry link for the Composite node.
linkNode = new wcPlayNodes.wcNodeCompositeEntry(compNode, {x: node.pos.x, y: bounds.top - 100}, linkName);
createdLinks.push({
name: linkName,
node: linkNode
});
}
linkNode.connectExit('out', node, linkName);
compNode.connectEntry(linkNode.name, targetNode, targetName);
targetNode.disconnectExit(targetName, node, linkName);
}
// External exit chains.
createdLinks = [];
for (a = 0; a < exitChains.length; ++a) {
targetNode = this._engine.nodeById(exitChains[a].inNodeId);
targetName = exitChains[a].inName;
linkName = exitChains[a].outName;
node = this._engine.nodeById(exitChains[a].outNodeId);
// Make sure we only create one Composite Entry per link.
linkNode = null;
for (b = 0; b < createdLinks.length; ++b) {
if (createdLinks[b].name === linkName) {
linkNode = createdLinks[b].node;
break;
}
}
if (!linkNode) {
// Create a Composite Exit Node, this acts as a surrogate exit link for the Composite node.
linkNode = new wcPlayNodes.wcNodeCompositeExit(compNode, {x: node.pos.x, y: bounds.top + bounds.height + 50}, linkName);
createdLinks.push({
name: linkName,
node: linkNode
});
}
linkNode.connectEntry('in', node, linkName);
compNode.connectExit(linkNode.name, targetNode, targetName);
targetNode.disconnectEntry(targetName, node, linkName);
}
// External property input chains.
createdLinks = [];
for (a = 0; a < inputChains.length; ++a) {
targetNode = this._engine.nodeById(inputChains[a].outNodeId);
targetName = inputChains[a].outName;
linkName = inputChains[a].inName;
node = this._engine.nodeById(inputChains[a].inNodeId);
// Make sure we only create one Composite Entry per link.
linkNode = null;
for (b = 0; b < createdLinks.length; ++b) {
if (createdLinks[b].name === linkName) {
linkNode = createdLinks[b].node;
break;
}
}
if (!linkNode) {
// Create a Composite Property Node, this acts as a surrogate property link for the Composite node.
linkNode = new wcPlayNodes.wcNodeCompositeProperty(compNode, {x: bounds.left - 200, y: node.pos.y}, linkName);
createdLinks.push({
name: linkName,
node: linkNode
});
}
linkNode.connectOutput('value', node, linkName);
compNode.connectInput(linkNode.name, targetNode, targetName);
targetNode.disconnectOutput(targetName, node, linkName);
}
// External property output chains.
createdLinks = [];
for (a = 0; a < outputChains.length; ++a) {
targetNode = this._engine.nodeById(outputChains[a].inNodeId);
targetName = outputChains[a].inName;
linkName = outputChains[a].outName;
node = this._engine.nodeById(outputChains[a].outNodeId);
// Make sure we only create one Composite Entry per link.
linkNode = null;
for (b = 0; b < createdLinks.length; ++b) {
if (createdLinks[b].name === linkName) {
linkNode = createdLinks[b].node;
break;
}
}
if (!linkNode) {
// Create a Composite Property Node, this acts as a surrogate property link for the Composite node.
linkNode = new wcPlayNodes.wcNodeCompositeProperty(compNode, {x: bounds.left + bounds.width + 200, y: node.pos.y}, linkName);
createdLinks.push({
name: linkName,
node: linkNode
});
}
linkNode.connectInput('value', node, linkName);
compNode.connectOutput(linkNode.name, targetNode, targetName);
targetNode.disconnectInput(targetName, node, linkName);
}
}
this._selectedNode = null;
this._selectedNodes = [];
compNode.pos.x = bounds.left + bounds.width/2;
compNode.pos.y = bounds.top + bounds.height/2;
// Compile the meta data for this node based on the nodes inside.
// compNode.compile();
// Create undo event for creating the composite node.
this.__onCreateNode(compNode);
this._undoManager && this._undoManager.endGroup();
this.__setupPalette();
}
},
/**
* Generates an undo event for a node that was created.
* @function wcPlayEditor#__onCreateNode
* @private
* @param {wcNode} node - The node that was created.
*/
__onCreateNode: function(node) {
this._undoManager && this._undoManager.addEvent('Created Node "' + node.category + '.' + node.type + '"', {
id: node.id,
className: node.className,
data: node.export(),
engine: this._engine,
parent: this._parent.id || this._parent
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
// If we are viewing a script inside the node that is being removed, re-direct our view to its parents.
for (var i = 0; i < this.engine._editors.length; ++i) {
var parent = this.engine._editors[i]._parent;
while (!(parent && parent.instanceOf('wcPlay'))) {
if (parent == myNode) {
this.engine._editors[i]._parent = myNode._parent;
this.engine._editors[i].center();
break;
}
parent = parent._parent;
}
}
// Now destroy this node.
myNode.destroy();
},
// Redo
function() {
var parent = this.parent;
if (typeof parent === 'number') {
parent = this.engine.nodeById(parent);
}
var myNode = new window.wcPlayNodes[this.className](parent, this.data.pos);
myNode.id = this.id;
myNode.import(this.data);
});
},
/**
* Generates an undo event for a node that is destroyed.
* @function wcPlayEditor#__onDestroyNode
* @private
* @param {wcNode} node - The node to destroy.
*/
__onDestroyNode: function(node) {
this._undoManager && this._undoManager.addEvent('', {
data: node.export(),
parent: this._parent,
engine: this._engine
},
// Undo
function() {
var myNode = new window.wcPlayNodes[this.data.className](this.parent, this.data.pos);
myNode.import(this.data);
},
// Redo
function() {
var myNode = this.engine.nodeById(this.data.id);
// If we are viewing a script inside the node that is being removed, re-direct our view to its parents.
for (var i = 0; i < this.engine._editors.length; ++i) {
var parent = this.engine._editors[i]._parent;
while (!(parent && parent.instanceOf('wcPlay'))) {
if (parent == myNode) {
this.engine._editors[i]._parent = myNode._parent;
this.engine._editors[i].center();
break;
}
parent = parent._parent;
}
}
// Now destroy this node.
myNode.destroy();
});
},
/**
* Handles auto scrolling based on mouse position.
* @function wcPlayEditor#__handleAutoScroll
* @private
* @param {boolean} active - Whether the auto scroll is active.
* @param {boolean} movingNodes - If true, the auto scroll will also move selected nodes.
*/
__handleAutoScroll: function(active, movingNodes) {
var shouldBeActive = false;
active = active && this._mouseMoved; // Don't scroll if user has not moved the mouse at all.
var width = this.$viewport.width();
var height = this.$viewport.height();
var THRESHOLD = Math.min(100, width/2, height/2);
var SPEED = 0.15;
this._autoScrollNodes = movingNodes;
if (active) {
if (this._mouse.x >= width - THRESHOLD) {
shouldBeActive = true;
this._autoScrollDirection.x = this._mouse.x - width + THRESHOLD;
this._autoScrollDirection.y = 0;
} else if (this._mouse.x <= THRESHOLD) {
shouldBeActive = true;
this._autoScrollDirection.x = this._mouse.x - THRESHOLD;
this._autoScrollDirection.y = 0;
}
if (this._mouse.y >= height - THRESHOLD) {
shouldBeActive = true;
this._autoScrollDirection.x = 0;
this._autoScrollDirection.y = this._mouse.y - height + THRESHOLD;
} else if (this._mouse.y <= THRESHOLD) {
shouldBeActive = true;
this._autoScrollDirection.x = 0;
this._autoScrollDirection.y = this._mouse.y - THRESHOLD;
}
}
if (shouldBeActive && !this._autoScrollInterval) {
var self = this;
this._autoScrollInterval = setInterval(function() {
var moveX = self._autoScrollDirection.x * SPEED;
var moveY = self._autoScrollDirection.y * SPEED;
self._viewportCamera.x -= moveX;
self._viewportCamera.y -= moveY;
if (self._autoScrollNodes) {
for (var i = 0; i < self._selectedNodes.length; ++i) {
var node = self._selectedNodes[i];
var oldPos = {
x: node.pos.x,
y: node.pos.y
};
var newPos = {
x: node.pos.x + (moveX / self._viewportCamera.z),
y: node.pos.y + (moveY / self._viewportCamera.z)
};
newPos = node.onMoving(oldPos, newPos) || newPos;
if (oldPos.x !== newPos.x || oldPos.y !== newPos.y) {
node.pos.x = newPos.x;
node.pos.y = newPos.y;
node.onMoved(oldPos, newPos);
}
}
}
}, 10);
} else if (this._autoScrollInterval && !shouldBeActive) {
clearInterval(this._autoScrollInterval);
this._autoScrollInterval = 0;
}
},
/**
* Initializes user control.
* @function wcPlayEditor#__setupControls
* @private
*/
__setupControls: function() {
var self = this;
// Menu
this.__bindMenuHandlers();
// Palette
// this.$palette.on('mousemove', function(event){self.__onPaletteMouseMove(event, this);});
// this.$palette.on('mousedown', function(event){self.__onPaletteMouseDown(event, this);});
// this.$palette.on('mouseup', function(event){self.__onPaletteMouseUp(event, this);});
// Viewport
this.$viewport.on('mousemove', function(event) { self.__onViewportMouseMove(event, this); });
this.$viewport.on('mousedown', function(event) { self.__onViewportMouseDown(event, this); });
this.$viewport.on('mouseup', function(event) { self.__onViewportMouseUp(event, this); });
this.$viewport.on('mouseenter', function(event) { self.__onViewportMouseEnter(event, this); });
this.$viewport.on('mouseleave', function(event) { self.__onViewportMouseLeave(event, this); });
this.$viewport.on('click', function(event) { self.__onViewportMouseClick(event, this); });
this.$viewport.on('dblclick', function(event) { self.__onViewportMouseDoubleClick(event, this); });
this.$viewport.on('mousewheel DOMMouseScroll', function(event) { self.__onViewportMouseWheel(event, this); });
},
/**
* Binds click event handlers to each of the options in the menu and toolbar.
* @function wcPlayEditor#__bindMenuHandlers
* @private
*/
__bindMenuHandlers: function() {
var self = this;
// Import the contents of a file.
function __importScriptFile(file, importing) {
if (importing) {
self.triggerEvent('onBeforeImport', []);
} else {
self.triggerEvent('onBeforeLoad', []);
}
var reader = new FileReader();
reader.onload = function(e) {
if (self._engine) {
if (importing) {
// Import the script as its own Composite node.
if (self._engine.import(e.target.result, file.name)) {
self.__setupPalette();
self.triggerEvent('onImported', []);
self.$typeButton[3].click();
}
} else {
if (self._engine.load(e.target.result)) {
self._parent = self._engine;
self._selectedNode = null;
self._selectedNodes = [];
self._undoManager && self._undoManager.clear();
self.center();
self.triggerEvent('onLoaded', []);
} else {
alert('Failed to open file "' + file.name + '"\nPlease check to ensure it is actually a wcPlay script file.');
}
}
}
};
reader.readAsText(file);
}
// A hidden file input field that will handle opening the open file dialog for us.
$('body').on('change', '#wcPlayEditorHiddenFileLoader', function(event) {
if ($(this).hasClass('disabled')) {
return;
}
if (event.target.files.length) {
__importScriptFile(event.target.files[0]);
$(this).val('');
$(this).remove();
}
});
$('body').on('change', '#wcPlayEditorHiddenFileImporter', function(event) {
if ($(this).hasClass('disabled')) {
return;
}
if (event.target.files.length) {
__importScriptFile(event.target.files[0], true);
$(this).val('');
$(this).remove();
}
});
// Support drag-drop over the entire window.
this.$container.on('dragover', function(event) {
event.stopPropagation();
event.preventDefault();
event.originalEvent.dataTransfer.dropEffect = 'copy';
});
this.$container.on('drop', function(event) {
event.stopPropagation();
event.preventDefault();
if (event.originalEvent.dataTransfer.files.length) {
__importScriptFile(event.originalEvent.dataTransfer.files[0], event.ctrlKey);
}
});
},
// /**
// * Handle mouse move events over the palette view.
// * @function wcPlayEditor#__onPaletteMouseMove
// * @private
// * @param {Object} event - The mouse event.
// * @param {Object} elem - The target element.
// */
// __onPaletteMouseMove: function(event, elem) {
// var mouse = this.__mouse(event);
// this._highlightTitle = false;
// this._highlightDetails = false;
// this._highlightDebugLog = false;
// this._highlightBreakpoint = false;
// this._highlightEntryLink = false;
// this._highlightExitLink = false;
// this._highlightInputLink = false;
// this._highlightOutputLink = false;
// this._highlightPropertyValue = false;
// this._highlightPropertyInitialValue = false;
// this._highlightViewport = false;
// // Dragging a node from the palette view.
// if (this._draggingNodeData) {
// var pos = {
// x: mouse.gx + this._draggingNodeData.offset.x,
// y: mouse.gy + this._draggingNodeData.offset.y,
// };
// this._draggingNodeData.$canvas.css('left', pos.x).css('top', pos.y);
// return;
// }
// var categoryData = this.__findCategoryAreaAtPos(mouse);
// if (categoryData) {
// var offset = categoryData.$canvas.offset();
// mouse = this.__mouse(event, offset);
// var node = this.__findNodeAtPos(mouse, {x:0,y:0,z:this._drawStyle.palette.scale}, categoryData.nodes);
// if (node) {
// this._highlightNode = node;
// this.$palette.addClass('wcClickable');
// this.$palette.attr('title', 'Create a new instance of this node by dragging this into your script.');
// } else {
// this._highlightNode = null;
// this.$palette.removeClass('wcClickable');
// this.$palette.attr('title', '');
// }
// }
// },
// /**
// * Handle mouse down events over the palette view.
// * @function wcPlayEditor#__onPaletteMouseDown
// * @private
// * @param {Object} event - The mouse event.
// * @param {Object} elem - The target element.
// */
// __onPaletteMouseDown: function(event, elem) {
// if (this._highlightNode) {
// this.__onPaletteMouseUp(event, elem);
// var mouse = this.__mouse(event);
// var rect = this._highlightNode._meta.bounds.rect;
// var categoryData = this.__findCategoryAreaAtPos(mouse);
// if (categoryData) {
// var offset = categoryData.$canvas.offset();
// var screenOffset = this.$container.offset();
// this._draggingNodeData = {
// node: this._highlightNode,
// $canvas: $('<canvas class="wcPlayHoverCanvas">'),
// offset: {x: 0, y: 0}
// };
// this.$container.append(this._draggingNodeData.$canvas);
// this.$palette.addClass('wcMoving');
// this.$viewport.addClass('wcMoving');
// this._draggingNodeData.$canvas.css('left', this._highlightNode.pos.x + rect.left + offset.left - screenOffset.left)
// .css('top', this._highlightNode.pos.y + rect.top + offset.top - screenOffset.top);
// this._draggingNodeData.$canvas.attr('width', rect.width).css('width', rect.width);
// this._draggingNodeData.$canvas.attr('height', rect.height).css('height', rect.height);
// this._draggingNodeData.offset.x = (this._highlightNode.pos.x * this._drawStyle.palette.scale + rect.left + offset.left - screenOffset.left) - mouse.x;
// this._draggingNodeData.offset.y = (this._highlightNode.pos.y * this._drawStyle.palette.scale + rect.top + offset.top - screenOffset.top) - mouse.y;
// var yPos = 0;
// if (!this._highlightNode.chain.entry.length) {
// yPos += this._drawStyle.links.length;
// }
// this._highlightNode.pos.x = rect.width/2;
// this._highlightNode.pos.y = yPos+3;
// this.__drawNode(this._highlightNode, this._draggingNodeData.$canvas[0].getContext('2d'), true);
// }
// }
// },
// /**
// * Handle mouse up events over the palette view.
// * @function wcPlayEditor#__onPaletteMouseDown
// * @private
// * @param {Object} event - The mouse event.
// * @param {Object} elem - The target element.
// */
// __onPaletteMouseUp: function(event, elem) {
// if (this._draggingNodeData) {
// this._draggingNodeData.$canvas.remove();
// this._draggingNodeData.$canvas = null;
// this._draggingNodeData = null;
// this.$palette.removeClass('wcMoving');
// this.$viewport.removeClass('wcMoving');
// }
// },
/**
* Handle mouse move events over the viewport canvas.
* @function wcPlayEditor#__onViewportMouseMove
* @private
* @param {Object} event - The mouse event.
* @param {Object} elem - The target element.
*/
__onViewportMouseMove: function(event, elem) {
elem;
var mouse = this.__mouse(event, this.$viewport.offset());
if (mouse.x !== this._mouse.x || mouse.y !== this._mouse.y) {
this._mouseMoved = true;
}
// // Dragging a node from the palette view.
// if (this._draggingNodeData) {
// var pos = {
// x: mouse.gx + this._draggingNodeData.offset.x,
// y: mouse.gy + this._draggingNodeData.offset.y,
// };
// this._draggingNodeData.$canvas.css('left', pos.x).css('top', pos.y);
// this._mouse = mouse;
// this.__handleAutoScroll(true);
// return;
// }
var self = this, i = 0, a = 0, moveX = 0, moveY = 0, node = null, rect = null, link = null;
var canConnect = true, myOptions = null, targetOptions = null;
function __nodesInRect(nodes) {
for (var i = 0; i < nodes.length; ++i) {
if (self.__rectOnRect(nodes[i]._meta.bounds.inner, self._highlightRect, nodes[i].pos)) {
self._selectedNodes.push(nodes[i]);
}
}
}
// Box selection.
if (this._highlightRect && this._parent) {
this._highlightRect.x = ((mouse.x - this._viewportCamera.x) / this._viewportCamera.z) - this._highlightRect.ox;
this._highlightRect.y = ((mouse.y - this._viewportCamera.y) / this._viewportCamera.z) - this._highlightRect.oy;
this._highlightRect.width = this._highlightRect.x;
this._highlightRect.height = this._highlightRect.y;
if (this._highlightRect.width < 0) {
this._highlightRect.left = this._highlightRect.ox + this._highlightRect.width;
this._highlightRect.width *= -1;
}
if (this._highlightRect.height < 0) {
this._highlightRect.top = this._highlightRect.oy + this._highlightRect.height;
this._highlightRect.height *= -1;
}
this._selectedNodes = [];
__nodesInRect(this._parent._storageNodes);
__nodesInRect(this._parent._compositeNodes);
__nodesInRect(this._parent._processNodes);
__nodesInRect(this._parent._entryNodes);
this._mouse = mouse;
this.__handleAutoScroll(true);
return;
}
// Viewport panning.
if (this._viewportMoving) {
moveX = mouse.x - this._mouse.x;
moveY = mouse.y - this._mouse.y;
this._viewportCamera.x += moveX;
this._viewportCamera.y += moveY;
this._mouse = mouse;
if (!this._viewportMoved && this._mouseMoved) {
this._viewportMoved = true;
this.$viewport.addClass('wcMoving');
}
this.__handleAutoScroll(false);
return;
}
// Moving nodes
if (this._viewportMovingNode) {
moveX = mouse.x - this._mouse.x;
moveY = mouse.y - this._mouse.y;
for (i = 0; i < this._selectedNodes.length; ++i) {
node = this._selectedNodes[i];
var oldPos = {
x: node.pos.x,
y: node.pos.y
};
var newPos = {
x: node.pos.x + (moveX / this._viewportCamera.z),
y: node.pos.y + (moveY / this._viewportCamera.z)
};
newPos = node.onMoving(oldPos, newPos) || newPos;
if (oldPos.x !== newPos.x || oldPos.y !== newPos.y) {
node.pos.x = newPos.x;
node.pos.y = newPos.y;
node.onMoved(oldPos, newPos);
}
}
this._mouse = mouse;
this.__handleAutoScroll(true, true);
return;
}
this._mouse = mouse;
this._highlightNode = null;
this._highlightCrumb = -1;
this._highlightTitle = false;
this._highlightDetails = false;
this._highlightDebugLog = false;
this._highlightBreakpoint = false;
this._highlightEntryLink = false;
this._highlightExitLink = false;
this._highlightInputLink = false;
this._highlightOutputLink = false;
this._highlightPropertyValue = false;
this._highlightPropertyInitialValue = false;
this.__handleAutoScroll(this._selectedEntryLink || this._selectedExitLink || this._selectedInputLink || this._selectedOutputLink);
var wasOverViewport = this._highlightViewport;
this._highlightViewport = false;
this.$viewport.removeClass('wcClickable wcMoving wcGrab wcNoDrop');
this.$viewport.attr('title', '');
for (i = 0; i < this._crumbBounds.length; ++i) {
if (this.__inRect(mouse, this._crumbBounds[i].rect)) {
this._highlightCrumb = i;
this.$viewport.addClass('wcClickable');
this.$viewport.attr('title', 'Click to go to this level in the hierarchy.');
break;
}
}
node = null;
if (this._highlightCrumb === -1) {
node = this.__findNodeAtPos(mouse, this._viewportCamera);
}
if (node) {
// Check for main node collision.
if (!this._options.readOnly && this.__inRect(mouse, node._meta.bounds.farRect, node.pos, this._viewportCamera)) {
this._highlightNode = node;
// if (this.__inRect(mouse, node._meta.bounds.inner, node.pos, this._viewportCamera)) {
this.$viewport.attr('title', (node._meta.description? node._meta.description + '\n': ''));
this.$viewport.addClass('wcMoving');
// }
}
if (!this._selectedEntryLink && !this._selectedExitLink && !this._selectedInputLink && !this._selectedOutputLink) {
// Debug Log button.
if (this.__inRect(mouse, node._meta.bounds.debugLog, node.pos, this._viewportCamera)) {
this._highlightDebugLog = true;
this._highlightNode = node;
this.$viewport.addClass('wcClickable');
if (node._log) {
this.$viewport.attr('title', 'Disable debug logging for this node.');
} else {
this.$viewport.attr('title', 'Enable debug logging for this node.');
}
}
// Breakpoint button.
if (this.__inRect(mouse, node._meta.bounds.breakpoint, node.pos, this._viewportCamera)) {
this._highlightBreakpoint = true;
this._highlightNode = node;
this.$viewport.addClass('wcClickable');
this.$viewport.attr('title', 'Toggle debug breakpoint on this node.');
}
}
// Entry links.
if (!this._options.readOnly && !this._selectedEntryLink && !this._selectedInputLink && !this._selectedOutputLink) {
rect = this._selectedExitLink? 'longRect': 'rect';
for (i = 0; i < node._meta.bounds.entryBounds.length; ++i) {
if (this.__inRect(mouse, node._meta.bounds.entryBounds[i][rect], node.pos, this._viewportCamera)) {
this._highlightNode = node;
this._highlightEntryLink = node._meta.bounds.entryBounds[i];
link = null;
for (a = 0; a < node.chain.entry.length; ++a) {
if (node.chain.entry[a].name == this._highlightEntryLink.name) {
link = node.chain.entry[a];
break;
}
}
this.$viewport.attr('title', (link.meta.description? link.meta.description + '\n': '') + 'Click and drag to create a new flow chain from another node. Double click to manually fire this entry link.');
this.$viewport.addClass('wcGrab');
break;
}
}
}
// Exit links.
if (!this._options.readOnly && !this._selectedExitLink && !this._selectedInputLink && !this._selectedOutputLink) {
rect = this._selectedEntryLink? 'longRect': 'rect';
for (i = 0; i < node._meta.bounds.exitBounds.length; ++i) {
if (this.__inRect(mouse, node._meta.bounds.exitBounds[i][rect], node.pos, this._viewportCamera)) {
this._highlightNode = node;
this._highlightExitLink = node._meta.bounds.exitBounds[i];
link = null;
for (a = 0; a < node.chain.exit.length; ++a) {
if (node.chain.exit[a].name == this._highlightExitLink.name) {
link = node.chain.exit[a];
break;
}
}
this.$viewport.attr('title', (link.meta.description? link.meta.description + '\n': '') + 'Click and drag to create a new flow chain to another node. Double click to manually fire this exit link.');
this.$viewport.addClass('wcGrab');
break;
}
}
}
// Input links.
if (!this._options.readOnly && !this._selectedEntryLink && !this._selectedExitLink && !this._selectedInputLink) {
rect = this._selectedOutputLink? 'longRect': 'rect';
for (i = 0; i < node._meta.bounds.inputBounds.length; ++i) {
if (this.__inRect(mouse, node._meta.bounds.inputBounds[i][rect], node.pos, this._viewportCamera)) {
canConnect = true;
this._highlightNode = node;
if (this._selectedOutputLink) {
// Test for connectivity
myOptions = this._selectedNode.propertyOptions(this._selectedOutputLink.name);
targetOptions = node.propertyOptions(node._meta.bounds.inputBounds[i].name);
if ((myOptions.outputCondition && !myOptions.outputCondition.call(this._selectedNode, node, node._meta.bounds.inputBounds[i].name)) ||
(targetOptions.inputCondition && !targetOptions.inputCondition.call(node, this._selectedNode, this._selectedOutputLink.name))) {
canConnect = false;
}
}
if (canConnect) {
this._highlightInputLink = node._meta.bounds.inputBounds[i];
this.$viewport.attr('title', 'Click and drag to chain this property to another.');
this.$viewport.addClass('wcGrab');
} else {
this.$viewport.addClass('wcNoDrop');
}
break;
}
}
}
// Output links.
if (!this._options.readOnly && !this._selectedEntryLink && !this._selectedExitLink && !this._selectedOutputLink) {
rect = this._selectedInputLink? 'longRect': 'rect';
for (i = 0; i < node._meta.bounds.outputBounds.length; ++i) {
if (this.__inRect(mouse, node._meta.bounds.outputBounds[i][rect], node.pos, this._viewportCamera)) {
canConnect = true;
this._highlightNode = node;
if (this._selectedInputLink) {
// Test for connectivity
myOptions = this._selectedNode.propertyOptions(this._selectedInputLink.name);
targetOptions = node.propertyOptions(node._meta.bounds.outputBounds[i].name);
if ((myOptions.inputCondition && !myOptions.inputCondition.call(this._selectedNode, node, node._meta.bounds.outputBounds[i].name)) ||
(targetOptions.outputCondition && !targetOptions.outputCondition.call(node, this._selectedNode, this._selectedInputLink.name))) {
canConnect = false;
}
}
if (canConnect) {
this._highlightOutputLink = node._meta.bounds.outputBounds[i];
this.$viewport.attr('title', 'Click and drag to chain this property to another. Double click to send its value through the chain.');
this.$viewport.addClass('wcGrab');
} else {
this.$viewport.addClass('wcNoDrop');
}
break;
}
}
}
if (!this._selectedEntryLink && !this._selectedExitLink && !this._selectedInputLink && !this._selectedOutputLink) {
if (!this._options.readOnly) {
// Title label.
if (this.__inRect(this._mouse, node._meta.bounds.titleBounds, node.pos, this._viewportCamera)) {
this._highlightNode = node;
this._highlightTitle = true;
this.$viewport.attr('title', 'Click to add or modify an additional label for this title.');
this.$viewport.addClass('wcClickable');
}
// Details button.
if (this.__inRect(this._mouse, node._meta.bounds.detailsBounds, node.pos, this._viewportCamera)) {
this._highlightNode = node;
this._highlightDetails = true;
this.$viewport.attr('title', 'Click to see futher details for this node.');
this.$viewport.addClass('wcClickable');
}
// Property labels.
var propBounds;
for (i = 0; i < node._meta.bounds.propertyBounds.length; ++i) {
if (this.__inRect(this._mouse, node._meta.bounds.propertyBounds[i].rect, node.pos, this._viewportCamera)) {
propBounds = node._meta.bounds.propertyBounds[i];
break;
}
}
if (propBounds) {
for (i = 0; i < node.properties.length; ++i) {
if (node.properties[i].name === propBounds.name) {
this.$viewport.attr('title', (node.properties[i].options.description? node.properties[i].options.description + '\n': ''));
break;
}
}
}
// Property values.
var valueBounds;
for (i = 0; i < node._meta.bounds.valueBounds.length; ++i) {
if (this.__inRect(this._mouse, node._meta.bounds.valueBounds[i].rect, node.pos, this._viewportCamera)) {
valueBounds = node._meta.bounds.valueBounds[i];
break;
}
}
if (valueBounds) {
for (i = 0; i < node.properties.length; ++i) {
if (node.properties[i].name === valueBounds.name) {
this._highlightNode = node;
this._highlightPropertyValue = valueBounds;
this.$viewport.attr('title', (node.properties[i].options.description? node.properties[i].options.description + '\n': '') + 'Click to change the current value of this property.\nValue = "' + node.properties[i].value + '"\n');
if (!node.properties[i].options.readOnly) {
this.$viewport.addClass('wcClickable');
}
break;
}
}
}
// Property initial values.
var initialBounds;
for (i = 0; i < node._meta.bounds.initialBounds.length; ++i) {
if (this.__inRect(this._mouse, node._meta.bounds.initialBounds[i].rect, node.pos, this._viewportCamera)) {
initialBounds = node._meta.bounds.initialBounds[i];
break;
}
}
if (initialBounds) {
for (i = 0; i < node.properties.length; ++i) {
if (node.properties[i].name === initialBounds.name) {
this._highlightNode = node;
this._highlightPropertyInitialValue = initialBounds;
this.$viewport.attr('title', (node.properties[i].options.description? node.properties[i].options.description + '\n': '') + 'Click to change the initial value of this property.\nValue = "' + node.properties[i].initialValue + '"\n');
if (!node.properties[i].options.readOnly) {
this.$viewport.addClass('wcClickable');
}
break;
}
}
}
}
// Custom viewport area.
if (node._meta.bounds.viewportBounds) {
var pos = {
x: (mouse.x - this._viewportCamera.x) / this._viewportCamera.z - (node.pos.x + node._meta.bounds.viewportBounds.left),
y: (mouse.y - this._viewportCamera.y) / this._viewportCamera.z - (node.pos.y + node._meta.bounds.viewportBounds.top)
};
if (this.__inRect(this._mouse, node._meta.bounds.viewportBounds, node.pos, this._viewportCamera)) {
this._highlightNode = node;
this._highlightViewport = true;
this.$viewport.addClass('wcClickable');
if (!wasOverViewport) {
node.onViewportMouseEnter(event, pos, this._options.readOnly);
}
node.onViewportMouseMove(event, pos, this._options.readOnly);
} else if (wasOverViewport) {
node.onViewportMouseLeave(event, pos, this._options.readOnly);
}
}
}
}
},
/**
* Handle mouse press events over the viewport canvas.
* @function wcPlayEditor#__onViewportMouseDown
* @private
* @param {Object} event - The mouse event.
* @param {Object} elem - The target element.
*/
__onViewportMouseDown: function(event, elem) {
elem;
this._mouse = this.__mouse(event, this.$viewport.offset());
this._mouseMoved = false;
var i = 0;
// Control+drag or middle+drag to box select.
if (event.ctrlKey || this._mouse.which === 2) {
this._highlightRect = {
top: (this._mouse.y - this._viewportCamera.y) / this._viewportCamera.z,
left: (this._mouse.x - this._viewportCamera.x) / this._viewportCamera.z,
oy: (this._mouse.y - this._viewportCamera.y) / this._viewportCamera.z,
ox: (this._mouse.x - this._viewportCamera.x) / this._viewportCamera.z,
x: 0,
y: 0,
width: 0,
height: 0
};
return;
}
var hasTarget = false;
var node = this.__findNodeAtPos(this._mouse, this._viewportCamera);
if (node) {
// Entry links.
if (!hasTarget && !this._options.readOnly) {
for (i = 0; i < node._meta.bounds.entryBounds.length; ++i) {
if (this.__inRect(this._mouse, node._meta.bounds.entryBounds[i].rect, node.pos, this._viewportCamera)) {
hasTarget = true;
// Alt click to disconnect all chains from this link.
if (event.altKey) {
this.onDisconnectEntryChains(node, node._meta.bounds.entryBounds[i].name);
break;
}
this._selectedNode = node;
this._selectedNodes = [node];
this._selectedEntryLink = node._meta.bounds.entryBounds[i];
this.$viewport.addClass('wcGrabbing');
break;
}
}
}
// Exit links.
if (!hasTarget && !this._options.readOnly) {
for (i = 0; i < node._meta.bounds.exitBounds.length; ++i) {
if (this.__inRect(this._mouse, node._meta.bounds.exitBounds[i].rect, node.pos, this._viewportCamera)) {
hasTarget = true;
// Alt click to disconnect all chains from this link.
if (event.altKey) {
this.onDisconnectExitChains(node, node._meta.bounds.exitBounds[i].name);
break;
}
// Shift click to manually fire this exit chain.
else if (event.shiftKey) {
node.activateExit(node._meta.bounds.exitBounds[i].name);
break;
}
this._selectedNode = node;
this._selectedNodes = [node];
this._selectedExitLink = node._meta.bounds.exitBounds[i];
this.$viewport.addClass('wcGrabbing');
break;
}
}
}
// Input links.
if (!hasTarget && !this._options.readOnly) {
for (i = 0; i < node._meta.bounds.inputBounds.length; ++i) {
if (this.__inRect(this._mouse, node._meta.bounds.inputBounds[i].rect, node.pos, this._viewportCamera)) {
hasTarget = true;
// Alt click to disconnect all chains from this link.
if (event.altKey) {
this.onDisconnectInputChains(node, node._meta.bounds.inputBounds[i].name);
break;
}
this._selectedNode = node;
this._selectedNodes = [node];
this._selectedInputLink = node._meta.bounds.inputBounds[i];
this.$viewport.addClass('wcGrabbing');
break;
}
}
}
// Output links.
if (!hasTarget && !this._options.readOnly) {
for (i = 0; i < node._meta.bounds.outputBounds.length; ++i) {
if (this.__inRect(this._mouse, node._meta.bounds.outputBounds[i].rect, node.pos, this._viewportCamera)) {
hasTarget = true;
// Alt click to disconnect all chains from this link.
if (event.altKey) {
this.onDisconnectOutputChains(node, node._meta.bounds.outputBounds[i].name);
break;
}
this._selectedNode = node;
this._selectedNodes = [node];
this._selectedOutputLink = node._meta.bounds.outputBounds[i];
this.$viewport.addClass('wcGrabbing');
break;
}
}
}
// Custom viewport area.
if (!hasTarget && node._meta.bounds.viewportBounds) {
var pos = {
x: (this._mouse.x - this._viewportCamera.x) / this._viewportCamera.z - node._meta.bounds.viewportBounds.left,
y: (this._mouse.y - this._viewportCamera.y) / this._viewportCamera.z - node._meta.bounds.viewportBounds.top
};
if (this.__inRect(this._mouse, node._meta.bounds.viewportBounds, node.pos, this._viewportCamera)) {
this._selectedNode = node;
this._selectedNodes = [node];
if (node.onViewportMouseDown(event, pos, this._options.readOnly)) {
hasTarget = true;
}
}
}
// Center area.
if (!hasTarget && this.__inRect(this._mouse, node._meta.bounds.farRect, node.pos, this._viewportCamera)) {
hasTarget = true;
if (!this._selectedNodes.length || this._selectedNodes.indexOf(node) === -1) {
this._selectedNode = node;
this._selectedNodes = [node];
}
this._viewportMovingNode = !this._options.readOnly;
this._selectedNodeOrigins = [];
for (i = 0; i < this._selectedNodes.length; ++i) {
var myNode = this._selectedNodes[i];
this._selectedNodeOrigins.push({
x: myNode.pos.x,
y: myNode.pos.y
});
}
}
}
// Click outside of a node begins the canvas drag process.
if (!hasTarget) {
this._viewportMoving = true;
this._viewportMoved = false;
}
},
/**
* Handle mouse release events over the viewport canvas.
* @function wcPlayEditor#__onViewportMouseUp
* @private
* @param {Object} event - The mouse event.
* @param {Object} elem - The target element.
*/
__onViewportMouseUp: function(event, elem) {
elem;
this.$viewport.removeClass('wcGrabbing');
var i = 0;
// if (this._draggingNodeData && event.type === 'mouseup') {
// // Create an instance of the node and add it to the script.
// var screenOffset = this.$container.offset();
// var mouse = this.__mouse(event, this.$viewport.offset(), this._viewportCamera);
// var newNode = new window.wcPlayNodes[this._draggingNodeData.node.className](this._parent, {x: 0, y: 0});
// var data = this._draggingNodeData.node.export();
// data.id = newNode.id;
// data.pos.x = (mouse.x / this._viewportCamera.z) + (this._draggingNodeData.$canvas.width()/2 + this._draggingNodeData.offset.x + screenOffset.left);
// data.pos.y = (mouse.y / this._viewportCamera.z) + (this._draggingNodeData.offset.y + 5 + screenOffset.top);
// if (!newNode.chain.entry.length) {
// data.y += this._drawStyle.links.length;
// }
// newNode.import(data, []);
// this.__onCreateNode(newNode);
// this._selectedNode = newNode;
// this._selectedNodes = [newNode];
// this.__updateNode(newNode, 0, this._viewportContext);
// this.__drawNode(newNode, this._viewportContext);
// this._draggingNodeData.$canvas.remove();
// this._draggingNodeData.$canvas = null;
// this._draggingNodeData = null;
// this.$palette.removeClass('wcMoving');
// this.$viewport.removeClass('wcMoving');
// }
if (this._highlightRect && this._parent) {
this._highlightRect = null;
return;
}
// Finished moving a node.
if (this._selectedNodes.length && this._selectedNodeOrigins.length) {
this._undoManager && this._undoManager.beginGroup('Node(s) moved.');
for (i = 0; i < this._selectedNodes.length; ++i) {
var node = this._selectedNodes[i];
if (node.pos.x !== this._selectedNodeOrigins[i].x || node.pos.y !== this._selectedNodeOrigins[i].y) {
node.onMoved({x: this._selectedNodeOrigins[i].x, y: this._selectedNodeOrigins[i].y}, {x: node.pos.x, y: node.pos.y});
this._undoManager && this._undoManager.addEvent('Moved Node "' + node.category + '.' + node.type + '"', {
id: node.id,
start: {
x: this._selectedNodeOrigins[i].x,
y: this._selectedNodeOrigins[i].y
},
end: {
x: node.pos.x,
y: node.pos.y
},
engine: this._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
var pos = myNode.onMoving({x: this.end.x, y: this.end.y}, {x: this.start.x, y: this.start.y}) || this.start;
myNode.pos.x = this.start.x;
myNode.pos.y = this.start.y;
myNode.onMoved({x: this.end.x, y: this.end.y}, {x: pos.x, y: pos.y});
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
var pos = myNode.onMoving({x: this.start.x, y: this.start.y}, {x: this.end.x, y: this.end.y}) || this.end;
myNode.pos.x = this.end.x;
myNode.pos.y = this.end.y;
myNode.onMoved({x: this.start.x, y: this.start.y}, {x: pos.x, y: pos.y});
});
}
}
this._undoManager && this._undoManager.endGroup();
this._selectedNodeOrigins = [];
}
// Check for link connections.
if (this._selectedNode && this._selectedEntryLink && this._highlightNode && this._highlightExitLink) {
if (this._selectedNode.connectEntry(this._selectedEntryLink.name, this._highlightNode, this._highlightExitLink.name) === wcNode.CONNECT_RESULT.ALREADY_CONNECTED) {
this._selectedNode.disconnectEntry(this._selectedEntryLink.name, this._highlightNode, this._highlightExitLink.name);
this._undoManager && this._undoManager.addEvent('Disconnected Entry Link "' + this._selectedNode.category + '.' + this._selectedNode.type + '.' + this._selectedEntryLink.name + '" to Exit Link "' + this._highlightNode.category + '.' + this._highlightNode.type + '.' + this._highlightExitLink.name + '"', {
id: this._selectedNode.id,
name: this._selectedEntryLink.name,
targetId: this._highlightNode.id,
targetName: this._highlightExitLink.name,
engine: this._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
var targetNode = this.engine.nodeById(this.targetId);
myNode.connectEntry(this.name, targetNode, this.targetName);
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
var targetNode = this.engine.nodeById(this.targetId);
myNode.disconnectEntry(this.name, targetNode, this.targetName);
});
} else {
this._undoManager && this._undoManager.addEvent('Connected Entry Link "' + this._selectedNode.category + '.' + this._selectedNode.type + '.' + this._selectedEntryLink.name + '" to Exit Link "' + this._highlightNode.category + '.' + this._highlightNode.type + '.' + this._highlightExitLink.name + '"', {
id: this._selectedNode.id,
name: this._selectedEntryLink.name,
targetId: this._highlightNode.id,
targetName: this._highlightExitLink.name,
engine: this._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
var targetNode = this.engine.nodeById(this.targetId);
myNode.disconnectEntry(this.name, targetNode, this.targetName);
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
var targetNode = this.engine.nodeById(this.targetId);
myNode.connectEntry(this.name, targetNode, this.targetName);
});
}
}
if (this._selectedNode && this._selectedExitLink && this._highlightNode && this._highlightEntryLink) {
if (this._selectedNode.connectExit(this._selectedExitLink.name, this._highlightNode, this._highlightEntryLink.name) === wcNode.CONNECT_RESULT.ALREADY_CONNECTED) {
this._selectedNode.disconnectExit(this._selectedExitLink.name, this._highlightNode, this._highlightEntryLink.name);
this._undoManager && this._undoManager.addEvent('Disconnected Exit Link "' + this._selectedNode.category + '.' + this._selectedNode.type + '.' + this._selectedExitLink.name + '" to Entry Link "' + this._highlightNode.category + '.' + this._highlightNode.type + '.' + this._highlightEntryLink.name + '"', {
id: this._selectedNode.id,
name: this._selectedExitLink.name,
targetId: this._highlightNode.id,
targetName: this._highlightEntryLink.name,
engine: this._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
var targetNode = this.engine.nodeById(this.targetId);
myNode.connectExit(this.name, targetNode, this.targetName);
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
var targetNode = this.engine.nodeById(this.targetId);
myNode.disconnectExit(this.name, targetNode, this.targetName);
});
} else {
this._undoManager && this._undoManager.addEvent('Connected Exit Link "' + this._selectedNode.category + '.' + this._selectedNode.type + '.' + this._selectedExitLink.name + '" to Entry Link "' + this._highlightNode.category + '.' + this._highlightNode.type + '.' + this._highlightEntryLink.name + '"', {
id: this._selectedNode.id,
name: this._selectedExitLink.name,
targetId: this._highlightNode.id,
targetName: this._highlightEntryLink.name,
engine: this._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
var targetNode = this.engine.nodeById(this.targetId);
myNode.disconnectExit(this.name, targetNode, this.targetName);
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
var targetNode = this.engine.nodeById(this.targetId);
myNode.connectExit(this.name, targetNode, this.targetName);
});
}
}
if (this._selectedNode && this._selectedInputLink && this._highlightNode && this._highlightOutputLink) {
if (this._selectedNode.connectInput(this._selectedInputLink.name, this._highlightNode, this._highlightOutputLink.name) === wcNode.CONNECT_RESULT.ALREADY_CONNECTED) {
this._selectedNode.disconnectInput(this._selectedInputLink.name, this._highlightNode, this._highlightOutputLink.name);
this._undoManager && this._undoManager.addEvent('Disconnected Property Input Link "' + this._selectedNode.category + '.' + this._selectedNode.type + '.' + this._selectedInputLink.name + '" to Property Output Link "' + this._highlightNode.category + '.' + this._highlightNode.type + '.' + this._highlightOutputLink.name + '"', {
id: this._selectedNode.id,
name: this._selectedInputLink.name,
targetId: this._highlightNode.id,
targetName: this._highlightOutputLink.name,
engine: this._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
var targetNode = this.engine.nodeById(this.targetId);
myNode.connectInput(this.name, targetNode, this.targetName);
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
var targetNode = this.engine.nodeById(this.targetId);
myNode.disconnectInput(this.name, targetNode, this.targetName);
});
} else {
this._undoManager && this._undoManager.addEvent('Connected Property Input Link "' + this._selectedNode.category + '.' + this._selectedNode.type + '.' + this._selectedInputLink.name + '" to Property Output Link "' + this._highlightNode.category + '.' + this._highlightNode.type + '.' + this._highlightOutputLink.name + '"', {
id: this._selectedNode.id,
name: this._selectedInputLink.name,
targetId: this._highlightNode.id,
targetName: this._highlightOutputLink.name,
engine: this._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
var targetNode = this.engine.nodeById(this.targetId);
myNode.disconnectInput(this.name, targetNode, this.targetName);
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
var targetNode = this.engine.nodeById(this.targetId);
myNode.connectInput(this.name, targetNode, this.targetName);
});
}
}
if (this._selectedNode && this._selectedOutputLink && this._highlightNode && this._highlightInputLink) {
if (this._selectedNode.connectOutput(this._selectedOutputLink.name, this._highlightNode, this._highlightInputLink.name) === wcNode.CONNECT_RESULT.ALREADY_CONNECTED) {
this._selectedNode.disconnectOutput(this._selectedOutputLink.name, this._highlightNode, this._highlightInputLink.name);
this._undoManager && this._undoManager.addEvent('Disconnected Property Output Link "' + this._selectedNode.category + '.' + this._selectedNode.type + '.' + this._selectedOutputLink.name + '" to Property Input Link "' + this._highlightNode.category + '.' + this._highlightNode.type + '.' + this._highlightInputLink.name + '"', {
id: this._selectedNode.id,
name: this._selectedOutputLink.name,
targetId: this._highlightNode.id,
targetName: this._highlightInputLink.name,
engine: this._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
var targetNode = this.engine.nodeById(this.targetId);
myNode.connectOutput(this.name, targetNode, this.targetName);
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
var targetNode = this.engine.nodeById(this.targetId);
myNode.disconnectOutput(this.name, targetNode, this.targetName);
});
} else {
this._undoManager && this._undoManager.addEvent('Connected Property Output Link "' + this._selectedNode.category + '.' + this._selectedNode.type + '.' + this._selectedOutputLink.name + '" to Property Input Link "' + this._highlightNode.category + '.' + this._highlightNode.type + '.' + this._highlightInputLink.name + '"', {
id: this._selectedNode.id,
name: this._selectedOutputLink.name,
targetId: this._highlightNode.id,
targetName: this._highlightInputLink.name,
engine: this._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
var targetNode = this.engine.nodeById(this.targetId);
myNode.disconnectOutput(this.name, targetNode, this.targetName);
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
var targetNode = this.engine.nodeById(this.targetId);
myNode.connectOutput(this.name, targetNode, this.targetName);
});
}
}
// Custom viewport area.
var mouse = null;
if (this._highlightNode && this._highlightViewport) {
mouse = this.__mouse(event, this.$viewport.offset());
var pos = {
x: (mouse.x - this._viewportCamera.x) / this._viewportCamera.z - this._highlightNode._meta.bounds.viewportBounds.left,
y: (mouse.y - this._viewportCamera.y) / this._viewportCamera.z - this._highlightNode._meta.bounds.viewportBounds.top
};
this._highlightNode.onViewportMouseUp(event, pos, this._options.readOnly);
}
var self = this;
function __cleanup() {
self._selectedEntryLink = false;
self._selectedExitLink = false;
self._selectedInputLink = false;
self._selectedOutputLink = false;
self._viewportMovingNode = false;
if (self._viewportMoving) {
self._viewportMoving = false;
if (!self._viewportMoved) {
self._selectedNode = null;
self._selectedNodes = [];
} else {
self._viewportMoved = false;
self.$viewport.removeClass('wcMoving');
}
}
}
if (this._selectedNode && !this._highlightNode && !this._highlightViewport) {
var linkName = null;
var linkType = null;
if (this._selectedEntryLink) {
linkName = this._selectedEntryLink.name;
linkType = wcNode.LINK_TYPE.ENTRY;
}
if (this._selectedExitLink) {
linkName = this._selectedExitLink.name;
linkType = wcNode.LINK_TYPE.EXIT;
}
if (this._selectedInputLink) {
linkName = this._selectedInputLink.name;
linkType = wcNode.LINK_TYPE.INPUT;
}
if (this._selectedOutputLink) {
linkName = this._selectedOutputLink.name;
linkType = wcNode.LINK_TYPE.OUTPUT;
}
if (linkName && linkType) {
mouse = this.__mouse(event, this.$viewport.offset());
this.__drawPalettePopup(mouse, this._selectedNode, linkName, linkType, function(newNode) {
newNode;
__cleanup();
});
return;
}
}
__cleanup();
// Right click on an empty area to show the popup.
if (!this._selectedNode && !this._mouseMoved && this._mouse.which === 3) {
this.__drawPalettePopup(this._mouse);
}
},
/**
* Handle mouse entering the viewport canvas.
* @function wcPlayEditor#__onViewportMouseEnter
* @private
* @param {Object} event - The mouse event.
* @param {Object} elem - The target element.
*/
__onViewportMouseEnter: function(event, elem) {
elem;
this._mouseInViewport = true;
},
/**
* Handle mouse leaving the viewport canvas.
* @function wcPlayEditor#__onViewportMouseLeave
* @private
* @param {Object} event - The mouse event.
* @param {Object} elem - The target element.
*/
__onViewportMouseLeave: function(event, elem) {
this._mouseInViewport = false;
this.__handleAutoScroll(false);
this.__onViewportMouseUp(event, elem);
},
/**
* Handle mouse click events over the viewport canvas.
* @function wcPlayEditor#__onViewportMouseDown
* @private
* @param {Object} event - The mouse event.
* @param {Object} elem - The target element.
*/
__onViewportMouseClick: function(event, elem) {
elem;
var state = false, i = 0;
if (!this._mouseMoved) {
if (this._highlightCrumb > -1) {
if (this._crumbBounds[this._highlightCrumb].parent !== this._parent) {
var focusNode = this._crumbBounds[this._highlightCrumb+1].parent;
this._parent = this._crumbBounds[this._highlightCrumb].parent;
this._selectedNode = focusNode;
this._selectedNodes = [focusNode];
this.focus(this._selectedNodes);
}
return;
}
this._mouse = this.__mouse(event, this.$viewport.offset());
var node = this.__findNodeAtPos(this._mouse, this._viewportCamera);
if (node) {
// Debug Log button.
if (this.__inRect(this._mouse, node._meta.bounds.debugLog, node.pos, this._viewportCamera)) {
state = !node._log;
node.debugLog(state);
this._undoManager && this._undoManager.addEvent((state? 'Enabled': 'Disabled') + ' Debug Logging for Node "' + node.category + '.' + node.type + '"', {
id: node.id,
state: state,
engine: this._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
myNode.debugLog(!this.state);
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
myNode.debugLog(this.state);
});
}
// Breakpoint button.
if (this.__inRect(this._mouse, node._meta.bounds.breakpoint, node.pos, this._viewportCamera)) {
state = !node._break;
node.debugBreak(state);
this._undoManager && this._undoManager.addEvent((state? 'Enabled': 'Disabled') + ' Breakpoint on Node "' + node.category + '.' + node.type + '"', {
id: node.id,
state: state,
engine: this._engine
},
// Undo
function() {
var myNode = this.engine.nodeById(this.id);
myNode.debugBreak(!this.state);
},
// Redo
function() {
var myNode = this.engine.nodeById(this.id);
myNode.debugBreak(this.state);
});
}
// Title label.
if (this.__inRect(this._mouse, node._meta.bounds.titleBounds, node.pos, this._viewportCamera)) {
this.__drawTitleEditor(node, node._meta.bounds.titleBounds);
}
// Details button.
if (this.__inRect(this._mouse, node._meta.bounds.detailsBounds, node.pos, this._viewportCamera)) {
this.__drawDetailsPopup(node);
}
// Property values.
var propBounds;
for (i = 0; i < node._meta.bounds.valueBounds.length; ++i) {
if (this.__inRect(this._mouse, node._meta.bounds.valueBounds[i].rect, node.pos, this._viewportCamera)) {
propBounds = node._meta.bounds.valueBounds[i];
break;
}
}
if (propBounds) {
for (i = 0; i < node.properties.length; ++i) {
if (node.properties[i].name === propBounds.name) {
this.__drawPropertyEditor(node, node.properties[i], propBounds);
break;
}
}
}
var propInitialBounds;
for (i = 0; i < node._meta.bounds.initialBounds.length; ++i) {
if (this.__inRect(this._mouse, node._meta.bounds.initialBounds[i].rect, node.pos, this._viewportCamera)) {
propInitialBounds = node._meta.bounds.initialBounds[i];
break;
}
}
if (propInitialBounds) {
for (i = 0; i < node.properties.length; ++i) {
if (node.properties[i].name === propInitialBounds.name) {
this.__drawPropertyEditor(node, node.properties[i], propInitialBounds, true);
break;
}
}
}
// Custom viewport area.
if (node._meta.bounds.viewportBounds) {
var pos = {
x: (this._mouse.x - this._viewportCamera.x) / this._viewportCamera.z - node._meta.bounds.viewportBounds.left,
y: (this._mouse.y - this._viewportCamera.y) / this._viewportCamera.z - node._meta.bounds.viewportBounds.top
};
if (this.__inRect(this._mouse, node._meta.bounds.viewportBounds, node.pos, this._viewportCamera)) {
node.onViewportMouseClick(event, pos, this._options.readOnly);
}
}
}
}
},
/**
* Handle mouse double click events over the viewport canvas.
* @function wcPlayEditor#__onViewportMouseDoubleClick
* @private
* @param {Object} event - The mouse event.
* @param {Object} elem - The target element.
*/
__onViewportMouseDoubleClick: function(event, elem) {
elem;
var i = 0;
this._mouse = this.__mouse(event, this.$viewport.offset());
var hasTarget = false;
var node = this.__findNodeAtPos(this._mouse, this._viewportCamera);
if (node) {
// Debug Log button.
if (this.__inRect(this._mouse, node._meta.bounds.debugLog, node.pos, this._viewportCamera)) {
hasTarget = true;
}
// Breakpoint button.
if (this.__inRect(this._mouse, node._meta.bounds.breakpoint, node.pos, this._viewportCamera)) {
hasTarget = true;
}
// Property values.
for (i = 0; i < node._meta.bounds.valueBounds.length; ++i) {
if (this.__inRect(this._mouse, node._meta.bounds.valueBounds[i].rect, node.pos, this._viewportCamera)) {
hasTarget = true;
break;
}
}
// Entry links.
if (!this._options.readOnly && !hasTarget) {
for (i = 0; i < node._meta.bounds.entryBounds.length; ++i) {
if (this.__inRect(this._mouse, node._meta.bounds.entryBounds[i].rect, node.pos, this._viewportCamera)) {
hasTarget = true;
// Double click to manually fire this entry chain.
node.activateEntry(node._meta.bounds.entryBounds[i].name);
break;
}
}
}
// Exit links.
if (!this._options.readOnly && !hasTarget) {
for (i = 0; i < node._meta.bounds.exitBounds.length; ++i) {
if (this.__inRect(this._mouse, node._meta.bounds.exitBounds[i].rect, node.pos, this._viewportCamera)) {
hasTarget = true;
// Double click to manually fire this exit chain.
node.activateExit(node._meta.bounds.exitBounds[i].name);
break;
}
}
}
// Output links.
if (!this._options.readOnly && !hasTarget) {
for (i = 0; i < node._meta.bounds.outputBounds.length; ++i) {
if (this.__inRect(this._mouse, node._meta.bounds.outputBounds[i].rect, node.pos, this._viewportCamera)) {
hasTarget = true;
// Double click to manually fire this output chain.
node.property(node._meta.bounds.outputBounds[i].name, node.property(node._meta.bounds.outputBounds[i].name), true);
break;
}
}
}
// Custom viewport area.
if (!hasTarget && node._meta.bounds.viewportBounds) {
var pos = {
x: (this._mouse.x - this._viewportCamera.x) / this._viewportCamera.z - node._meta.bounds.viewportBounds.left,
y: (this._mouse.y - this._viewportCamera.y) / this._viewportCamera.z - node._meta.bounds.viewportBounds.top
};
if (this.__inRect(this._mouse, node._meta.bounds.viewportBounds, node.pos, this._viewportCamera)) {
hasTarget = node.onViewportMouseDoubleClick(event, pos, this._options.readOnly);
}
}
// Center area.
if (!hasTarget && this.__inRect(this._mouse, node._meta.bounds.inner, node.pos, this._viewportCamera)) {
hasTarget = true;
if (node.instanceOf('wcNodeCompositeScript')) {
// Step into composite script nodes.
this._parent = node;
this._selectedNode = null;
this._selectedNodes = [];
this.center();
} else if (node.instanceOf('wcNodeComposite') && this._parent.instanceOf('wcNodeCompositeScript')) {
// Step out if double clicking on an external link node.
var focusNode = this._parent;
this._parent = this._parent._parent;
this._selectedNode = focusNode;
this._selectedNodes = [focusNode];
this.focus(this._selectedNodes);
} else if (node.instanceOf('wcNodeEntry')) {
node.onActivated();
}
}
}
},
__onViewportMouseWheel: function(event, elem) {
elem;
// Disable zoom if we are currently highlighting
if (this._highlightRect) {
return;
}
var oldZoom = this._viewportCamera.z;
// Custom viewport area.
if (this._highlightNode && this._highlightNode._meta.bounds.viewportBounds) {
var pos = {
x: (this._mouse.x - this._viewportCamera.x) / this._viewportCamera.z - this._highlightNode._meta.bounds.viewportBounds.left,
y: (this._mouse.y - this._viewportCamera.y) / this._viewportCamera.z - this._highlightNode._meta.bounds.viewportBounds.top
};
if (this.__inRect(this._mouse, this._highlightNode._meta.bounds.viewportBounds, this._highlightNode.pos, this._viewportCamera) &&
this._highlightNode.onViewportMouseWheel(event, pos, (event.originalEvent.wheelDelta > 0 || event.originalEvent.detail < 0), this._options.readOnly)) {
return;
}
}
if (event.originalEvent.wheelDelta > 0 || event.originalEvent.detail < 0) {
// scroll up to zoom in.
this._viewportCamera.z = Math.min(this._viewportCamera.z * 1.25, 5);
} else {
// scroll down to zoom out.
this._viewportCamera.z = Math.max(this._viewportCamera.z * 0.75, 0.1);
}
this._viewportCamera.x = (this._viewportCamera.x - this._mouse.x) / (oldZoom / this._viewportCamera.z) + this._mouse.x;
this._viewportCamera.y = (this._viewportCamera.y - this._mouse.y) / (oldZoom / this._viewportCamera.z) + this._mouse.y;
},
/**
* Does a bounding collision test to find any nodes at a given position.
* @function wcPlayEditor#__findNodeAtPos
* @private
* @param {wcPlay~Coordinates} pos - The position.
* @param {wcPlay~Coordinates} camera - The position of the camera.
* @param {wcNode[]} [nodes] - If supplied, will only search this list of nodes, otherwise will search all nodes in the viewport.
* @returns {wcNode|null} - A node at the given position, or null if none was found.
*/
__findNodeAtPos: function(pos, camera, nodes) {
var self = this;
function __test(nodes) {
// Iterate backwards so we always test the nodes that are drawn on top first.
for (var i = nodes.length-1; i >= 0; --i) {
if (nodes[i]._meta.bounds && self.__inRect(pos, nodes[i]._meta.bounds.rect, nodes[i].pos, camera)) {
return nodes[i];
}
}
return null;
}
if (this._parent) {
if (nodes === undefined) {
return __test(this._parent._storageNodes) ||
__test(this._parent._compositeNodes) ||
__test(this._parent._processNodes) ||
__test(this._parent._entryNodes);
} else {
return __test(nodes);
}
}
return null;
}
// /**
// * Finds the category area of the palette at a given position.
// * @function wcPlayEditor#__findCategoryAreaAtPos
// * @private
// * @param {wcPlay~Coordinates} pos - The position.
// * @returns {Object|null} - The category data found, or null if not found.
// */
// __findCategoryAreaAtPos: function(pos) {
// for (var cat in this._nodeLibrary) {
// for (var type in this._nodeLibrary[cat]) {
// // Ignore types that are not visible.
// if (!this.$typeButton[this.__typeIndex(type)].hasClass('wcToggled')) continue;
// var typeData = this._nodeLibrary[cat][type];
// // Ignore categories that are not visible.
// if (typeData.$button.hasClass('wcToggled')) continue;
// var rect = typeData.$canvas.offset();
// rect.width = typeData.$canvas.width();
// rect.height = typeData.$canvas.height();
// if (this.__inRect(pos, rect)) {
// return typeData;
// }
// }
// }
// },
};