mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-07-07 03:24:04 -04:00
Merge branch 'develop' into bugfix/npm-install
This commit is contained in:
commit
908c34bcf4
16 changed files with 868 additions and 25 deletions
|
@ -45,7 +45,7 @@
|
||||||
"scratch-audio": "latest",
|
"scratch-audio": "latest",
|
||||||
"scratch-blocks": "latest",
|
"scratch-blocks": "latest",
|
||||||
"scratch-render": "latest",
|
"scratch-render": "latest",
|
||||||
"scratch-storage": "^0.1.0",
|
"scratch-storage": "^0.2.0",
|
||||||
"script-loader": "0.7.0",
|
"script-loader": "0.7.0",
|
||||||
"socket.io-client": "1.7.3",
|
"socket.io-client": "1.7.3",
|
||||||
"stats.js": "^0.17.0",
|
"stats.js": "^0.17.0",
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
const Cast = require('../util/cast');
|
const Cast = require('../util/cast');
|
||||||
const Timer = require('../util/timer');
|
|
||||||
|
|
||||||
class Scratch3ControlBlocks {
|
class Scratch3ControlBlocks {
|
||||||
constructor (runtime) {
|
constructor (runtime) {
|
||||||
|
@ -73,18 +72,13 @@ class Scratch3ControlBlocks {
|
||||||
util.startBranch(1, true);
|
util.startBranch(1, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
wait (args, util) {
|
wait (args) {
|
||||||
if (!util.stackFrame.timer) {
|
const duration = Math.max(0, 1000 * Cast.toNumber(args.DURATION));
|
||||||
util.stackFrame.timer = new Timer();
|
return new Promise(resolve => {
|
||||||
util.stackFrame.timer.start();
|
setTimeout(() => {
|
||||||
util.yield();
|
resolve();
|
||||||
this.runtime.requestRedraw();
|
}, duration);
|
||||||
} else {
|
});
|
||||||
const duration = Math.max(0, 1000 * Cast.toNumber(args.DURATION));
|
|
||||||
if (util.stackFrame.timer.timeElapsed() < duration) {
|
|
||||||
util.yield();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (args, util) {
|
if (args, util) {
|
||||||
|
|
700
src/blocks/scratch3_wedo2.js
Normal file
700
src/blocks/scratch3_wedo2.js
Normal file
|
@ -0,0 +1,700 @@
|
||||||
|
const color = require('../util/color');
|
||||||
|
const log = require('../util/log');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage power, direction, and timers for one WeDo 2.0 motor.
|
||||||
|
*/
|
||||||
|
class WeDo2Motor {
|
||||||
|
/**
|
||||||
|
* Construct a WeDo2Motor instance.
|
||||||
|
* @param {WeDo2} parent - the WeDo 2.0 device which owns this motor.
|
||||||
|
* @param {int} index - the zero-based index of this motor on its parent device.
|
||||||
|
*/
|
||||||
|
constructor (parent, index) {
|
||||||
|
/**
|
||||||
|
* The WeDo 2.0 device which owns this motor.
|
||||||
|
* @type {WeDo2}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._parent = parent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The zero-based index of this motor on its parent device.
|
||||||
|
* @type {int}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._index = index;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This motor's current direction: 1 for "this way" or -1 for "that way"
|
||||||
|
* @type {number}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._direction = 1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This motor's current power level, in the range [0,100].
|
||||||
|
* @type {number}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._power = 100;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this motor currently moving?
|
||||||
|
* @type {boolean}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._isOn = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If the motor has been turned on or is actively braking for a specific duration, this is the timeout ID for
|
||||||
|
* the end-of-action handler. Cancel this when changing plans.
|
||||||
|
* @type {Object}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._pendingTimeoutId = null;
|
||||||
|
|
||||||
|
this.startBraking = this.startBraking.bind(this);
|
||||||
|
this.setMotorOff = this.setMotorOff.bind(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number} - the duration of active braking after a call to startBraking(). Afterward, turn the motor off.
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
static get BRAKE_TIME_MS () {
|
||||||
|
return 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {int} - this motor's current direction: 1 for "this way" or -1 for "that way"
|
||||||
|
*/
|
||||||
|
get direction () {
|
||||||
|
return this._direction;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {int} value - this motor's new direction: 1 for "this way" or -1 for "that way"
|
||||||
|
*/
|
||||||
|
set direction (value) {
|
||||||
|
if (value < 0) {
|
||||||
|
this._direction = -1;
|
||||||
|
} else {
|
||||||
|
this._direction = 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {int} - this motor's current power level, in the range [0,100].
|
||||||
|
*/
|
||||||
|
get power () {
|
||||||
|
return this._power;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {int} value - this motor's new power level, in the range [0,100].
|
||||||
|
*/
|
||||||
|
set power (value) {
|
||||||
|
this._power = Math.max(0, Math.min(value, 100));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {boolean} - true if this motor is currently moving, false if this motor is off or braking.
|
||||||
|
*/
|
||||||
|
get isOn () {
|
||||||
|
return this._isOn;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn this motor on indefinitely.
|
||||||
|
*/
|
||||||
|
setMotorOn () {
|
||||||
|
this._parent._send('motorOn', {motorIndex: this._index, power: this._direction * this._power});
|
||||||
|
this._isOn = true;
|
||||||
|
this._clearTimeout();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn this motor on for a specific duration.
|
||||||
|
* @param {number} milliseconds - run the motor for this long.
|
||||||
|
*/
|
||||||
|
setMotorOnFor (milliseconds) {
|
||||||
|
milliseconds = Math.max(0, milliseconds);
|
||||||
|
this.setMotorOn();
|
||||||
|
this._setNewTimeout(this.startBraking, milliseconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start active braking on this motor. After a short time, the motor will turn off.
|
||||||
|
*/
|
||||||
|
startBraking () {
|
||||||
|
this._parent._send('motorBrake', {motorIndex: this._index});
|
||||||
|
this._isOn = false;
|
||||||
|
this._setNewTimeout(this.setMotorOff, WeDo2Motor.BRAKE_TIME_MS);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn this motor off.
|
||||||
|
*/
|
||||||
|
setMotorOff () {
|
||||||
|
this._parent._send('motorOff', {motorIndex: this._index});
|
||||||
|
this._isOn = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear the motor action timeout, if any. Safe to call even when there is no pending timeout.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_clearTimeout () {
|
||||||
|
if (this._pendingTimeoutId !== null) {
|
||||||
|
clearTimeout(this._pendingTimeoutId);
|
||||||
|
this._pendingTimeoutId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set a new motor action timeout, after clearing an existing one if necessary.
|
||||||
|
* @param {Function} callback - to be called at the end of the timeout.
|
||||||
|
* @param {int} delay - wait this many milliseconds before calling the callback.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_setNewTimeout (callback, delay) {
|
||||||
|
this._clearTimeout();
|
||||||
|
const timeoutID = setTimeout(() => {
|
||||||
|
if (this._pendingTimeoutId === timeoutID) {
|
||||||
|
this._pendingTimeoutId = null;
|
||||||
|
}
|
||||||
|
callback();
|
||||||
|
}, delay);
|
||||||
|
this._pendingTimeoutId = timeoutID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manage communication with a WeDo 2.0 device over a Device Manager client socket.
|
||||||
|
*/
|
||||||
|
class WeDo2 {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string} - the type of Device Manager device socket that this class will handle.
|
||||||
|
*/
|
||||||
|
static get DEVICE_TYPE () {
|
||||||
|
return 'wedo2';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a WeDo2 communication object.
|
||||||
|
* @param {Socket} socket - the socket for a WeDo 2.0 device, as provided by a Device Manager client.
|
||||||
|
*/
|
||||||
|
constructor (socket) {
|
||||||
|
/**
|
||||||
|
* The socket-IO socket used to communicate with the Device Manager about this device.
|
||||||
|
* @type {Socket}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._socket = socket;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The motors which this WeDo 2.0 could possibly have.
|
||||||
|
* @type {[WeDo2Motor]}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._motors = [new WeDo2Motor(this, 0), new WeDo2Motor(this, 1)];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The most recently received value for each sensor.
|
||||||
|
* @type {Object.<string, number>}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
this._sensors = {
|
||||||
|
tiltX: 0,
|
||||||
|
tiltY: 0,
|
||||||
|
distance: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
this._onSensorChanged = this._onSensorChanged.bind(this);
|
||||||
|
this._onDisconnect = this._onDisconnect.bind(this);
|
||||||
|
|
||||||
|
this._connectEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manually dispose of this object.
|
||||||
|
*/
|
||||||
|
dispose () {
|
||||||
|
this._disconnectEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number} - the latest value received for the tilt sensor's tilt about the X axis.
|
||||||
|
*/
|
||||||
|
get tiltX () {
|
||||||
|
return this._sensors.tiltX;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number} - the latest value received for the tilt sensor's tilt about the Y axis.
|
||||||
|
*/
|
||||||
|
get tiltY () {
|
||||||
|
return this._sensors.tiltY;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number} - the latest value received from the distance sensor.
|
||||||
|
*/
|
||||||
|
get distance () {
|
||||||
|
return this._sensors.distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access a particular motor on this device.
|
||||||
|
* @param {int} index - the zero-based index of the desired motor.
|
||||||
|
* @return {WeDo2Motor} - the WeDo2Motor instance, if any, at that index.
|
||||||
|
*/
|
||||||
|
motor (index) {
|
||||||
|
return this._motors[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the WeDo 2.0 hub's LED to a specific color.
|
||||||
|
* @param {int} rgb - a 24-bit RGB color in 0xRRGGBB format.
|
||||||
|
*/
|
||||||
|
setLED (rgb) {
|
||||||
|
this._send('setLED', {rgb});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Play a tone from the WeDo 2.0 hub for a specific amount of time.
|
||||||
|
* @param {int} tone - the pitch of the tone, in Hz.
|
||||||
|
* @param {int} milliseconds - the duration of the note, in milliseconds.
|
||||||
|
*/
|
||||||
|
playTone (tone, milliseconds) {
|
||||||
|
this._send('playTone', {tone, ms: milliseconds});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the tone playing from the WeDo 2.0 hub, if any.
|
||||||
|
*/
|
||||||
|
stopTone () {
|
||||||
|
this._send('stopTone');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attach event handlers to the device socket.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_connectEvents () {
|
||||||
|
this._socket.on('sensorChanged', this._onSensorChanged);
|
||||||
|
this._socket.on('deviceWasClosed', this._onDisconnect);
|
||||||
|
this._socket.on('disconnect', this._onDisconnect);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detach event handlers from the device socket.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_disconnectEvents () {
|
||||||
|
this._socket.off('sensorChanged', this._onSensorChanged);
|
||||||
|
this._socket.off('deviceWasClosed', this._onDisconnect);
|
||||||
|
this._socket.off('disconnect', this._onDisconnect);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store the sensor value from an incoming 'sensorChanged' event.
|
||||||
|
* @param {object} event - the 'sensorChanged' event.
|
||||||
|
* @property {string} sensorName - the name of the sensor which changed.
|
||||||
|
* @property {number} sensorValue - the new value of the sensor.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onSensorChanged (event) {
|
||||||
|
this._sensors[event.sensorName] = event.sensorValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* React to device disconnection. May be called more than once.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_onDisconnect () {
|
||||||
|
this._disconnectEvents();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a message to the device socket.
|
||||||
|
* @param {string} message - the name of the message, such as 'playTone'.
|
||||||
|
* @param {object} [details] - optional additional details for the message, such as tone duration and pitch.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_send (message, details) {
|
||||||
|
this._socket.emit(message, details);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum for motor specification.
|
||||||
|
* @readonly
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
const MotorID = {
|
||||||
|
DEFAULT: 'motor',
|
||||||
|
A: 'motor A',
|
||||||
|
B: 'motor B',
|
||||||
|
ALL: 'all motors'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum for motor direction specification.
|
||||||
|
* @readonly
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
const MotorDirection = {
|
||||||
|
FORWARD: 'this way',
|
||||||
|
BACKWARD: 'that way',
|
||||||
|
REVERSE: 'reverse'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enum for tilt sensor direction.
|
||||||
|
* @readonly
|
||||||
|
* @enum {string}
|
||||||
|
*/
|
||||||
|
const TiltDirection = {
|
||||||
|
UP: 'up',
|
||||||
|
DOWN: 'down',
|
||||||
|
LEFT: 'left',
|
||||||
|
RIGHT: 'right',
|
||||||
|
ANY: 'any'
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scratch 3.0 blocks to interact with a LEGO WeDo 2.0 device.
|
||||||
|
*/
|
||||||
|
class Scratch3WeDo2Blocks {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {string} - the name of this extension.
|
||||||
|
*/
|
||||||
|
static get EXTENSION_NAME () {
|
||||||
|
return 'wedo2';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number} - the tilt sensor counts as "tilted" if its tilt angle meets or exceeds this threshold.
|
||||||
|
*/
|
||||||
|
static get TILT_THRESHOLD () {
|
||||||
|
return 15;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a set of WeDo 2.0 blocks.
|
||||||
|
* @param {Runtime} runtime - the Scratch 3.0 runtime.
|
||||||
|
*/
|
||||||
|
constructor (runtime) {
|
||||||
|
/**
|
||||||
|
* The Scratch 3.0 runtime.
|
||||||
|
* @type {Runtime}
|
||||||
|
*/
|
||||||
|
this.runtime = runtime;
|
||||||
|
|
||||||
|
this.runtime.HACK_WeDo2Blocks = this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use the Device Manager client to attempt to connect to a WeDo 2.0 device.
|
||||||
|
*/
|
||||||
|
connect () {
|
||||||
|
if (this._device || this._finder) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const deviceManager = this.runtime.ioDevices.deviceManager;
|
||||||
|
const finder = this._finder =
|
||||||
|
deviceManager.searchAndConnect(Scratch3WeDo2Blocks.EXTENSION_NAME, WeDo2.DEVICE_TYPE);
|
||||||
|
this._finder.promise.then(
|
||||||
|
socket => {
|
||||||
|
if (this._finder === finder) {
|
||||||
|
this._finder = null;
|
||||||
|
this._device = new WeDo2(socket);
|
||||||
|
} else {
|
||||||
|
log.warn('Ignoring success from stale WeDo 2.0 connection attempt');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
reason => {
|
||||||
|
if (this._finder === finder) {
|
||||||
|
this._finder = null;
|
||||||
|
log.warn(`WeDo 2.0 connection failed: ${reason}`);
|
||||||
|
} else {
|
||||||
|
log.warn('Ignoring failure from stale WeDo 2.0 connection attempt');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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.
|
||||||
|
* @property {MotorID} MOTOR_ID - the motor(s) to activate.
|
||||||
|
* @property {int} DURATION - the amount of time to run the motors.
|
||||||
|
* @return {Promise} - a promise which will resolve at the end of the duration.
|
||||||
|
*/
|
||||||
|
motorOnFor (args) {
|
||||||
|
const durationMS = args.DURATION * 1000;
|
||||||
|
return new Promise(resolve => {
|
||||||
|
this._forEachMotor(args.MOTOR_ID, motorIndex => {
|
||||||
|
this._device.motor(motorIndex).setMotorOnFor(durationMS);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Ensure this block runs for a fixed amount of time even when no device is connected.
|
||||||
|
setTimeout(resolve, durationMS);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn specified motor(s) on indefinitely.
|
||||||
|
* @param {object} args - the block's arguments.
|
||||||
|
* @property {MotorID} MOTOR_ID - the motor(s) to activate.
|
||||||
|
*/
|
||||||
|
motorOn (args) {
|
||||||
|
this._forEachMotor(args.MOTOR_ID, motorIndex => {
|
||||||
|
this._device.motor(motorIndex).setMotorOn();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn specified motor(s) off.
|
||||||
|
* @param {object} args - the block's arguments.
|
||||||
|
* @property {MotorID} MOTOR_ID - the motor(s) to deactivate.
|
||||||
|
*/
|
||||||
|
motorOff (args) {
|
||||||
|
this._forEachMotor(args.MOTOR_ID, motorIndex => {
|
||||||
|
this._device.motor(motorIndex).setMotorOff();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Turn specified motor(s) off.
|
||||||
|
* @param {object} args - the block's arguments.
|
||||||
|
* @property {MotorID} MOTOR_ID - the motor(s) to be affected.
|
||||||
|
* @property {int} POWER - the new power level for the motor(s).
|
||||||
|
*/
|
||||||
|
startMotorPower (args) {
|
||||||
|
this._forEachMotor(args.MOTOR_ID, motorIndex => {
|
||||||
|
const motor = this._device.motor(motorIndex);
|
||||||
|
motor.power = args.POWER;
|
||||||
|
motor.setMotorOn();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the direction of rotation for specified motor(s).
|
||||||
|
* If the direction is 'reverse' the motor(s) will be reversed individually.
|
||||||
|
* @param {object} args - the block's arguments.
|
||||||
|
* @property {MotorID} MOTOR_ID - the motor(s) to be affected.
|
||||||
|
* @property {MotorDirection} DIRECTION - the new direction for the motor(s).
|
||||||
|
*/
|
||||||
|
setMotorDirection (args) {
|
||||||
|
this._forEachMotor(args.MOTOR_ID, motorIndex => {
|
||||||
|
const motor = this._device.motor(motorIndex);
|
||||||
|
switch (args.DIRECTION) {
|
||||||
|
case MotorDirection.FORWARD:
|
||||||
|
motor.direction = 1;
|
||||||
|
break;
|
||||||
|
case MotorDirection.BACKWARD:
|
||||||
|
motor.direction = -1;
|
||||||
|
break;
|
||||||
|
case MotorDirection.REVERSE:
|
||||||
|
motor.direction = -motor.direction;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.warn(`Unknown motor direction in setMotorDirection: ${args.DIRECTION}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the LED's hue.
|
||||||
|
* @param {object} args - the block's arguments.
|
||||||
|
* @property {number} HUE - the hue to set, in the range [0,100].
|
||||||
|
*/
|
||||||
|
setLightHue (args) {
|
||||||
|
// Convert from [0,100] to [0,360]
|
||||||
|
const hue = args.HUE * 360 / 100;
|
||||||
|
|
||||||
|
const rgbObject = color.hsvToRgb({h: hue, s: 1, v: 1});
|
||||||
|
|
||||||
|
const rgbDecimal = color.rgbToDecimal(rgbObject);
|
||||||
|
|
||||||
|
this._device.setLED(rgbDecimal);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make the WeDo 2.0 hub play a MIDI note for the specified duration.
|
||||||
|
* @param {object} args - the block's arguments.
|
||||||
|
* @property {number} NOTE - the MIDI note to play.
|
||||||
|
* @property {number} DURATION - the duration of the note, in seconds.
|
||||||
|
* @return {Promise} - a promise which will resolve at the end of the duration.
|
||||||
|
*/
|
||||||
|
playNoteFor (args) {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const durationMS = args.DURATION * 1000;
|
||||||
|
const tone = this._noteToTone(args.NOTE);
|
||||||
|
this._device.playTone(tone, durationMS);
|
||||||
|
|
||||||
|
// Ensure this block runs for a fixed amount of time even when no device is connected.
|
||||||
|
setTimeout(resolve, durationMS);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare the distance sensor's value to a reference.
|
||||||
|
* @param {object} args - the block's arguments.
|
||||||
|
* @property {string} OP - the comparison operation: '<' or '>'.
|
||||||
|
* @property {number} REFERENCE - the value to compare against.
|
||||||
|
* @return {boolean} - the result of the comparison, or false on error.
|
||||||
|
*/
|
||||||
|
whenDistance (args) {
|
||||||
|
switch (args.OP) {
|
||||||
|
case '<':
|
||||||
|
return this._device.distance < args.REFERENCE;
|
||||||
|
case '>':
|
||||||
|
return this._device.distance > args.REFERENCE;
|
||||||
|
default:
|
||||||
|
log.warn(`Unknown comparison operator in whenDistance: ${args.OP}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether the tilt sensor is currently tilted.
|
||||||
|
* @param {object} args - the block's arguments.
|
||||||
|
* @property {TiltDirection} DIRECTION - the tilt direction to test (up, down, left, right, or any).
|
||||||
|
* @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction.
|
||||||
|
*/
|
||||||
|
whenTilted (args) {
|
||||||
|
return this._isTilted(args.DIRECTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return {number} - the distance sensor's value, scaled to the [0,100] range.
|
||||||
|
*/
|
||||||
|
getDistance () {
|
||||||
|
return this._device.distance * 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether the tilt sensor is currently tilted.
|
||||||
|
* @param {object} args - the block's arguments.
|
||||||
|
* @property {TiltDirection} DIRECTION - the tilt direction to test (up, down, left, right, or any).
|
||||||
|
* @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction.
|
||||||
|
*/
|
||||||
|
isTilted (args) {
|
||||||
|
return this._isTilted(args.DIRECTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {object} args - the block's arguments.
|
||||||
|
* @property {TiltDirection} DIRECTION - the direction (up, down, left, right) to check.
|
||||||
|
* @return {number} - the tilt sensor's angle in the specified direction.
|
||||||
|
* Note that getTiltAngle(up) = -getTiltAngle(down) and getTiltAngle(left) = -getTiltAngle(right).
|
||||||
|
*/
|
||||||
|
getTiltAngle (args) {
|
||||||
|
return this._getTiltAngle(args.DIRECTION);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test whether the tilt sensor is currently tilted.
|
||||||
|
* @param {TiltDirection} direction - the tilt direction to test (up, down, left, right, or any).
|
||||||
|
* @return {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_isTilted (direction) {
|
||||||
|
switch (direction) {
|
||||||
|
case TiltDirection.ANY:
|
||||||
|
return (Math.abs(this._device.tiltX) >= Scratch3WeDo2Blocks.TILT_THRESHOLD) ||
|
||||||
|
(Math.abs(this._device.tiltY) >= Scratch3WeDo2Blocks.TILT_THRESHOLD);
|
||||||
|
default:
|
||||||
|
return this._getTiltAngle(direction) >= Scratch3WeDo2Blocks.TILT_THRESHOLD;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {TiltDirection} direction - the direction (up, down, left, right) to check.
|
||||||
|
* @return {number} - the tilt sensor's angle in the specified direction.
|
||||||
|
* Note that getTiltAngle(up) = -getTiltAngle(down) and getTiltAngle(left) = -getTiltAngle(right).
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_getTiltAngle (direction) {
|
||||||
|
switch (direction) {
|
||||||
|
case TiltDirection.UP:
|
||||||
|
return -this._device.tiltY;
|
||||||
|
case TiltDirection.DOWN:
|
||||||
|
return this._device.tiltY;
|
||||||
|
case TiltDirection.LEFT:
|
||||||
|
return -this._device.tiltX;
|
||||||
|
case TiltDirection.RIGHT:
|
||||||
|
return this._device.tiltX;
|
||||||
|
default:
|
||||||
|
log.warn(`Unknown tilt direction in _getTiltAngle: ${direction}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Call a callback for each motor indexed by the provided motor ID.
|
||||||
|
* @param {MotorID} motorID - the ID specifier.
|
||||||
|
* @param {Function} callback - the function to call with the numeric motor index for each motor.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_forEachMotor (motorID, callback) {
|
||||||
|
let motors;
|
||||||
|
switch (motorID) {
|
||||||
|
case MotorID.A:
|
||||||
|
motors = [0];
|
||||||
|
break;
|
||||||
|
case MotorID.B:
|
||||||
|
motors = [1];
|
||||||
|
break;
|
||||||
|
case MotorID.ALL:
|
||||||
|
case MotorID.DEFAULT:
|
||||||
|
motors = [0, 1];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.warn(`Invalid motor ID: ${motorID}`);
|
||||||
|
motors = [];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (const index of motors) {
|
||||||
|
callback(index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} midiNote - the MIDI note value to convert.
|
||||||
|
* @return {number} - the frequency, in Hz, corresponding to that MIDI note value.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
_noteToTone (midiNote) {
|
||||||
|
// Note that MIDI note 69 is A4, 440 Hz
|
||||||
|
return 440 * Math.pow(2, (midiNote - 69) / 12);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = Scratch3WeDo2Blocks;
|
|
@ -20,7 +20,8 @@ const defaultBlockPackages = {
|
||||||
scratch3_sound: require('../blocks/scratch3_sound'),
|
scratch3_sound: require('../blocks/scratch3_sound'),
|
||||||
scratch3_sensing: require('../blocks/scratch3_sensing'),
|
scratch3_sensing: require('../blocks/scratch3_sensing'),
|
||||||
scratch3_data: require('../blocks/scratch3_data'),
|
scratch3_data: require('../blocks/scratch3_data'),
|
||||||
scratch3_procedures: require('../blocks/scratch3_procedures')
|
scratch3_procedures: require('../blocks/scratch3_procedures'),
|
||||||
|
scratch3_wedo2: require('../blocks/scratch3_wedo2')
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
const StringUtil = require('../util/string-util');
|
||||||
const log = require('../util/log');
|
const log = require('../util/log');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -19,17 +20,17 @@ const loadCostume = function (md5ext, costume, runtime) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const AssetType = runtime.storage.AssetType;
|
const AssetType = runtime.storage.AssetType;
|
||||||
const idParts = md5ext.split('.');
|
const idParts = StringUtil.splitFirst(md5ext, '.');
|
||||||
const md5 = idParts[0];
|
const md5 = idParts[0];
|
||||||
const ext = idParts[1].toUpperCase();
|
const ext = idParts[1].toLowerCase();
|
||||||
const assetType = (ext === 'SVG') ? AssetType.ImageVector : AssetType.ImageBitmap;
|
const assetType = (ext === 'svg') ? AssetType.ImageVector : AssetType.ImageBitmap;
|
||||||
|
|
||||||
const rotationCenter = [
|
const rotationCenter = [
|
||||||
costume.rotationCenterX / costume.bitmapResolution,
|
costume.rotationCenterX / costume.bitmapResolution,
|
||||||
costume.rotationCenterY / costume.bitmapResolution
|
costume.rotationCenterY / costume.bitmapResolution
|
||||||
];
|
];
|
||||||
|
|
||||||
let promise = runtime.storage.load(assetType, md5).then(costumeAsset => {
|
let promise = runtime.storage.load(assetType, md5, ext).then(costumeAsset => {
|
||||||
costume.assetId = costumeAsset.assetId;
|
costume.assetId = costumeAsset.assetId;
|
||||||
costume.assetType = assetType;
|
costume.assetType = assetType;
|
||||||
return costumeAsset;
|
return costumeAsset;
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
const StringUtil = require('../util/string-util');
|
||||||
const log = require('../util/log');
|
const log = require('../util/log');
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -17,9 +18,10 @@ const loadSound = function (sound, runtime) {
|
||||||
log.error('No audio engine present; cannot load sound asset: ', sound.md5);
|
log.error('No audio engine present; cannot load sound asset: ', sound.md5);
|
||||||
return Promise.resolve(sound);
|
return Promise.resolve(sound);
|
||||||
}
|
}
|
||||||
const idParts = sound.md5.split('.');
|
const idParts = StringUtil.splitFirst(sound.md5, '.');
|
||||||
const md5 = idParts[0];
|
const md5 = idParts[0];
|
||||||
return runtime.storage.load(runtime.storage.AssetType.Sound, md5)
|
const ext = idParts[1].toLowerCase();
|
||||||
|
return runtime.storage.load(runtime.storage.AssetType.Sound, md5, ext)
|
||||||
.then(soundAsset => {
|
.then(soundAsset => {
|
||||||
sound.assetId = soundAsset.assetId;
|
sound.assetId = soundAsset.assetId;
|
||||||
sound.assetType = runtime.storage.AssetType.Sound;
|
sound.assetType = runtime.storage.AssetType.Sound;
|
||||||
|
|
|
@ -34,7 +34,7 @@ const getAssetUrl = function (asset) {
|
||||||
'internalapi/asset/',
|
'internalapi/asset/',
|
||||||
asset.assetId,
|
asset.assetId,
|
||||||
'.',
|
'.',
|
||||||
asset.assetType.runtimeFormat,
|
asset.dataFormat,
|
||||||
'/get/'
|
'/get/'
|
||||||
];
|
];
|
||||||
return assetUrlParts.join('');
|
return assetUrlParts.join('');
|
||||||
|
|
|
@ -75,7 +75,7 @@ const parseScratchObject = function (object, runtime) {
|
||||||
rotationCenterX: costumeSource.rotationCenterX,
|
rotationCenterX: costumeSource.rotationCenterX,
|
||||||
rotationCenterY: costumeSource.rotationCenterY
|
rotationCenterY: costumeSource.rotationCenterY
|
||||||
};
|
};
|
||||||
const costumeMd5 = `${costumeSource.assetId}.${costumeSource.assetType.runtimeFormat}`;
|
const costumeMd5 = `${costumeSource.assetId}.${costumeSource.dataFormat}`;
|
||||||
return loadCostume(costumeMd5, costume, runtime);
|
return loadCostume(costumeMd5, costume, runtime);
|
||||||
});
|
});
|
||||||
// Sounds from JSON
|
// Sounds from JSON
|
||||||
|
|
|
@ -832,6 +832,8 @@ class RenderedTarget extends Target {
|
||||||
*/
|
*/
|
||||||
dispose () {
|
dispose () {
|
||||||
this.runtime.changeCloneCounter(-1);
|
this.runtime.changeCloneCounter(-1);
|
||||||
|
this.runtime.stopForTarget(this);
|
||||||
|
this.sprite.removeClone(this);
|
||||||
if (this.renderer && this.drawableID !== null) {
|
if (this.renderer && this.drawableID !== null) {
|
||||||
this.renderer.destroyDrawable(this.drawableID);
|
this.renderer.destroyDrawable(this.drawableID);
|
||||||
if (this.visible) {
|
if (this.visible) {
|
||||||
|
|
|
@ -58,6 +58,18 @@ class Sprite {
|
||||||
}
|
}
|
||||||
return newClone;
|
return newClone;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disconnect a clone from this sprite. The clone is unmodified.
|
||||||
|
* In particular, the clone's dispose() method is not called.
|
||||||
|
* @param {!RenderedTarget} clone - the clone to be removed.
|
||||||
|
*/
|
||||||
|
removeClone (clone) {
|
||||||
|
const cloneIndex = this.clones.indexOf(clone);
|
||||||
|
if (cloneIndex >= 0) {
|
||||||
|
this.clones.splice(cloneIndex, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = Sprite;
|
module.exports = Sprite;
|
||||||
|
|
|
@ -12,6 +12,30 @@ class StringUtil {
|
||||||
while (existingNames.indexOf(name + i) >= 0) i++;
|
while (existingNames.indexOf(name + i) >= 0) i++;
|
||||||
return name + i;
|
return name + i;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Split a string on the first occurrence of a split character.
|
||||||
|
* @param {string} text - the string to split.
|
||||||
|
* @param {string} separator - split the text on this character.
|
||||||
|
* @returns {[string, string]} - the two parts of the split string, or [text, null] if no split character found.
|
||||||
|
* @example
|
||||||
|
* // returns ['foo', 'tar.gz']
|
||||||
|
* splitFirst('foo.tar.gz', '.');
|
||||||
|
* @example
|
||||||
|
* // returns ['foo', null]
|
||||||
|
* splitFirst('foo', '.');
|
||||||
|
* @example
|
||||||
|
* // returns ['foo', '']
|
||||||
|
* splitFirst('foo.', '.');
|
||||||
|
*/
|
||||||
|
static splitFirst (text, separator) {
|
||||||
|
const index = text.indexOf(separator);
|
||||||
|
if (index >= 0) {
|
||||||
|
return [text.substring(0, index), text.substring(index + 1)];
|
||||||
|
} else {
|
||||||
|
return [text, null];
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = StringUtil;
|
module.exports = StringUtil;
|
||||||
|
|
|
@ -364,6 +364,8 @@ class VirtualMachine extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
deleteSprite (targetId) {
|
deleteSprite (targetId) {
|
||||||
const target = this.runtime.getTargetById(targetId);
|
const target = this.runtime.getTargetById(targetId);
|
||||||
|
const targetIndexBeforeDelete = this.runtime.targets.map(t => t.id).indexOf(target.id);
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
if (!target.isSprite()) {
|
if (!target.isSprite()) {
|
||||||
throw new Error('Cannot delete non-sprite targets.');
|
throw new Error('Cannot delete non-sprite targets.');
|
||||||
|
@ -379,7 +381,8 @@ class VirtualMachine extends EventEmitter {
|
||||||
this.runtime.disposeTarget(sprite.clones[i]);
|
this.runtime.disposeTarget(sprite.clones[i]);
|
||||||
// Ensure editing target is switched if we are deleting it.
|
// Ensure editing target is switched if we are deleting it.
|
||||||
if (clone === currentEditingTarget) {
|
if (clone === currentEditingTarget) {
|
||||||
this.setEditingTarget(this.runtime.targets[0].id);
|
const nextTargetIndex = Math.min(this.runtime.targets.length - 1, targetIndexBeforeDelete);
|
||||||
|
this.setEditingTarget(this.runtime.targets[nextTargetIndex].id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Sprite object should be deleted by GC.
|
// Sprite object should be deleted by GC.
|
||||||
|
|
2
test/fixtures/attach-test-storage.js
vendored
2
test/fixtures/attach-test-storage.js
vendored
|
@ -26,7 +26,7 @@ const getAssetUrl = function (asset) {
|
||||||
'internalapi/asset/',
|
'internalapi/asset/',
|
||||||
asset.assetId,
|
asset.assetId,
|
||||||
'.',
|
'.',
|
||||||
asset.assetType.runtimeFormat,
|
asset.dataFormat,
|
||||||
'/get/'
|
'/get/'
|
||||||
];
|
];
|
||||||
return assetUrlParts.join('');
|
return assetUrlParts.join('');
|
||||||
|
|
BIN
test/fixtures/clone-cleanup.sb2
vendored
Normal file
BIN
test/fixtures/clone-cleanup.sb2
vendored
Normal file
Binary file not shown.
96
test/integration/clone-cleanup.js
Normal file
96
test/integration/clone-cleanup.js
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
const path = require('path');
|
||||||
|
const test = require('tap').test;
|
||||||
|
const attachTestStorage = require('../fixtures/attach-test-storage');
|
||||||
|
const extract = require('../fixtures/extract');
|
||||||
|
const VirtualMachine = require('../../src/index');
|
||||||
|
|
||||||
|
const projectUri = path.resolve(__dirname, '../fixtures/clone-cleanup.sb2');
|
||||||
|
const project = extract(projectUri);
|
||||||
|
|
||||||
|
test('clone-cleanup', t => {
|
||||||
|
const vm = new VirtualMachine();
|
||||||
|
attachTestStorage(vm);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track which step of the project is currently under test.
|
||||||
|
* @type {number}
|
||||||
|
*/
|
||||||
|
let testStep = -1;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We test using setInterval; track the interval ID here so we can cancel it.
|
||||||
|
* @type {object}
|
||||||
|
*/
|
||||||
|
let testInterval = null;
|
||||||
|
|
||||||
|
const verifyCounts = (expectedClones, extraThreads) => {
|
||||||
|
// stage plus one sprite, plus clones
|
||||||
|
t.strictEqual(vm.runtime.targets.length, 2 + expectedClones,
|
||||||
|
`target count at step ${testStep}`);
|
||||||
|
|
||||||
|
// the stage should never have any clones
|
||||||
|
t.strictEqual(vm.runtime.targets[0].sprite.clones.length, 1,
|
||||||
|
`stage clone count at step ${testStep}`);
|
||||||
|
|
||||||
|
// check sprite clone count (+1 for original)
|
||||||
|
t.strictEqual(vm.runtime.targets[1].sprite.clones.length, 1 + expectedClones,
|
||||||
|
`sprite clone count at step ${testStep}`);
|
||||||
|
|
||||||
|
// thread count isn't directly tied to clone count since threads can end
|
||||||
|
t.strictEqual(vm.runtime.threads.length, extraThreads + (2 * expectedClones),
|
||||||
|
`thread count at step ${testStep}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
const testNextStep = () => {
|
||||||
|
++testStep;
|
||||||
|
switch (testStep) {
|
||||||
|
case 0:
|
||||||
|
// Project has started, main thread running, no clones yet
|
||||||
|
verifyCounts(0, 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
// 10 clones have been created, main thread still running
|
||||||
|
verifyCounts(10, 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
// The first batch of clones has deleted themselves; main thread still running
|
||||||
|
verifyCounts(0, 1);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
// The second batch of clones has been created and the main thread has ended
|
||||||
|
verifyCounts(10, 0);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
// The second batch of clones has deleted themselves; everything is finished
|
||||||
|
verifyCounts(0, 0);
|
||||||
|
|
||||||
|
clearInterval(testInterval);
|
||||||
|
t.end();
|
||||||
|
process.nextTick(process.exit);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Start VM, load project, and run
|
||||||
|
t.doesNotThrow(() => {
|
||||||
|
vm.start();
|
||||||
|
vm.clear();
|
||||||
|
vm.setCompatibilityMode(false);
|
||||||
|
vm.setTurboMode(false);
|
||||||
|
vm.loadProject(project).then(() => {
|
||||||
|
|
||||||
|
// Verify initial state: no clones, nothing running ("step -1")
|
||||||
|
verifyCounts(0, 0);
|
||||||
|
|
||||||
|
vm.greenFlag();
|
||||||
|
|
||||||
|
// Every second, advance the testing step
|
||||||
|
testInterval = setInterval(testNextStep, 1000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
|
@ -1,6 +1,14 @@
|
||||||
const test = require('tap').test;
|
const test = require('tap').test;
|
||||||
const StringUtil = require('../../src/util/string-util');
|
const StringUtil = require('../../src/util/string-util');
|
||||||
|
|
||||||
|
test('splitFirst', t => {
|
||||||
|
t.deepEqual(StringUtil.splitFirst('asdf.1234', '.'), ['asdf', '1234']);
|
||||||
|
t.deepEqual(StringUtil.splitFirst('asdf.', '.'), ['asdf', '']);
|
||||||
|
t.deepEqual(StringUtil.splitFirst('.1234', '.'), ['', '1234']);
|
||||||
|
t.deepEqual(StringUtil.splitFirst('foo', '.'), ['foo', null]);
|
||||||
|
t.end();
|
||||||
|
});
|
||||||
|
|
||||||
test('withoutTrailingDigits', t => {
|
test('withoutTrailingDigits', t => {
|
||||||
t.strictEqual(StringUtil.withoutTrailingDigits('boeing747'), 'boeing');
|
t.strictEqual(StringUtil.withoutTrailingDigits('boeing747'), 'boeing');
|
||||||
t.strictEqual(StringUtil.withoutTrailingDigits('boeing747 '), 'boeing747 ');
|
t.strictEqual(StringUtil.withoutTrailingDigits('boeing747 '), 'boeing747 ');
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue