Make loadExtensionURL handle built-in extensions

WeDo2 and Pen blocks have been converted to internal extensions, and can
now be loaded by giving `loadExtensionURL` the string 'pen' or 'wedo2'
instead of an actual URL.
This commit is contained in:
Christopher Willis-Ford 2017-10-04 12:16:27 -07:00
parent 6757fb6de9
commit e9aed49a05
4 changed files with 207 additions and 26 deletions

View file

@ -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 {

View file

@ -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.<string, Function>} 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.

View file

@ -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');

View file

@ -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);