diff --git a/src/blocks/scratch3_pen.js b/src/blocks/scratch3_pen.js index d2b63ad7c..c6866f25d 100644 --- a/src/blocks/scratch3_pen.js +++ b/src/blocks/scratch3_pen.js @@ -237,7 +237,7 @@ class Scratch3PenBlocks { } /** - * @returns {{id: string, name: string, blocks: []}} metadata for this extension and its blocks. + * @returns {object} metadata for this extension and its blocks. */ getInfo () { return { diff --git a/src/blocks/scratch3_wedo2.js b/src/blocks/scratch3_wedo2.js index d79ef9096..bc0750f56 100644 --- a/src/blocks/scratch3_wedo2.js +++ b/src/blocks/scratch3_wedo2.js @@ -1,3 +1,5 @@ +const ArgumentType = require('../extension-support/argument-type'); +const BlockType = require('../extension-support/block-type'); const color = require('../util/color'); const log = require('../util/log'); @@ -398,6 +400,183 @@ class Scratch3WeDo2Blocks { this.runtime.HACK_WeDo2Blocks = this; } + /** + * @returns {object} metadata for this extension and its blocks. + */ + getInfo () { + return { + id: 'wedo2', + name: 'WeDo 2.0', + blocks: [ + { + opcode: 'motorOnFor', + text: 'turn [MOTOR_ID] on for [DURATION] seconds', + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'motorID', + defaultValue: MotorID.DEFAULT + }, + DURATION: { + type: ArgumentType.NUMBER, + defaultValue: 1 + } + } + }, + { + opcode: 'motorOn', + text: 'turn [MOTOR_ID] on', + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'motorID', + defaultValue: MotorID.DEFAULT + } + } + }, + { + opcode: 'motorOff', + text: 'turn [MOTOR_ID] off', + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'motorID', + defaultValue: MotorID.DEFAULT + } + } + }, + { + opcode: 'startMotorPower', + text: 'set [MOTOR_ID] power to [POWER]', + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'motorID', + defaultValue: MotorID.DEFAULT + }, + POWER: { + type: ArgumentType.NUMBER, + defaultValue: 100 + } + } + }, + { + opcode: 'startMotorDirection', + text: 'set [MOTOR_ID] direction to [DIRECTION]', + blockType: BlockType.COMMAND, + arguments: { + MOTOR_ID: { + type: ArgumentType.STRING, + menu: 'motorID', + defaultValue: MotorID.DEFAULT + }, + DIRECTION: { + type: ArgumentType.STRING, + menu: 'motorDirection', + defaultValue: MotorDirection.FORWARD + } + } + }, + { + opcode: 'setLightHue', + text: 'set light color to [HUE]', + blockType: BlockType.COMMAND, + arguments: { + HUE: { + type: ArgumentType.NUMBER, + defaultValue: 50 + } + } + }, + { + opcode: 'playNoteFor', + text: 'play note [NOTE] for [DURATION] seconds', + blockType: BlockType.COMMAND, + arguments: { + NOTE: { + type: ArgumentType.NUMBER, // TODO: ArgumentType.MIDI_NOTE? + defaultValue: 60 + }, + DURATION: { + type: ArgumentType.NUMBER, + defaultValue: 0.5 + } + } + }, + { + opcode: 'whenDistance', + text: 'when distance [OP] [REFERENCE]', + blockType: BlockType.HAT, + arguments: { + OP: { + type: ArgumentType.STRING, + menu: 'lessMore', + defaultValue: '<' + }, + REFERENCE: { + type: ArgumentType.NUMBER, + defaultValue: 50 + } + } + }, + { + opcode: 'whenTilted', + text: 'when tilted [DIRECTION]', + func: 'isTilted', + blockType: BlockType.HAT, + arguments: { + DIRECTION: { + type: ArgumentType.STRING, + menu: 'tiltDirectionAny', + defaultValue: TiltDirection.ANY + } + } + }, + { + opcode: 'getDistance', + text: 'distance', + blockType: BlockType.REPORTER + }, + { + opcode: 'isTilted', + text: 'tilted [DIRECTION]?', + blockType: BlockType.REPORTER, + arguments: { + DIRECTION: { + type: ArgumentType.STRING, + menu: 'tiltDirectionAny', + defaultValue: TiltDirection.ANY + } + } + }, + { + opcode: 'getTiltAngle', + text: 'tilt angle [DIRECTION]', + blockType: BlockType.REPORTER, + arguments: { + DIRECTION: { + type: ArgumentType.STRING, + menu: 'tiltDirection', + defaultValue: TiltDirection.UP + } + } + } + ], + menus: { + motorID: [MotorID.DEFAULT, MotorID.A, MotorID.B, MotorID.ALL], + motorDirection: [MotorDirection.FORWARD, MotorDirection.BACKWARD, MotorDirection.REVERSE], + tiltDirection: [TiltDirection.UP, TiltDirection.DOWN, TiltDirection.LEFT, TiltDirection.RIGHT], + tiltDirectionAny: + [TiltDirection.UP, TiltDirection.DOWN, TiltDirection.LEFT, TiltDirection.RIGHT, TiltDirection.ANY], + lessMore: ['<', '>'] + } + }; + } + /** * Use the Device Manager client to attempt to connect to a WeDo 2.0 device. */ @@ -427,27 +606,6 @@ class Scratch3WeDo2Blocks { }); } - /** - * Retrieve the block primitives implemented by this package. - * @return {object.} Mapping of opcode to Function. - */ - getPrimitives () { - return { - wedo2_motorOnFor: this.motorOnFor, - wedo2_motorOn: this.motorOn, - wedo2_motorOff: this.motorOff, - wedo2_startMotorPower: this.startMotorPower, - wedo2_setMotorDirection: this.setMotorDirection, - wedo2_setLightHue: this.setLightHue, - wedo2_playNoteFor: this.playNoteFor, - wedo2_whenDistance: this.whenDistance, - wedo2_whenTilted: this.whenTilted, - wedo2_getDistance: this.getDistance, - wedo2_isTilted: this.isTilted, - wedo2_getTiltAngle: this.getTiltAngle - }; - } - /** * Turn specified motor(s) on for a specified duration. * @param {object} args - the block's arguments. diff --git a/src/extension-support/extension-manager.js b/src/extension-support/extension-manager.js index 827bc491d..d5a3e07ea 100644 --- a/src/extension-support/extension-manager.js +++ b/src/extension-support/extension-manager.js @@ -3,6 +3,16 @@ const log = require('../util/log'); const BlockType = require('./block-type'); +// These extensions are currently built into the VM repository but should not be loaded at startup. +// TODO: move these out into a separate repository? +// TODO: change extension spec so that library info, including extension ID, can be collected through static methods +const Scratch3PenBlocks = require('../blocks/scratch3_pen'); +const Scratch3WeDo2Blocks = require('../blocks/scratch3_wedo2'); +const builtinExtensions = { + pen: Scratch3PenBlocks, + wedo2: Scratch3WeDo2Blocks +}; + /** * @typedef {object} ArgumentInfo - Information about an extension block argument * @property {ArgumentType} type - the type of value this argument can take @@ -39,7 +49,7 @@ const BlockType = require('./block-type'); */ class ExtensionManager { - constructor () { + constructor (runtime) { /** * The ID number to provide to the next extension worker. * @type {int} @@ -60,17 +70,30 @@ class ExtensionManager { */ this.pendingWorkers = []; + /** + * Keep a reference to the runtime so we can construct internal extension objects. + * TODO: remove this in favor of extensions accessing the runtime as a service. + * @type {Runtime} + */ + this.runtime = runtime; + dispatch.setService('extensions', this).catch(e => { log.error(`ExtensionManager was unable to register extension service: ${JSON.stringify(e)}`); }); } /** - * Load an extension by URL - * @param {string} extensionURL - the URL for the extension to load + * Load an extension by URL or internal extension ID + * @param {string} extensionURL - the URL for the extension to load OR the ID of an internal extension * @returns {Promise} resolved once the extension is loaded and initialized or rejected on failure */ loadExtensionURL (extensionURL) { + if (builtinExtensions.hasOwnProperty(extensionURL)) { + const extension = builtinExtensions[extensionURL]; + const extensionInstance = new extension(this.runtime); + return this._registerInternalExtension(extensionInstance); + } + return new Promise((resolve, reject) => { // If we `require` this at the global level it breaks non-webpack targets, including tests const ExtensionWorker = require('worker-loader?name=extension-worker.js!./extension-worker'); diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 721477f09..eec4d0752 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -68,7 +68,7 @@ class VirtualMachine extends EventEmitter { this.emit(Runtime.EXTENSION_ADDED, blocksInfo); }); - this.extensionManager = new ExtensionManager(); + this.extensionManager = new ExtensionManager(this.runtime); this.blockListener = this.blockListener.bind(this); this.flyoutBlockListener = this.flyoutBlockListener.bind(this);