diff --git a/.gitignore b/.gitignore index 753e3f65..aad54ff5 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ npm-* /dist.js /vm.js /vm.min.js +/playground/assets /playground/media /playground/vendor.js /playground/vm.js diff --git a/.travis.yml b/.travis.yml index 091fe53f..a44d5380 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,14 @@ language: node_js node_js: -- "4" -- "stable" +- '4' +- stable sudo: false cache: directories: - node_modules -before_install: -# Install the most up to date scratch-* dependencies -- rm -rf node_modules/scratch-* +install: +- npm install +- npm update after_script: - | # RELEASE_BRANCHES and NPM_TOKEN defined in Travis settings panel diff --git a/package.json b/package.json index e042b6de..15f2dd45 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "main": "./dist.js", "scripts": { "prepublish": "./node_modules/.bin/webpack --bail", - "start": "make serve", + "start": "webpack-dev-server", + "build": "webpack --colors --progress", "test": "make test", "version": "./node_modules/.bin/json -f package.json -I -e \"this.repository.sha = '$(git log -n1 --pretty=format:%H)'\"" }, diff --git a/playground/index.html b/playground/index.html index 804d31f8..46f102e6 100644 --- a/playground/index.html +++ b/playground/index.html @@ -51,751 +51,6 @@
- diff --git a/playground/playground.js b/playground/playground.js index b42c1577..bdd7909d 100644 --- a/playground/playground.js +++ b/playground/playground.js @@ -1,6 +1,12 @@ +var NEW_PROJECT_HASH = 'createEmptyProject'; + var loadProject = function () { var id = location.hash.substring(1); - if (id.length < 1) { + if (id === NEW_PROJECT_HASH) { + window.vm.createEmptyProject(); + return; + } + if (id.length < 1 || !isFinite(id)) { id = '119615668'; } var url = 'https://projects.scratch.mit.edu/internalapi/project/' + @@ -32,7 +38,7 @@ window.onload = function() { }; document.getElementById('createEmptyProject').addEventListener('click', function() { - document.location = '#' + 'createEmptyProject'; + document.location = '#' + NEW_PROJECT_HASH; location.reload(); }); loadProject(); @@ -47,9 +53,7 @@ window.onload = function() { window.audioEngine = new window.AudioEngine(); // Instantiate scratch-blocks and attach it to the DOM. - var toolbox = document.getElementById('toolbox'); var workspace = window.Blockly.inject('blocks', { - toolbox: toolbox, media: './media/', zoom: { controls: true, @@ -70,8 +74,9 @@ window.onload = function() { window.workspace = workspace; // Attach scratch-blocks events to VM. - // @todo: Re-enable flyout listening after fixing GH-69. workspace.addChangeListener(vm.blockListener); + var flyoutWorkspace = workspace.getFlyout().getWorkspace(); + flyoutWorkspace.addChangeListener(vm.flyoutBlockListener); // Create FPS counter. var stats = new window.Stats(); @@ -116,11 +121,9 @@ window.onload = function() { // Receipt of new block XML for the selected target. vm.on('workspaceUpdate', function (data) { - window.Blockly.Events.disable(); workspace.clear(); var dom = window.Blockly.Xml.textToDom(data.xml); window.Blockly.Xml.domToWorkspace(dom, workspace); - window.Blockly.Events.enable(); }); // Receipt of new list of targets, selected target update. diff --git a/src/blocks/scratch3_control.js b/src/blocks/scratch3_control.js index eab6c1a0..a2bab50e 100644 --- a/src/blocks/scratch3_control.js +++ b/src/blocks/scratch3_control.js @@ -133,9 +133,16 @@ Scratch3ControlBlocks.prototype.ifElse = function(args, util) { } }; -Scratch3ControlBlocks.prototype.stop = function() { - // @todo - don't use this.runtime - this.runtime.stopAll(); +Scratch3ControlBlocks.prototype.stop = function(args, util) { + var option = args.STOP_OPTION; + if (option == 'all') { + util.stopAll(); + } else if (option == 'other scripts in sprite' || + option == 'other scripts in stage') { + util.stopOtherTargetThreads(); + } else if (option == 'this script') { + util.stopThread(); + } }; // @todo (GH-146): remove. @@ -160,6 +167,7 @@ Scratch3ControlBlocks.prototype.createClone = function (args, util) { }; Scratch3ControlBlocks.prototype.deleteClone = function (args, util) { + if (util.target.isOriginal) return; this.runtime.disposeTarget(util.target); this.runtime.stopForTarget(util.target); }; diff --git a/src/blocks/scratch3_data.js b/src/blocks/scratch3_data.js index 576eb975..a4017242 100644 --- a/src/blocks/scratch3_data.js +++ b/src/blocks/scratch3_data.js @@ -17,7 +17,7 @@ Scratch3DataBlocks.prototype.getPrimitives = function () { 'data_variable': this.getVariable, 'data_setvariableto': this.setVariableTo, 'data_changevariableby': this.changeVariableBy, - 'data_list': this.getListContents, + 'data_listcontents': this.getListContents, 'data_addtolist': this.addToList, 'data_deleteoflist': this.deleteOfList, 'data_insertatlist': this.insertAtList, diff --git a/src/blocks/scratch3_motion.js b/src/blocks/scratch3_motion.js index 3e19e6ef..a8687e4f 100644 --- a/src/blocks/scratch3_motion.js +++ b/src/blocks/scratch3_motion.js @@ -37,7 +37,7 @@ Scratch3MotionBlocks.prototype.getPrimitives = function() { Scratch3MotionBlocks.prototype.moveSteps = function (args, util) { var steps = Cast.toNumber(args.STEPS); - var radians = MathUtil.degToRad(util.target.direction); + var radians = MathUtil.degToRad(90 - util.target.direction); var dx = steps * Math.cos(radians); var dy = steps * Math.sin(radians); util.target.setXY(util.target.x + dx, util.target.y + dy); diff --git a/src/blocks/scratch3_procedures.js b/src/blocks/scratch3_procedures.js index 8d43c4c9..eb33e322 100644 --- a/src/blocks/scratch3_procedures.js +++ b/src/blocks/scratch3_procedures.js @@ -13,7 +13,8 @@ function Scratch3ProcedureBlocks(runtime) { Scratch3ProcedureBlocks.prototype.getPrimitives = function() { return { 'procedures_defnoreturn': this.defNoReturn, - 'procedures_callnoreturn': this.callNoReturn + 'procedures_callnoreturn': this.callNoReturn, + 'procedures_param': this.param }; }; @@ -23,10 +24,24 @@ Scratch3ProcedureBlocks.prototype.defNoReturn = function () { Scratch3ProcedureBlocks.prototype.callNoReturn = function (args, util) { if (!util.stackFrame.executed) { - var procedureName = args.mutation.name; + var procedureName = args.mutation.proccode; + var paramNames = util.getProcedureParamNames(procedureName); + for (var i = 0; i < paramNames.length; i++) { + if (args.hasOwnProperty('input' + i)) { + util.pushParam(paramNames[i], args['input' + i]); + } + } util.stackFrame.executed = true; util.startProcedure(procedureName); } }; +Scratch3ProcedureBlocks.prototype.param = function (args, util) { + var value = util.getParam(args.mutation.paramname); + if (!value) { + return 0; + } + return value; +}; + module.exports = Scratch3ProcedureBlocks; diff --git a/src/blocks/scratch3_sensing.js b/src/blocks/scratch3_sensing.js index 15466e3c..c4e780dd 100644 --- a/src/blocks/scratch3_sensing.js +++ b/src/blocks/scratch3_sensing.js @@ -23,7 +23,8 @@ Scratch3SensingBlocks.prototype.getPrimitives = function() { 'sensing_mousey': this.getMouseY, 'sensing_mousedown': this.getMouseDown, 'sensing_keypressed': this.getKeyPressed, - 'sensing_current': this.current + 'sensing_current': this.current, + 'sensing_dayssince2000': this.daysSince2000 }; }; @@ -99,4 +100,15 @@ Scratch3SensingBlocks.prototype.getKeyPressed = function (args, util) { return util.ioQuery('keyboard', 'getKeyIsDown', args.KEY_OPTION); }; +Scratch3SensingBlocks.prototype.daysSince2000 = function() +{ + var msPerDay = 24 * 60 * 60 * 1000; + var start = new Date(2000, 1-1, 1); + var today = new Date(); + var dstAdjust = today.getTimezoneOffset() - start.getTimezoneOffset(); + var mSecsSinceStart = today.valueOf() - start.valueOf(); + mSecsSinceStart += ((today.getTimezoneOffset() - dstAdjust) * 60 * 1000); + return mSecsSinceStart / msPerDay; +}; + module.exports = Scratch3SensingBlocks; diff --git a/src/engine/blocks.js b/src/engine/blocks.js index f96c916a..0e89025e 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -151,24 +151,40 @@ Blocks.prototype.getProcedureDefinition = function (name) { var block = this._blocks[id]; if ((block.opcode == 'procedures_defnoreturn' || block.opcode == 'procedures_defreturn') && - block.fields['NAME'].value == name) { + block.mutation.proccode == name) { return id; } } return null; }; +/** + * Get the procedure definition for a given name. + * @param {?string} name Name of procedure to query. + * @return {?string} ID of procedure definition. + */ +Blocks.prototype.getProcedureParamNames = function (name) { + for (var id in this._blocks) { + var block = this._blocks[id]; + if ((block.opcode == 'procedures_defnoreturn' || + block.opcode == 'procedures_defreturn') && + block.mutation.proccode == name) { + return JSON.parse(block.mutation.argumentnames); + } + } + return null; +}; + // --------------------------------------------------------------------- /** * Create event listener for blocks. Handles validation and serves as a generic * adapter between the blocks and the runtime interface. * @param {Object} e Blockly "block" event - * @param {boolean} isFlyout If true, create a listener for flyout events. * @param {?Runtime} opt_runtime Optional runtime to forward click events to. */ -Blocks.prototype.blocklyListen = function (e, isFlyout, opt_runtime) { +Blocks.prototype.blocklyListen = function (e, opt_runtime) { // Validate event if (typeof e !== 'object') return; if (typeof e.blockId !== 'string') return; @@ -187,7 +203,7 @@ Blocks.prototype.blocklyListen = function (e, isFlyout, opt_runtime) { var newBlocks = adapter(e); // A create event can create many blocks. Add them all. for (var i = 0; i < newBlocks.length; i++) { - this.createBlock(newBlocks[i], isFlyout); + this.createBlock(newBlocks[i]); } break; case 'change': @@ -209,8 +225,10 @@ Blocks.prototype.blocklyListen = function (e, isFlyout, opt_runtime) { }); break; case 'delete': - // Don't accept delete events for shadow blocks being obscured. - if (this._blocks[e.blockId].shadow) { + // Don't accept delete events for missing blocks, + // or shadow blocks being obscured. + if (!this._blocks.hasOwnProperty(e.blockId) || + this._blocks[e.blockId].shadow) { return; } // Inform any runtime to forget about glows on this script. @@ -229,9 +247,8 @@ Blocks.prototype.blocklyListen = function (e, isFlyout, opt_runtime) { /** * Block management: create blocks and scripts from a `create` event * @param {!Object} block Blockly create event to be processed - * @param {boolean} opt_isFlyoutBlock Whether the block is in the flyout. */ -Blocks.prototype.createBlock = function (block, opt_isFlyoutBlock) { +Blocks.prototype.createBlock = function (block) { // Does the block already exist? // Could happen, e.g., for an unobscured shadow. if (this._blocks.hasOwnProperty(block.id)) { @@ -241,9 +258,8 @@ Blocks.prototype.createBlock = function (block, opt_isFlyoutBlock) { this._blocks[block.id] = block; // Push block id to scripts array. // Blocks are added as a top-level stack if they are marked as a top-block - // (if they were top-level XML in the event) and if they are not - // flyout blocks. - if (!opt_isFlyoutBlock && block.topLevel) { + // (if they were top-level XML in the event). + if (block.topLevel) { this._addScript(block.id); } }; @@ -271,6 +287,10 @@ Blocks.prototype.changeBlock = function (args) { * @param {!Object} e Blockly move event to be processed */ Blocks.prototype.moveBlock = function (e) { + if (!this._blocks.hasOwnProperty(e.id)) { + return; + } + // Move coordinate changes. if (e.newCoordinate) { this._blocks[e.id].x = e.newCoordinate.x; @@ -434,7 +454,9 @@ Blocks.prototype.mutationToXML = function (mutation) { var mutationString = '<' + mutation.tagName; for (var prop in mutation) { if (prop == 'children' || prop == 'tagName') continue; - mutationString += ' ' + prop + '="' + mutation[prop] + '"'; + var mutationValue = (typeof mutation[prop] === 'string') ? + xmlEscape(mutation[prop]) : mutation[prop]; + mutationString += ' ' + prop + '="' + mutationValue + '"'; } mutationString += '>'; for (var i = 0; i < mutation.children.length; i++) { diff --git a/src/engine/execute.js b/src/engine/execute.js index 59b4a3cb..c857b73d 100644 --- a/src/engine/execute.js +++ b/src/engine/execute.js @@ -22,19 +22,34 @@ var execute = function (sequencer, thread) { var currentBlockId = thread.peekStack(); var currentStackFrame = thread.peekStackFrame(); - // Verify that the block still exists. - if (!target || - typeof target.blocks.getBlock(currentBlockId) === 'undefined') { + // Check where the block lives: target blocks or flyout blocks. + var targetHasBlock = ( + typeof target.blocks.getBlock(currentBlockId) !== 'undefined' + ); + var flyoutHasBlock = ( + typeof runtime.flyoutBlocks.getBlock(currentBlockId) !== 'undefined' + ); + + // Stop if block or target no longer exists. + if (!target || (!targetHasBlock && !flyoutHasBlock)) { // No block found: stop the thread; script no longer exists. sequencer.retireThread(thread); return; } + // Query info about the block. - var opcode = target.blocks.getOpcode(currentBlockId); + var blockContainer = null; + if (targetHasBlock) { + blockContainer = target.blocks; + } else { + blockContainer = runtime.flyoutBlocks; + } + var opcode = blockContainer.getOpcode(currentBlockId); + var fields = blockContainer.getFields(currentBlockId); + var inputs = blockContainer.getInputs(currentBlockId); var blockFunction = runtime.getOpcodeFunction(opcode); var isHat = runtime.getIsHat(opcode); - var fields = target.blocks.getFields(currentBlockId); - var inputs = target.blocks.getInputs(currentBlockId); + if (!opcode) { console.warn('Could not get opcode for block: ' + currentBlockId); @@ -133,7 +148,7 @@ var execute = function (sequencer, thread) { } // Add any mutation to args (e.g., for procedures). - var mutation = target.blocks.getMutation(currentBlockId); + var mutation = blockContainer.getMutation(currentBlockId); if (mutation) { argValues.mutation = mutation; } @@ -161,9 +176,27 @@ var execute = function (sequencer, thread) { startBranch: function (branchNum) { sequencer.stepToBranch(thread, branchNum); }, + stopAll: function () { + runtime.stopAll(); + }, + stopOtherTargetThreads: function() { + runtime.stopForTarget(target, thread); + }, + stopThread: function() { + sequencer.retireThread(thread); + }, startProcedure: function (procedureName) { sequencer.stepToProcedure(thread, procedureName); }, + getProcedureParamNames: function (procedureName) { + return blockContainer.getProcedureParamNames(procedureName); + }, + pushParam: function (paramName, paramValue) { + thread.pushParam(paramName, paramValue); + }, + getParam: function (paramName) { + return thread.getParam(paramName); + }, startHats: function(requestedHat, opt_matchFields, opt_target) { return ( runtime.startHats(requestedHat, opt_matchFields, opt_target) diff --git a/src/engine/runtime.js b/src/engine/runtime.js index 72359c63..9a8143b6 100644 --- a/src/engine/runtime.js +++ b/src/engine/runtime.js @@ -1,5 +1,6 @@ var EventEmitter = require('events'); var Sequencer = require('./sequencer'); +var Blocks = require('./blocks'); var Thread = require('./thread'); var util = require('util'); @@ -45,6 +46,8 @@ function Runtime () { /** @type {!Sequencer} */ this.sequencer = new Sequencer(this); + this.flyoutBlocks = new Blocks(); + /** * Map to look up a block primitive's implementation function by its opcode. * This is a two-step lookup: package name first, then primitive name. @@ -244,6 +247,9 @@ Runtime.prototype._pushThread = function (id, target) { * @param {?Thread} thread Thread object to remove from actives */ Runtime.prototype._removeThread = function (thread) { + // Inform sequencer to stop executing that thread. + this.sequencer.retireThread(thread); + // Remove from the list. var i = this.threads.indexOf(thread); if (i > -1) { this.threads.splice(i, 1); @@ -342,7 +348,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode, // any existing threads starting with the top block. for (var i = 0; i < instance.threads.length; i++) { if (instance.threads[i].topBlock === topBlockId && - (!opt_target || instance.threads[i].target == opt_target)) { + instance.threads[i].target == target) { instance._removeThread(instance.threads[i]); } } @@ -351,7 +357,7 @@ Runtime.prototype.startHats = function (requestedHatOpcode, // give up if any threads with the top block are running. for (var j = 0; j < instance.threads.length; j++) { if (instance.threads[j].topBlock === topBlockId && - (!opt_target || instance.threads[j].target == opt_target)) { + instance.threads[j].target == target) { // Some thread is already running. return; } @@ -364,26 +370,38 @@ Runtime.prototype.startHats = function (requestedHatOpcode, }; /** - * Dispose of a target. - * @param {!Target} target Target to dispose of. + * Dispose all targets. Return to clean state. */ -Runtime.prototype.disposeTarget = function (target) { - // Allow target to do dispose actions. - target.dispose(); - // Remove from list of targets. - var index = this.targets.indexOf(target); - if (index > -1) { - this.targets.splice(index, 1); - } +Runtime.prototype.dispose = function () { + this.stopAll(); + this.targets.map(this.disposeTarget, this); +}; + +/** + * Dispose of a target. + * @param {!Target} disposingTarget Target to dispose of. + */ +Runtime.prototype.disposeTarget = function (disposingTarget) { + this.targets = this.targets.filter(function (target) { + if (disposingTarget !== target) return true; + // Allow target to do dispose actions. + target.dispose(); + // Remove from list of targets. + return false; + }); }; /** * Stop any threads acting on the target. * @param {!Target} target Target to stop threads for. + * @param {Thread=} opt_threadException Optional thread to skip. */ -Runtime.prototype.stopForTarget = function (target) { +Runtime.prototype.stopForTarget = function (target, opt_threadException) { // Stop any threads on the target. for (var i = 0; i < this.threads.length; i++) { + if (this.threads[i] === opt_threadException) { + continue; + } if (this.threads[i].target == target) { this._removeThread(this.threads[i]); } @@ -464,6 +482,10 @@ Runtime.prototype._updateScriptGlows = function () { if (thread.requestScriptGlowInFrame && target == this._editingTarget) { var blockForThread = thread.peekStack() || thread.topBlock; var script = target.blocks.getTopLevelScript(blockForThread); + if (!script) { + // Attempt to find in flyout blocks. + script = this.flyoutBlocks.getTopLevelScript(blockForThread); + } if (script) { requestedGlowsThisFrame.push(script); } diff --git a/src/engine/sequencer.js b/src/engine/sequencer.js index 85575c80..0e54ca4a 100644 --- a/src/engine/sequencer.js +++ b/src/engine/sequencer.js @@ -132,6 +132,11 @@ Sequencer.prototype.stepToBranch = function (thread, branchNum) { Sequencer.prototype.stepToProcedure = function (thread, procedureName) { var definition = thread.target.blocks.getProcedureDefinition(procedureName); thread.pushStack(definition); + // Check if the call is recursive. If so, yield. + // @todo: Have behavior match Scratch 2.0. + if (thread.stack.indexOf(definition) > -1) { + thread.setStatus(Thread.STATUS_YIELD_FRAME); + } }; /** diff --git a/src/engine/thread.js b/src/engine/thread.js index 3bb53596..6a08361f 100644 --- a/src/engine/thread.js +++ b/src/engine/thread.js @@ -83,6 +83,7 @@ Thread.prototype.pushStack = function (blockId) { this.stackFrames.push({ reported: {}, // Collects reported input values. waitingReporter: null, // Name of waiting reporter. + params: {}, // Procedure parameters. executionContext: {} // A context passed to block implementations. }); } @@ -135,6 +136,21 @@ Thread.prototype.pushReportedValue = function (value) { } }; +Thread.prototype.pushParam = function (paramName, value) { + var stackFrame = this.peekStackFrame(); + stackFrame.params[paramName] = value; +}; + +Thread.prototype.getParam = function (paramName) { + for (var i = this.stackFrames.length - 1; i >= 0; i--) { + var frame = this.stackFrames[i]; + if (frame.params.hasOwnProperty(paramName)) { + return frame.params[paramName]; + } + } + return null; +}; + /** * Whether the current execution of a thread is at the top of the stack. * @return {Boolean} True if execution is at top of the stack. diff --git a/src/import/sb2import.js b/src/import/sb2import.js index 68974cad..602068e0 100644 --- a/src/import/sb2import.js +++ b/src/import/sb2import.js @@ -218,6 +218,40 @@ function flatten (blocks) { return finalBlocks; } +/** + * Convert a Scratch 2.0 procedure string (e.g., "my_procedure %s %b %n") + * into an argument map. This allows us to provide the expected inputs + * to a mutated procedure call. + * @param {string} procCode Scratch 2.0 procedure string. + * @return {Object} Argument map compatible with those in sb2specmap. + */ +function parseProcedureArgMap (procCode) { + var argMap = [ + {} // First item in list is op string. + ]; + var INPUT_PREFIX = 'input'; + var inputCount = 0; + // Split by %n, %b, %s. + var parts = procCode.split(/(?=[^\\]\%[nbs])/); + for (var i = 0; i < parts.length; i++) { + var part = parts[i].trim(); + if (part.substring(0, 1) == '%') { + var argType = part.substring(1, 2); + var arg = { + type: 'input', + inputName: INPUT_PREFIX + (inputCount++) + }; + if (argType == 'n') { + arg.inputOp = 'math_number'; + } else if (argType == 's') { + arg.inputOp = 'text'; + } + argMap.push(arg); + } + } + return argMap; +} + /** * Parse a single SB2 JSON-formatted block and its children. * @param {!Object} sb2block SB2 JSON-formatted block. @@ -242,6 +276,10 @@ function parseBlock (sb2block) { shadow: false, // No shadow blocks in an SB2 by default. children: [] // Store any generated children, flattened in `flatten`. }; + // For a procedure call, generate argument map from proc string. + if (oldOpcode == 'call') { + blockMetadata.argMap = parseProcedureArgMap(sb2block[1]); + } // Look at the expected arguments in `blockMetadata.argMap.` // The basic problem here is to turn positional SB2 arguments into // non-positional named Scratch VM arguments. @@ -269,8 +307,14 @@ function parseBlock (sb2block) { // Single block occupies the input. innerBlocks = [parseBlock(providedArg)]; } + var previousBlock = null; for (var j = 0; j < innerBlocks.length; j++) { - innerBlocks[j].parent = activeBlock.id; + if (j == 0) { + innerBlocks[j].parent = activeBlock.id; + } else { + innerBlocks[j].parent = previousBlock; + } + previousBlock = innerBlocks[j].id; } // Obscures any shadow. shadowObscured = true; @@ -343,11 +387,44 @@ function parseBlock (sb2block) { } } // Special cases to generate mutations. - if (oldOpcode == 'call') { + if (oldOpcode == 'stopScripts') { + // Mutation for stop block: if the argument is 'other scripts', + // the block needs a next connection. + if (sb2block[1] == 'other scripts in sprite' || + sb2block[1] == 'other scripts in stage') { + activeBlock.mutation = { + tagName: 'mutation', + hasnext: 'true', + children: [] + }; + } + } else if (oldOpcode == 'procDef') { + // Mutation for procedure definition: + // store all 2.0 proc data. + var procData = sb2block.slice(1); + activeBlock.mutation = { + tagName: 'mutation', + proccode: procData[0], // e.g., "abc %n %b %s" + argumentnames: JSON.stringify(procData[1]), // e.g. ['arg1', 'arg2'] + argumentdefaults: JSON.stringify(procData[2]), // e.g., [1, 'abc'] + warp: procData[3], // Warp mode, e.g., true/false. + children: [] + }; + } else if (oldOpcode == 'call') { + // Mutation for procedure call: + // string for proc code (e.g., "abc %n %b %s"). activeBlock.mutation = { tagName: 'mutation', children: [], - name: sb2block[1] + proccode: sb2block[1] + }; + } else if (oldOpcode == 'getParam') { + // Mutation for procedure parameter. + activeBlock.mutation = { + tagName: 'mutation', + children: [], + paramname: sb2block[1], // Name of parameter. + shape: sb2block[2] // Shape - in 2.0, 'r' or 'b'. }; } return activeBlock; diff --git a/src/import/sb2specmap.js b/src/import/sb2specmap.js index 76e9683e..1cf0620f 100644 --- a/src/import/sb2specmap.js +++ b/src/import/sb2specmap.js @@ -752,9 +752,8 @@ var specMap = { 'opcode':'control_stop', 'argMap':[ { - 'type':'input', - 'inputOp':'control_stop_menu', - 'inputName':'STOP_OPTION' + 'type':'field', + 'fieldName':'STOP_OPTION' } ] }, @@ -1375,15 +1374,10 @@ var specMap = { }, 'procDef':{ 'opcode':'procedures_defnoreturn', - 'argMap':[ - { - 'type':'field', - 'fieldName':'NAME' - } - ] + 'argMap':[] }, 'getParam':{ - 'opcode':'proc_param', + 'opcode':'procedures_param', 'argMap':[] }, 'call':{ diff --git a/src/index.js b/src/index.js index fd987f4d..818ea4f1 100644 --- a/src/index.js +++ b/src/index.js @@ -44,6 +44,7 @@ function VirtualMachine () { }); this.blockListener = this.blockListener.bind(this); + this.flyoutBlockListener = this.flyoutBlockListener.bind(this); } /** @@ -72,6 +73,15 @@ VirtualMachine.prototype.stopAll = function () { this.runtime.stopAll(); }; +/** + * Clear out current running project data. + */ +VirtualMachine.prototype.clear = function () { + this.runtime.dispose(); + this.editingTarget = null; + this.emitTargetsUpdate(); +}; + /** * Get data for playground. Data comes back in an emitted event. */ @@ -115,6 +125,7 @@ VirtualMachine.prototype.postIOData = function (device, data) { * @param {?string} json JSON string representing the project. */ VirtualMachine.prototype.loadProject = function (json) { + this.clear(); // @todo: Handle other formats, e.g., Scratch 1.4, Scratch 3.0. sb2import(json, this.runtime); // Select the first target for editing, e.g., the stage. @@ -132,10 +143,10 @@ VirtualMachine.prototype.loadProject = function (json) { VirtualMachine.prototype.createEmptyProject = function () { // Stage. var blocks2 = new Blocks(); - var stage = new Sprite(blocks2); + var stage = new Sprite(blocks2, this.runtime); stage.name = 'Stage'; stage.costumes.push({ - skin: '/assets/stage.png', + skin: './assets/stage.png', name: 'backdrop1', bitmapResolution: 2, rotationCenterX: 480, @@ -151,10 +162,10 @@ VirtualMachine.prototype.createEmptyProject = function () { target2.isStage = true; // Sprite1 (cat). var blocks1 = new Blocks(); - var sprite = new Sprite(blocks1); + var sprite = new Sprite(blocks1, this.runtime); sprite.name = 'Sprite1'; sprite.costumes.push({ - skin: '/assets/scratch_cat.svg', + skin: './assets/scratch_cat.svg', name: 'costume1', bitmapResolution: 1, rotationCenterX: 47, @@ -186,14 +197,18 @@ VirtualMachine.prototype.attachRenderer = function (renderer) { */ VirtualMachine.prototype.blockListener = function (e) { if (this.editingTarget) { - this.editingTarget.blocks.blocklyListen( - e, - false, - this.runtime - ); + this.editingTarget.blocks.blocklyListen(e, this.runtime); } }; +/** + * Handle a Blockly event for the flyout. + * @param {!Blockly.Event} e Any Blockly event. + */ +VirtualMachine.prototype.flyoutBlockListener = function (e) { + this.runtime.flyoutBlocks.blocklyListen(e, this.runtime); +}; + /** * Set an editing target. An editor UI can use this function to switch * between editing different targets, sprites, etc. @@ -232,7 +247,7 @@ VirtualMachine.prototype.emitTargetsUpdate = function () { return [target.id, target.getName()]; }), // Currently editing target id. - editingTarget: this.editingTarget.id + editingTarget: this.editingTarget ? this.editingTarget.id : null }); }; diff --git a/src/sprites/clone.js b/src/sprites/clone.js index fd2a3e17..ed169523 100644 --- a/src/sprites/clone.js +++ b/src/sprites/clone.js @@ -438,9 +438,6 @@ Clone.prototype.onGreenFlag = function () { * Dispose of this clone, destroying any run-time properties. */ Clone.prototype.dispose = function () { - if (this.isOriginal) { // Don't allow a non-clone to delete itself. - return; - } this.runtime.changeCloneCounter(-1); if (this.renderer && this.drawableID !== null) { this.renderer.destroyDrawable(this.drawableID); diff --git a/src/util/math-util.js b/src/util/math-util.js index b53698ef..14ebb450 100644 --- a/src/util/math-util.js +++ b/src/util/math-util.js @@ -6,7 +6,7 @@ function MathUtil () {} * @return {!number} Equivalent value in radians. */ MathUtil.degToRad = function (deg) { - return (Math.PI * (90 - deg)) / 180; + return deg * Math.PI / 180; }; /** diff --git a/test/fixtures/default.json b/test/fixtures/default.json new file mode 100644 index 00000000..af478a4e --- /dev/null +++ b/test/fixtures/default.json @@ -0,0 +1,71 @@ +{ + "objName": "Stage", + "sounds": [{ + "soundName": "pop", + "soundID": -1, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + }], + "costumes": [{ + "costumeName": "backdrop1", + "baseLayerID": -1, + "baseLayerMD5": "739b5e2a2435f6e1ec2993791b423146.png", + "bitmapResolution": 1, + "rotationCenterX": 240, + "rotationCenterY": 180 + }], + "currentCostumeIndex": 0, + "penLayerMD5": "5c81a336fab8be57adc039a8a2b33ca9.png", + "penLayerID": -1, + "tempoBPM": 60, + "videoAlpha": 0.5, + "children": [{ + "objName": "Sprite1", + "sounds": [{ + "soundName": "meow", + "soundID": -1, + "md5": "83c36d806dc92327b9e7049a565c6bff.wav", + "sampleCount": 18688, + "rate": 22050, + "format": "" + }], + "costumes": [{ + "costumeName": "costume1", + "baseLayerID": -1, + "baseLayerMD5": "09dc888b0b7df19f70d81588ae73420e.svg", + "bitmapResolution": 1, + "rotationCenterX": 47, + "rotationCenterY": 55 + }, + { + "costumeName": "costume2", + "baseLayerID": -1, + "baseLayerMD5": "3696356a03a8d938318876a593572843.svg", + "bitmapResolution": 1, + "rotationCenterX": 47, + "rotationCenterY": 55 + }], + "currentCostumeIndex": 0, + "scratchX": 0, + "scratchY": 0, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "indexInLibrary": 1, + "visible": true, + "spriteInfo": { + } + }], + "info": { + "videoOn": false, + "userAgent": "Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_11_4) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/50.0.2661.102 Safari\/537.36", + "swfVersion": "v446", + "scriptCount": 0, + "spriteCount": 1, + "hasCloudData": false, + "flashVersion": "MAC 21,0,0,242" + } +} diff --git a/test/fixtures/demo.json b/test/fixtures/demo.json new file mode 100644 index 00000000..b10376ec --- /dev/null +++ b/test/fixtures/demo.json @@ -0,0 +1,359 @@ +{ + "objName": "Stage", + "variables": [{ + "name": "x", + "value": "1", + "isPersistent": false + }, + { + "name": "y", + "value": "1", + "isPersistent": false + }, + { + "name": "z", + "value": "1", + "isPersistent": false + }, + { + "name": "d", + "value": "1", + "isPersistent": false + }, + { + "name": "a", + "value": 4, + "isPersistent": false + }], + "lists": [{ + "listName": "D# Minor Pentatonic", + "contents": ["78", + "75", + "73", + "75", + "70", + "78", + "73", + "75", + "75", + "78", + "75", + "73", + "75", + "70", + "75", + "78", + "73", + "75", + "78", + "75", + "73", + "75", + "70", + "73", + "68", + "70", + "66", + "68", + "63"], + "isPersistent": false, + "x": 5, + "y": 32, + "width": 125, + "height": 206, + "visible": true + }], + "scripts": [[52, + 8, + [["whenIReceive", "start"], + ["setVar:to:", "a", "1"], + ["doRepeat", + ["lineCountOfList:", "D# Minor Pentatonic"], + [["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "a"], "D# Minor Pentatonic"], 0.5], ["changeVar:by:", "a", 1]]]]], + [53, + 186, + [["whenIReceive", "start"], + ["setVar:to:", "x", "1"], + ["rest:elapsed:from:", 7.25], + ["doRepeat", + ["lineCountOfList:", "D# Minor Pentatonic"], + [["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "x"], "D# Minor Pentatonic"], 0.25], ["changeVar:by:", "x", 1]]]]], + [48, + 557, + [["whenIReceive", "start"], + ["setVar:to:", "z", "1"], + ["rest:elapsed:from:", 13], + ["doRepeat", + ["lineCountOfList:", "D# Minor Pentatonic"], + [["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "z"], "D# Minor Pentatonic"], 0.0625], ["changeVar:by:", "z", 1]]]]], + [49, + 368, + [["whenIReceive", "start"], + ["setVar:to:", "y", "1"], + ["rest:elapsed:from:", 11], + ["doRepeat", + ["lineCountOfList:", "D# Minor Pentatonic"], + [["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "y"], "D# Minor Pentatonic"], 0.125], ["changeVar:by:", "y", 1]]]]], + [52, + 745, + [["whenIReceive", "start"], + ["setVar:to:", "d", "1"], + ["rest:elapsed:from:", 13.5], + ["doRepeat", + ["lineCountOfList:", "D# Minor Pentatonic"], + [["noteOn:duration:elapsed:from:", ["getLine:ofList:", ["readVariable", "d"], "D# Minor Pentatonic"], 0.03125], ["changeVar:by:", "d", 1]]]]]], + "sounds": [{ + "soundName": "pop", + "soundID": 0, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + }], + "costumes": [{ + "costumeName": "backdrop1", + "baseLayerID": 4, + "baseLayerMD5": "b61b1077b0ea1931abee9dbbfa7903ff.png", + "bitmapResolution": 2, + "rotationCenterX": 480, + "rotationCenterY": 360 + }], + "currentCostumeIndex": 0, + "penLayerMD5": "5c81a336fab8be57adc039a8a2b33ca9.png", + "penLayerID": 0, + "tempoBPM": 60, + "videoAlpha": 0.5, + "children": [{ + "objName": "Indicator", + "scripts": [[247.85, + 32.8, + [["procDef", "foo %n", ["bar"], [1], false], + ["hide"], + ["clearPenTrails"], + ["penColor:", 5968094], + ["say:", ["getParam", "bar", "r"]], + ["stopScripts", "this script"]]], + [41, 36, [["whenGreenFlag"], ["call", "foo %n", 1]]]], + "sounds": [{ + "soundName": "pop", + "soundID": 0, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + }], + "costumes": [{ + "costumeName": "costume1", + "baseLayerID": 1, + "baseLayerMD5": "d36f6603ec293d2c2198d3ea05109fe0.png", + "bitmapResolution": 2, + "rotationCenterX": 0, + "rotationCenterY": 0 + }], + "currentCostumeIndex": 0, + "scratchX": 22, + "scratchY": -26, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "indexInLibrary": 3, + "visible": false, + "spriteInfo": { + } + }, + { + "target": "Stage", + "cmd": "timer", + "param": null, + "color": 2926050, + "label": "timer", + "mode": 1, + "sliderMin": 0, + "sliderMax": 100, + "isDiscrete": true, + "x": 5, + "y": 5, + "visible": false + }, + { + "target": "Stage", + "cmd": "getVar:", + "param": "x", + "color": 15629590, + "label": "x", + "mode": 1, + "sliderMin": 0, + "sliderMax": 100, + "isDiscrete": true, + "x": 5, + "y": 268, + "visible": true + }, + { + "target": "Stage", + "cmd": "getVar:", + "param": "y", + "color": 15629590, + "label": "y", + "mode": 1, + "sliderMin": 0, + "sliderMax": 100, + "isDiscrete": true, + "x": 5, + "y": 295, + "visible": true + }, + { + "target": "Stage", + "cmd": "getVar:", + "param": "z", + "color": 15629590, + "label": "z", + "mode": 1, + "sliderMin": 0, + "sliderMax": 100, + "isDiscrete": true, + "x": 78, + "y": 268, + "visible": true + }, + { + "objName": "Play", + "scripts": [[32, 33, [["whenClicked"], ["broadcast:", "start"]]]], + "sounds": [{ + "soundName": "pop", + "soundID": 0, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + }], + "costumes": [{ + "costumeName": "costume1", + "baseLayerID": 2, + "baseLayerMD5": "30f811366ae3a53e6447932cc7f0212d.png", + "bitmapResolution": 2, + "rotationCenterX": 68, + "rotationCenterY": 115 + }], + "currentCostumeIndex": 0, + "scratchX": 2, + "scratchY": -48, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "indexInLibrary": 1, + "visible": true, + "spriteInfo": { + } + }, + { + "target": "Stage", + "cmd": "getVar:", + "param": "d", + "color": 15629590, + "label": "d", + "mode": 1, + "sliderMin": 0, + "sliderMax": 100, + "isDiscrete": true, + "x": 5, + "y": 241, + "visible": true + }, + { + "target": "Stage", + "cmd": "getVar:", + "param": "a", + "color": 15629590, + "label": "a", + "mode": 1, + "sliderMin": 0, + "sliderMax": 100, + "isDiscrete": true, + "x": 78, + "y": 241, + "visible": true + }, + { + "objName": "Stop", + "scripts": [[45, 104, [["whenClicked"], ["stopScripts", "all"]]]], + "sounds": [{ + "soundName": "pop", + "soundID": 0, + "md5": "83a9787d4cb6f3b7632b4ddfebf74367.wav", + "sampleCount": 258, + "rate": 11025, + "format": "" + }], + "costumes": [{ + "costumeName": "costume1", + "baseLayerID": 3, + "baseLayerMD5": "3de406f265b8d664406adf7c70762514.png", + "bitmapResolution": 2, + "rotationCenterX": 68, + "rotationCenterY": 70 + }], + "currentCostumeIndex": 0, + "scratchX": 121, + "scratchY": -33, + "scale": 1, + "direction": 90, + "rotationStyle": "normal", + "isDraggable": false, + "indexInLibrary": 2, + "visible": true, + "spriteInfo": { + } + }, + { + "listName": "D# Minor Pentatonic", + "contents": ["78", + "75", + "73", + "75", + "70", + "78", + "73", + "75", + "75", + "78", + "75", + "73", + "75", + "70", + "75", + "78", + "73", + "75", + "78", + "75", + "73", + "75", + "70", + "73", + "68", + "70", + "66", + "68", + "63"], + "isPersistent": false, + "x": 5, + "y": 32, + "width": 125, + "height": 206, + "visible": true + }], + "info": { + "spriteCount": 3, + "projectID": "118381369", + "videoOn": false, + "hasCloudData": false, + "userAgent": "Mozilla\/5.0 (Macintosh; Intel Mac OS X 10_12_0) AppleWebKit\/537.36 (KHTML, like Gecko) Chrome\/53.0.2785.143 Safari\/537.36", + "scriptCount": 9, + "flashVersion": "MAC 23,0,0,185", + "swfVersion": "v450.1" + } +} diff --git a/test/unit/import_sb2.js b/test/unit/import_sb2.js new file mode 100644 index 00000000..be4d6286 --- /dev/null +++ b/test/unit/import_sb2.js @@ -0,0 +1,88 @@ +var fs = require('fs'); +var path = require('path'); +var test = require('tap').test; + +var clone = require('../../src/sprites/clone'); +var runtime = require('../../src/engine/runtime'); +var sb2 = require('../../src/import/sb2import'); + +test('spec', function (t) { + t.type(sb2, 'function'); + t.end(); +}); + +test('default', function (t) { + // Get SB2 JSON (string) + var uri = path.resolve(__dirname, '../fixtures/default.json'); + var file = fs.readFileSync(uri, 'utf8'); + + // Create runtime instance & load SB2 into it + var rt = new runtime(); + sb2(file, rt); + + // Test + t.type(file, 'string'); + t.type(rt, 'object'); + t.type(rt.targets, 'object'); + + t.ok(rt.targets[0] instanceof clone); + t.type(rt.targets[0].id, 'string'); + t.type(rt.targets[0].blocks, 'object'); + t.type(rt.targets[0].variables, 'object'); + t.type(rt.targets[0].lists, 'object'); + + t.equal(rt.targets[0].isOriginal, true); + t.equal(rt.targets[0].currentCostume, 0); + t.equal(rt.targets[0].isOriginal, true); + t.equal(rt.targets[0].isStage, true); + + t.ok(rt.targets[1] instanceof clone); + t.type(rt.targets[1].id, 'string'); + t.type(rt.targets[1].blocks, 'object'); + t.type(rt.targets[1].variables, 'object'); + t.type(rt.targets[1].lists, 'object'); + + t.equal(rt.targets[1].isOriginal, true); + t.equal(rt.targets[1].currentCostume, 0); + t.equal(rt.targets[1].isOriginal, true); + t.equal(rt.targets[1].isStage, false); + t.end(); +}); + +test('demo', function (t) { + // Get SB2 JSON (string) + var uri = path.resolve(__dirname, '../fixtures/demo.json'); + var file = fs.readFileSync(uri, 'utf8'); + + // Create runtime instance & load SB2 into it + var rt = new runtime(); + sb2(file, rt); + + // Test + t.type(file, 'string'); + t.type(rt, 'object'); + t.type(rt.targets, 'object'); + + t.ok(rt.targets[0] instanceof clone); + t.type(rt.targets[0].id, 'string'); + t.type(rt.targets[0].blocks, 'object'); + t.type(rt.targets[0].variables, 'object'); + t.type(rt.targets[0].lists, 'object'); + + t.equal(rt.targets[0].isOriginal, true); + t.equal(rt.targets[0].currentCostume, 0); + t.equal(rt.targets[0].isOriginal, true); + t.equal(rt.targets[0].isStage, true); + + t.ok(rt.targets[1] instanceof clone); + t.type(rt.targets[1].id, 'string'); + t.type(rt.targets[1].blocks, 'object'); + t.type(rt.targets[1].variables, 'object'); + t.type(rt.targets[1].lists, 'object'); + + t.equal(rt.targets[1].isOriginal, true); + t.equal(rt.targets[1].currentCostume, 0); + t.equal(rt.targets[1].isOriginal, true); + t.equal(rt.targets[1].isStage, false); + t.end(); +}); diff --git a/test/unit/util_math.js b/test/unit/util_math.js index b282e90c..b198ef1d 100644 --- a/test/unit/util_math.js +++ b/test/unit/util_math.js @@ -2,12 +2,11 @@ var test = require('tap').test; var math = require('../../src/util/math-util'); test('degToRad', function (t) { - // @todo This is incorrect - t.strictEqual(math.degToRad(0), 1.5707963267948966); - t.strictEqual(math.degToRad(1), 1.5533430342749535); - t.strictEqual(math.degToRad(180), -1.5707963267948966); - t.strictEqual(math.degToRad(360), -4.71238898038469); - t.strictEqual(math.degToRad(720), -10.995574287564276); + t.strictEqual(math.degToRad(0), 0); + t.strictEqual(math.degToRad(1), 0.017453292519943295); + t.strictEqual(math.degToRad(180), Math.PI); + t.strictEqual(math.degToRad(360), 2 * Math.PI); + t.strictEqual(math.degToRad(720), 4 * Math.PI); t.end(); }); diff --git a/webpack.config.js b/webpack.config.js index 6d08c73a..d46f55bf 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -52,6 +52,7 @@ module.exports = [ entry: { 'dist': './src/index.js' }, + output: { library: 'VirtualMachine', libraryTarget: 'commonjs2', @@ -108,6 +109,9 @@ module.exports = [ to: 'media' }, { from: 'node_modules/highlightjs/styles/zenburn.css' + }, { + from: 'assets', + to: 'assets' }]) ]) })