diff --git a/src/engine/adapter.js b/src/engine/adapter.js index 90e401d68..26f52be90 100644 --- a/src/engine/adapter.js +++ b/src/engine/adapter.js @@ -31,7 +31,7 @@ function domToBlocks (blocksDOM) { } var tagName = block.name.toLowerCase(); if (tagName == 'block' || tagName == 'shadow') { - domToBlock(block, blocks, true); + domToBlock(block, blocks, true, null); } } // Flatten blocks object into a list. @@ -48,8 +48,9 @@ function domToBlocks (blocksDOM) { * @param {Element} blockDOM DOM tree for an individual block. * @param {Object} blocks Collection of blocks to add to. * @param {Boolean} isTopBlock Whether blocks at this level are "top blocks." + * @param {?string} parent Parent block ID. */ -function domToBlock (blockDOM, blocks, isTopBlock) { +function domToBlock (blockDOM, blocks, isTopBlock, parent) { // Block skeleton. var block = { id: blockDOM.attribs.id, // Block ID @@ -58,6 +59,7 @@ function domToBlock (blockDOM, blocks, isTopBlock) { fields: {}, // Fields on this block and their values. next: null, // Next block in the stack, if one exists. topLevel: isTopBlock, // If this block starts a stack. + parent: parent, // Parent block ID, if available. shadow: blockDOM.name == 'shadow', // If this represents a shadow/slot. x: blockDOM.attribs.x, // X position of script, if top-level. y: blockDOM.attribs.y // Y position of script, if top-level. @@ -113,10 +115,10 @@ function domToBlock (blockDOM, blocks, isTopBlock) { case 'value': case 'statement': // Recursively generate block structure for input block. - domToBlock(childBlockNode, blocks, false); + domToBlock(childBlockNode, blocks, false, block.id); if (childShadowNode && childBlockNode != childShadowNode) { // Also generate the shadow block. - domToBlock(childShadowNode, blocks, false); + domToBlock(childShadowNode, blocks, false, block.id); } // Link this block's input to the child block. var inputName = xmlChild.attribs.name; @@ -132,7 +134,7 @@ function domToBlock (blockDOM, blocks, isTopBlock) { continue; } // Recursively generate block structure for next block. - domToBlock(childBlockNode, blocks, false); + domToBlock(childBlockNode, blocks, false, block.id); // Link next block to this block. block.next = childBlockNode.attribs.id; break; diff --git a/src/engine/blocks.js b/src/engine/blocks.js index 421087f5b..ba9aac5bb 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -115,6 +115,20 @@ Blocks.prototype.getInputs = function (id) { return inputs; }; +/** + * Get the top-level script for a given block. + * @param {?string} id ID of block to query. + * @return {?string} ID of top-level script block. + */ +Blocks.prototype.getTopLevelScript = function (id) { + if (typeof this._blocks[id] === 'undefined') return null; + var block = this._blocks[id]; + while (block.parent !== null) { + block = this._blocks[block.parent]; + } + return block.id; +}; + // --------------------------------------------------------------------- /** @@ -237,6 +251,7 @@ Blocks.prototype.moveBlock = function (e) { // This block was connected to the old parent's next connection. oldParent.next = null; } + this._blocks[e.id].parent = null; } // Has the block become a top-level block? @@ -262,6 +277,7 @@ Blocks.prototype.moveBlock = function (e) { // Moved to the new parent's next connection. this._blocks[e.newParent].next = e.id; } + this._blocks[e.id].parent = e.newParent; } }; diff --git a/src/engine/execute.js b/src/engine/execute.js index 5efe9bebd..6e19fcdfd 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -41,6 +41,7 @@ var execute = function (sequencer, thread) { } // Skip through the block. // (either hat with no predicate, or missing op). + thread.requestScriptGlowInFrame = true; return; } @@ -110,6 +111,12 @@ var execute = function (sequencer, thread) { } }); + if (typeof primitiveReportedValue === 'undefined') { + // No value reported - potentially a command block. + // Edge-activated hats don't request a glow; all commands do. + thread.requestScriptGlowInFrame = true; + } + /** * Handle any reported value from the primitive, either directly returned * or after a promise resolves. diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 143f30aa7..473227ba8 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -57,28 +57,31 @@ function Runtime () { 'keyboard': new Keyboard(this), 'mouse': new Mouse() }; + + this._scriptGlowsPreviousFrame = []; + this._editingTarget = null; } /** - * Event name for glowing a stack + * Event name for glowing a script. * @const {string} */ -Runtime.STACK_GLOW_ON = 'STACK_GLOW_ON'; +Runtime.SCRIPT_GLOW_ON = 'STACK_GLOW_ON'; /** - * Event name for unglowing a stack + * Event name for unglowing a script. * @const {string} */ -Runtime.STACK_GLOW_OFF = 'STACK_GLOW_OFF'; +Runtime.SCRIPT_GLOW_OFF = 'STACK_GLOW_OFF'; /** - * Event name for glowing a block + * Event name for glowing a block. * @const {string} */ Runtime.BLOCK_GLOW_ON = 'BLOCK_GLOW_ON'; /** - * Event name for unglowing a block + * Event name for unglowing a block. * @const {string} */ Runtime.BLOCK_GLOW_OFF = 'BLOCK_GLOW_OFF'; @@ -196,7 +199,6 @@ Runtime.prototype.clearEdgeActivatedValues = function () { */ Runtime.prototype._pushThread = function (id) { var thread = new Thread(id); - this.glowScript(id, true); thread.pushStack(id); this.threads.push(thread); return thread; @@ -209,7 +211,6 @@ Runtime.prototype._pushThread = function (id) { Runtime.prototype._removeThread = function (thread) { var i = this.threads.indexOf(thread); if (i > -1) { - this.glowScript(thread.topBlock, false); this.threads.splice(i, 1); } }; @@ -341,11 +342,6 @@ Runtime.prototype.stopAll = function () { var threadsCopy = this.threads.slice(); while (threadsCopy.length > 0) { var poppedThread = threadsCopy.pop(); - // Unglow any blocks on this thread's stack. - for (var i = 0; i < poppedThread.stack.length; i++) { - this.glowBlock(poppedThread.stack[i], false); - } - // Actually remove the thread. this._removeThread(poppedThread); } }; @@ -363,11 +359,55 @@ Runtime.prototype._step = function () { } } var inactiveThreads = this.sequencer.stepThreads(this.threads); + this._updateScriptGlows(); for (var i = 0; i < inactiveThreads.length; i++) { this._removeThread(inactiveThreads[i]); } }; +Runtime.prototype.setEditingTarget = function (editingTarget) { + this._scriptGlowsPreviousFrame = []; + this._editingTarget = editingTarget; + this._updateScriptGlows(); +}; + +Runtime.prototype._updateScriptGlows = function () { + // Set of scripts that request a glow this frame. + var requestedGlowsThisFrame = []; + // Final set of scripts glowing during this frame. + var finalScriptGlows = []; + // Find all scripts that should be glowing. + for (var i = 0; i < this.threads.length; i++) { + var thread = this.threads[i]; + var target = this.targetForThread(thread); + if (thread.requestScriptGlowInFrame && target == this._editingTarget) { + var blockForThread = thread.peekStack() || thread.topBlock; + var script = target.blocks.getTopLevelScript(blockForThread); + requestedGlowsThisFrame.push(script); + } + } + // Compare to previous frame. + for (var j = 0; j < this._scriptGlowsPreviousFrame.length; j++) { + var previousFrameGlow = this._scriptGlowsPreviousFrame[j]; + if (requestedGlowsThisFrame.indexOf(previousFrameGlow) < 0) { + // Glow turned off. + this.glowScript(previousFrameGlow, false); + } else { + // Still glowing. + finalScriptGlows.push(previousFrameGlow); + } + } + for (var k = 0; k < requestedGlowsThisFrame.length; k++) { + var currentFrameGlow = requestedGlowsThisFrame[k]; + if (this._scriptGlowsPreviousFrame.indexOf(currentFrameGlow) < 0) { + // Glow turned on. + this.glowScript(currentFrameGlow, true); + finalScriptGlows.push(currentFrameGlow); + } + } + this._scriptGlowsPreviousFrame = finalScriptGlows; +}; + /** * Emit feedback for block glowing (used in the sequencer). * @param {?string} blockId ID for the block to update glow @@ -388,9 +428,9 @@ Runtime.prototype.glowBlock = function (blockId, isGlowing) { */ Runtime.prototype.glowScript = function (topBlockId, isGlowing) { if (isGlowing) { - this.emit(Runtime.STACK_GLOW_ON, topBlockId); + this.emit(Runtime.SCRIPT_GLOW_ON, topBlockId); } else { - this.emit(Runtime.STACK_GLOW_OFF, topBlockId); + this.emit(Runtime.SCRIPT_GLOW_OFF, topBlockId); } }; diff --git a/src/engine/thread.js b/src/engine/thread.js index 07a98b862..0dc982702 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -28,6 +28,12 @@ function Thread (firstBlock) { * @type {number} */ this.status = 0; /* Thread.STATUS_RUNNING */ + + /** + * Whether the thread requests its script to glow during this frame. + * @type {boolean} + */ + this.requestScriptGlowInFrame = false; } /** diff --git a/src/import/sb2import.js b/src/import/sb2import.js index 391ed06e3..e8f14b708 100644 --- a/src/import/sb2import.js +++ b/src/import/sb2import.js @@ -115,6 +115,7 @@ function parseScripts (scripts, blocks) { parsedBlockList[0].x = scriptX * 1.1; parsedBlockList[0].y = scriptY * 1.1; parsedBlockList[0].topLevel = true; + parsedBlockList[0].parent = null; } // Flatten children and create add the blocks. var convertedBlocks = flatten(parsedBlockList); @@ -139,6 +140,7 @@ function parseBlockList (blockList) { var block = blockList[i]; var parsedBlock = parseBlock(block); if (previousBlock) { + parsedBlock.parent = previousBlock.id; previousBlock.next = parsedBlock.id; } previousBlock = parsedBlock; @@ -217,6 +219,9 @@ function parseBlock (sb2block) { // Single block occupies the input. innerBlocks = [parseBlock(providedArg)]; } + for (var j = 0; j < innerBlocks.length; j++) { + innerBlocks[j].parent = activeBlock.id; + } // Obscures any shadow. shadowObscured = true; activeBlock.inputs[expectedArg.inputName].block = ( @@ -271,6 +276,7 @@ function parseBlock (sb2block) { fields: fields, next: null, topLevel: false, + parent: activeBlock.id, shadow: true }); activeBlock.inputs[expectedArg.inputName].shadow = inputUid; diff --git a/src/index.js b/src/index.js index e5c19ee1c..e4dd8df5a 100644 --- a/src/index.js +++ b/src/index.js @@ -31,11 +31,11 @@ function VirtualMachine () { */ instance.editingTarget = null; // Runtime emits are passed along as VM emits. - instance.runtime.on(Runtime.STACK_GLOW_ON, function (id) { - instance.emit(Runtime.STACK_GLOW_ON, {id: id}); + instance.runtime.on(Runtime.SCRIPT_GLOW_ON, function (id) { + instance.emit(Runtime.SCRIPT_GLOW_ON, {id: id}); }); - instance.runtime.on(Runtime.STACK_GLOW_OFF, function (id) { - instance.emit(Runtime.STACK_GLOW_OFF, {id: id}); + instance.runtime.on(Runtime.SCRIPT_GLOW_OFF, function (id) { + instance.emit(Runtime.SCRIPT_GLOW_OFF, {id: id}); }); instance.runtime.on(Runtime.BLOCK_GLOW_ON, function (id) { instance.emit(Runtime.BLOCK_GLOW_ON, {id: id}); @@ -114,6 +114,7 @@ VirtualMachine.prototype.loadProject = function (json) { // Update the VM user's knowledge of targets and blocks on the workspace. this.emitTargetsUpdate(); this.emitWorkspaceUpdate(); + this.runtime.setEditingTarget(this.editingTarget); }; /** @@ -135,6 +136,7 @@ VirtualMachine.prototype.setEditingTarget = function (targetId) { // Emit appropriate UI updates. this.emitTargetsUpdate(); this.emitWorkspaceUpdate(); + this.runtime.setEditingTarget(target); } }; @@ -233,11 +235,11 @@ if (ENV_WORKER) { } }; // Bind runtime's emitted events to postmessages. - self.vmInstance.runtime.on(Runtime.STACK_GLOW_ON, function (id) { - self.postMessage({method: Runtime.STACK_GLOW_ON, id: id}); + self.vmInstance.runtime.on(Runtime.SCRIPT_GLOW_ON, function (id) { + self.postMessage({method: Runtime.SCRIPT_GLOW_ON, id: id}); }); - self.vmInstance.runtime.on(Runtime.STACK_GLOW_OFF, function (id) { - self.postMessage({method: Runtime.STACK_GLOW_OFF, id: id}); + self.vmInstance.runtime.on(Runtime.SCRIPT_GLOW_OFF, function (id) { + self.postMessage({method: Runtime.SCRIPT_GLOW_OFF, id: id}); }); self.vmInstance.runtime.on(Runtime.BLOCK_GLOW_ON, function (id) { self.postMessage({method: Runtime.BLOCK_GLOW_ON, id: id});