mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2025-01-10 06:52:00 -05:00
Merge branch 'develop' into privateSprite
This commit is contained in:
commit
b885402081
12 changed files with 204 additions and 40 deletions
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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 = '';
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -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 = [];
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 = [
|
||||
|
|
Loading…
Reference in a new issue