Merge branch 'develop' into privateSprite

This commit is contained in:
DD 2018-03-01 17:27:10 -05:00
commit b885402081
12 changed files with 204 additions and 40 deletions

View file

@ -1,5 +1,6 @@
const mutationAdapter = require('./mutation-adapter');
const html = require('htmlparser2');
const uid = require('../util/uid');
/**
* Convert and an individual block DOM to the representation tree.
@ -11,6 +12,10 @@ const html = require('htmlparser2');
* @return {undefined}
*/
const domToBlock = function (blockDOM, blocks, isTopBlock, parent) {
if (!blockDOM.attribs.id) {
blockDOM.attribs.id = uid();
}
// Block skeleton.
const block = {
id: blockDOM.attribs.id, // Block ID
@ -152,7 +157,7 @@ const domToBlocks = function (blocksDOM) {
/**
* Adapter between block creation events and block representation which can be
* used by the Scratch runtime.
* @param {object} e `Blockly.events.create`
* @param {object} e `Blockly.events.create` or `Blockly.events.endDrag`
* @return {Array.<object>} List of blocks from this CREATE event.
*/
const adapter = function (e) {

View file

@ -288,6 +288,22 @@ class Blocks {
newCoordinate: e.newCoordinate
});
break;
case 'dragOutside':
if (optRuntime) {
optRuntime.emitBlockDragUpdate(e.isOutside);
}
break;
case 'endDrag':
if (optRuntime) {
optRuntime.emitBlockDragUpdate(false /* areBlocksOverGui */);
// Drag blocks onto another sprite
if (e.isOutside) {
const newBlocks = adapter(e);
optRuntime.emitBlockEndDrag(newBlocks);
}
}
break;
case 'delete':
// Don't accept delete events for missing blocks,
// or shadow blocks being obscured.

View file

@ -379,6 +379,22 @@ class Runtime extends EventEmitter {
return 'MONITORS_UPDATE';
}
/**
* Event name for block drag update.
* @const {string}
*/
static get BLOCK_DRAG_UPDATE () {
return 'BLOCK_DRAG_UPDATE';
}
/**
* Event name for block drag end.
* @const {string}
*/
static get BLOCK_DRAG_END () {
return 'BLOCK_DRAG_END';
}
/**
* Event name for reporting that an extension was added.
* @const {string}
@ -1387,6 +1403,22 @@ class Runtime extends EventEmitter {
}
}
/**
* Emit whether blocks are being dragged over gui
* @param {boolean} areBlocksOverGui True if blocks are dragged out of blocks workspace, false otherwise
*/
emitBlockDragUpdate (areBlocksOverGui) {
this.emit(Runtime.BLOCK_DRAG_UPDATE, areBlocksOverGui);
}
/**
* Emit event to indicate that the block drag has ended with the blocks outside the blocks workspace
* @param {Array.<object>} blocks The set of blocks dragged to the GUI
*/
emitBlockEndDrag (blocks) {
this.emit(Runtime.BLOCK_DRAG_END, blocks);
}
/**
* Emit value for reporter to show in the blocks.
* @param {string} blockId ID for the block.

View file

@ -174,7 +174,8 @@ class Sequencer {
// A "null block" - empty branch.
thread.popStack();
}
while (thread.peekStack()) {
// Save the current block ID to notice if we did control flow.
while ((currentBlockId = thread.peekStack()) !== 0) {
let isWarpMode = thread.peekStackFrame().warpMode;
if (isWarpMode && !thread.warpTimer) {
// Initialize warp-mode timer if it hasn't been already.
@ -183,8 +184,6 @@ class Sequencer {
thread.warpTimer.start();
}
// Execute the current block.
// Save the current block ID to notice if we did control flow.
currentBlockId = thread.peekStack();
if (this.runtime.profiler !== null) {
if (executeProfilerId === -1) {
executeProfilerId = this.runtime.profiler.idByName(executeProfilerFrame);

View file

@ -43,13 +43,6 @@ class Scratch3MusicBlocks {
*/
this.runtime = runtime;
/**
* The current tempo in beats per minute. The tempo is a global property of the project,
* not a property of each sprite, so it is not stored in the MusicState object.
* @type {number}
*/
this.tempo = 60;
/**
* The number of drum and instrument sounds currently being played simultaneously.
* @type {number}
@ -464,7 +457,7 @@ class Scratch3MusicBlocks {
arguments: {
DRUM: {
type: ArgumentType.NUMBER,
menu: 'drums',
menu: 'DRUM',
defaultValue: 1
},
BEATS: {
@ -506,7 +499,7 @@ class Scratch3MusicBlocks {
arguments: {
INSTRUMENT: {
type: ArgumentType.NUMBER,
menu: 'instruments',
menu: 'INSTRUMENT',
defaultValue: 1
}
}
@ -540,8 +533,8 @@ class Scratch3MusicBlocks {
}
],
menus: {
drums: this._buildMenu(this.DRUM_INFO),
instruments: this._buildMenu(this.INSTRUMENT_INFO)
DRUM: this._buildMenu(this.DRUM_INFO),
INSTRUMENT: this._buildMenu(this.INSTRUMENT_INFO)
}
};
}
@ -759,7 +752,7 @@ class Scratch3MusicBlocks {
* @private
*/
_beatsToSec (beats) {
return (60 / this.tempo) * beats;
return (60 / this.getTempo()) * beats;
}
/**
@ -829,7 +822,7 @@ class Scratch3MusicBlocks {
*/
changeTempo (args) {
const change = Cast.toNumber(args.TEMPO);
const tempo = change + this.tempo;
const tempo = change + this.getTempo();
this._updateTempo(tempo);
}
@ -840,7 +833,10 @@ class Scratch3MusicBlocks {
*/
_updateTempo (tempo) {
tempo = MathUtil.clamp(tempo, Scratch3MusicBlocks.TEMPO_RANGE.min, Scratch3MusicBlocks.TEMPO_RANGE.max);
this.tempo = tempo;
const stage = this.runtime.getTargetForStage();
if (stage) {
stage.tempo = tempo;
}
}
/**
@ -848,7 +844,11 @@ class Scratch3MusicBlocks {
* @return {number} - the current tempo, in beats per minute.
*/
getTempo () {
return this.tempo;
const stage = this.runtime.getTargetForStage();
if (stage) {
return stage.tempo;
}
return 60;
}
}

View file

@ -320,6 +320,9 @@ const parseScratchObject = function (object, runtime, extensions, topLevel) {
target.rotationStyle = RenderedTarget.ROTATION_STYLE_ALL_AROUND;
}
}
if (object.hasOwnProperty('tempoBPM')) {
target.tempo = object.tempoBPM;
}
target.isStage = topLevel;
@ -533,6 +536,14 @@ const parseBlock = function (sb2block, addBroadcastMsg, getVariableId, extension
if (shadowObscured) {
fieldValue = '';
}
} else if (expectedArg.inputOp === 'music.menu.DRUM') {
if (shadowObscured) {
fieldValue = 1;
}
} else if (expectedArg.inputOp === 'music.menu.INSTRUMENT') {
if (shadowObscured) {
fieldValue = 1;
}
} else if (shadowObscured) {
// Filled drop-down menu.
fieldValue = '';

View file

@ -424,7 +424,7 @@ const specMap = {
argMap: [
{
type: 'input',
inputOp: 'math_number',
inputOp: 'music.menu.DRUM',
inputName: 'DRUM'
},
{
@ -464,7 +464,7 @@ const specMap = {
argMap: [
{
type: 'input',
inputOp: 'math_number',
inputOp: 'music.menu.INSTRUMENT',
inputName: 'INSTRUMENT'
}
]

View file

@ -116,6 +116,12 @@ class RenderedTarget extends Target {
* @type {!string}
*/
this.rotationStyle = RenderedTarget.ROTATION_STYLE_ALL_AROUND;
/**
* Current tempo (used by the music extension)
* @type {number}
*/
this.tempo = 60;
}
/**
@ -401,18 +407,14 @@ class RenderedTarget extends Target {
/**
* Add a costume, taking care to avoid duplicate names.
* @param {!object} costumeObject Object representing the costume.
* @param {?int} index Index at which to add costume
*/
addCostume (costumeObject) {
this.sprite.addCostumeAt(costumeObject, this.sprite.costumes.length);
}
/**
* Add a costume at the given index, taking care to avoid duplicate names.
* @param {!object} costumeObject Object representing the costume.
* @param {!int} index Index at which to add costume
*/
addCostumeAt (costumeObject, index) {
this.sprite.addCostumeAt(costumeObject, index);
addCostume (costumeObject, index) {
if (index) {
this.sprite.addCostumeAt(costumeObject, index);
} else {
this.sprite.addCostumeAt(costumeObject, this.sprite.costumes.length);
}
}
/**
@ -466,11 +468,16 @@ class RenderedTarget extends Target {
/**
* Add a sound, taking care to avoid duplicate names.
* @param {!object} soundObject Object representing the sound.
* @param {?int} index Index at which to add costume
*/
addSound (soundObject) {
addSound (soundObject, index) {
const usedNames = this.sprite.sounds.map(sound => sound.name);
soundObject.name = StringUtil.unusedName(soundObject.name, usedNames);
this.sprite.sounds.push(soundObject);
if (index) {
this.sprite.sounds.splice(index, 0, soundObject);
} else {
this.sprite.sounds.push(soundObject);
}
}
/**

View file

@ -129,7 +129,7 @@ class Sprite {
newSprite.blocks = this.blocks.duplicate();
const allNames = this.runtime.targets.map(t => t.name);
const allNames = this.runtime.targets.map(t => t.sprite.name);
newSprite.name = StringUtil.unusedName(this.name, allNames);
const assetPromises = [];

View file

@ -74,6 +74,12 @@ class VirtualMachine extends EventEmitter {
this.runtime.on(Runtime.MONITORS_UPDATE, monitorList => {
this.emit(Runtime.MONITORS_UPDATE, monitorList);
});
this.runtime.on(Runtime.BLOCK_DRAG_UPDATE, areBlocksOverGui => {
this.emit(Runtime.BLOCK_DRAG_UPDATE, areBlocksOverGui);
});
this.runtime.on(Runtime.BLOCK_DRAG_END, blocks => {
this.emit(Runtime.BLOCK_DRAG_END, blocks);
});
this.runtime.on(Runtime.EXTENSION_ADDED, blocksInfo => {
this.emit(Runtime.EXTENSION_ADDED, blocksInfo);
});
@ -327,6 +333,36 @@ class VirtualMachine extends EventEmitter {
});
}
/**
* Duplicate the costume at the given index. Add it at that index + 1.
* @param {!int} costumeIndex Index of costume to duplicate
* @returns {?Promise} - a promise that resolves when the costume has been decoded and added
*/
duplicateCostume (costumeIndex) {
const originalCostume = this.editingTarget.getCostumes()[costumeIndex];
const clone = Object.assign({}, originalCostume);
const md5ext = `${clone.assetId}.${clone.dataFormat}`;
return loadCostume(md5ext, clone, this.runtime).then(() => {
this.editingTarget.addCostume(clone, costumeIndex + 1);
this.editingTarget.setCostume(costumeIndex + 1);
this.emitTargetsUpdate();
});
}
/**
* Duplicate the sound at the given index. Add it at that index + 1.
* @param {!int} soundIndex Index of sound to duplicate
* @returns {?Promise} - a promise that resolves when the sound has been decoded and added
*/
duplicateSound (soundIndex) {
const originalSound = this.editingTarget.getSounds()[soundIndex];
const clone = Object.assign({}, originalSound);
return loadSound(clone, this.runtime).then(() => {
this.editingTarget.addSound(clone, soundIndex + 1);
this.emitTargetsUpdate();
});
}
/**
* Rename a costume on the current editing target.
* @param {int} costumeIndex - the index of the costume to be renamed.
@ -384,12 +420,34 @@ class VirtualMachine extends EventEmitter {
* Update a sound buffer.
* @param {int} soundIndex - the index of the sound to be updated.
* @param {AudioBuffer} newBuffer - new audio buffer for the audio engine.
* @param {ArrayBuffer} soundEncoding - the new (wav) encoded sound to be stored
*/
updateSoundBuffer (soundIndex, newBuffer) {
const id = this.editingTarget.sprite.sounds[soundIndex].soundId;
updateSoundBuffer (soundIndex, newBuffer, soundEncoding) {
const sound = this.editingTarget.sprite.sounds[soundIndex];
const id = sound ? sound.soundId : null;
if (id && this.runtime && this.runtime.audioEngine) {
this.runtime.audioEngine.updateSoundBuffer(id, newBuffer);
}
// Update sound in runtime
if (soundEncoding) {
// Now that we updated the sound, the format should also be updated
// so that the sound can eventually be decoded the right way.
// Sounds that were formerly 'adpcm', but were updated in sound editor
// will not get decoded by the audio engine correctly unless the format
// is updated as below.
sound.format = '';
const storage = this.runtime.storage;
sound.assetId = storage.builtinHelper.cache(
storage.AssetType.Sound,
storage.DataFormat.WAV,
soundEncoding
);
sound.md5 = `${sound.assetId}.${sound.dataFormat}`;
}
// If soundEncoding is null, it's because gui had a problem
// encoding the updated sound. We don't want to store anything in this
// case, and gui should have logged an error.
this.emitTargetsUpdate();
}
@ -652,6 +710,19 @@ class VirtualMachine extends EventEmitter {
}
}
/**
* Called when blocks are dragged from one sprite to another. Adds the blocks to the
* workspace of the given target.
* @param {!Array<object>} blocks Blocks to add.
* @param {!string} targetId Id of target to add blocks to.
*/
shareBlocksToTarget (blocks, targetId) {
const target = this.runtime.getTargetById(targetId);
for (let i = 0; i < blocks.length; i++) {
target.blocks.createBlock(blocks[i]);
}
}
/**
* Repopulate the workspace with the blocks of the current editingTarget. This
* allows us to get around bugs like gui#413.

View file

@ -1,8 +1,11 @@
const test = require('tap').test;
const Music = require('../../src/extensions/scratch3_music/index.js');
const runtime = Object.create(null);
const blocks = new Music(runtime);
const fakeRuntime = {
getTargetForStage: () => ({tempo: 60})
};
const blocks = new Music(fakeRuntime);
const util = {
stackFrame: Object.create(null),

View file

@ -105,7 +105,7 @@ test('renameSprite does not increment when renaming to the same name', t => {
t.equal(vm.runtime.targets[0].sprite.name, 'foo');
vm.renameSprite(target.id, 'foo');
t.equal(vm.runtime.targets[0].sprite.name, 'foo');
t.end();
});
@ -276,6 +276,26 @@ test('duplicateSprite duplicates a sprite when given id is associated with known
});
test('duplicateSprite assigns duplicated sprite a fresh name', t => {
const vm = new VirtualMachine();
const spr = new Sprite(null, vm.runtime);
spr.name = 'sprite1';
const currTarget = spr.createClone();
vm.editingTarget = currTarget;
vm.emitWorkspaceUpdate = () => null;
vm.runtime.targets = [currTarget];
t.equal(vm.runtime.targets.length, 1);
vm.duplicateSprite(currTarget.id).then(() => {
t.equal(vm.runtime.targets.length, 2);
t.equal(vm.runtime.targets[0].sprite.name, 'sprite1');
t.equal(vm.runtime.targets[1].sprite.name, 'sprite2');
t.end();
});
});
test('emitWorkspaceUpdate', t => {
const vm = new VirtualMachine();
vm.runtime.targets = [