diff --git a/src/engine/blocks.js b/src/engine/blocks.js index 85aeabfb8..53e346a5a 100644 --- a/src/engine/blocks.js +++ b/src/engine/blocks.js @@ -2,6 +2,7 @@ const adapter = require('./adapter'); const mutationAdapter = require('./mutation-adapter'); const xmlEscape = require('../util/xml-escape'); const MonitorRecord = require('./monitor-record'); +const Clone = require('../util/clone'); /** * @fileoverview @@ -178,6 +179,12 @@ class Blocks { return null; } + duplicate () { + const newBlocks = new Blocks(); + newBlocks._blocks = Clone.simple(this._blocks); + newBlocks._scripts = Clone.simple(this._scripts); + return newBlocks; + } // --------------------------------------------------------------------- /** diff --git a/src/import/load-costume.js b/src/import/load-costume.js index d435129b2..9e2f165b6 100644 --- a/src/import/load-costume.js +++ b/src/import/load-costume.js @@ -25,55 +25,54 @@ const loadCostume = function (md5ext, costume, runtime) { const ext = idParts[1].toLowerCase(); const assetType = (ext === 'svg') ? AssetType.ImageVector : AssetType.ImageBitmap; + return runtime.storage.load(assetType, md5, ext).then(costumeAsset => { + costume.dataFormat = ext; + return loadCostumeFromAsset(costume, costumeAsset, runtime); + }); +}; + +const loadCostumeFromAsset = function (costume, costumeAsset, runtime) { const rotationCenter = [ costume.rotationCenterX / costume.bitmapResolution, costume.rotationCenterY / costume.bitmapResolution ]; - let promise = runtime.storage.load(assetType, md5, ext).then(costumeAsset => { - costume.assetId = costumeAsset.assetId; - costume.dataFormat = ext; - return costumeAsset; - }); - if (!runtime.renderer) { log.error('No rendering module present; cannot load costume asset: ', md5ext); - return promise.then(() => costume); + return costume; + } + const AssetType = runtime.storage.AssetType; + costume.assetId = costumeAsset.assetId; + if (costumeAsset.assetType === AssetType.ImageVector) { + costume.skinId = runtime.renderer.createSVGSkin(costumeAsset.decodeText(), rotationCenter); + return costume; } - if (assetType === AssetType.ImageVector) { - promise = promise.then(costumeAsset => { - costume.skinId = runtime.renderer.createSVGSkin(costumeAsset.decodeText(), rotationCenter); - return costume; - }); - } else { - promise = promise.then(costumeAsset => ( - new Promise((resolve, reject) => { - const imageElement = new Image(); - const onError = function () { + return new Promise((resolve, reject) => { + const imageElement = new Image(); + const onError = function () { // eslint-disable-next-line no-use-before-define - removeEventListeners(); - reject(); - }; - const onLoad = function () { + removeEventListeners(); + reject(); + }; + const onLoad = function () { // eslint-disable-next-line no-use-before-define - removeEventListeners(); - resolve(imageElement); - }; - const removeEventListeners = function () { - imageElement.removeEventListener('error', onError); - imageElement.removeEventListener('load', onLoad); - }; - imageElement.addEventListener('error', onError); - imageElement.addEventListener('load', onLoad); - imageElement.src = costumeAsset.encodeDataURI(); - }) - )).then(imageElement => { - costume.skinId = runtime.renderer.createBitmapSkin(imageElement, costume.bitmapResolution, rotationCenter); - return costume; - }); - } - return promise; + removeEventListeners(); + resolve(imageElement); + }; + const removeEventListeners = function () { + imageElement.removeEventListener('error', onError); + imageElement.removeEventListener('load', onLoad); + }; + imageElement.addEventListener('error', onError); + imageElement.addEventListener('load', onLoad); + imageElement.src = costumeAsset.encodeDataURI(); + }).then(imageElement => { + costume.skinId = runtime.renderer.createBitmapSkin(imageElement, costume.bitmapResolution, rotationCenter); + return costume; + }); }; +loadCostume.loadCostumeFromAsset = loadCostumeFromAsset; + module.exports = loadCostume; diff --git a/src/import/load-sound.js b/src/import/load-sound.js index 8f4f9edff..84425c189 100644 --- a/src/import/load-sound.js +++ b/src/import/load-sound.js @@ -23,18 +23,22 @@ const loadSound = function (sound, runtime) { const ext = idParts[1].toLowerCase(); return runtime.storage.load(runtime.storage.AssetType.Sound, md5, ext) .then(soundAsset => { - sound.assetId = soundAsset.assetId; sound.dataFormat = ext; - return runtime.audioEngine.decodeSound(Object.assign( - {}, - sound, - {data: soundAsset.data} - )); - }) - .then(soundId => { - sound.soundId = soundId; - return sound; + return loadSoundFromAsset(sound, soundAsset, runtime); }); }; +const loadSoundFromAsset = function (sound, soundAsset, runtime) { + sound.assetId = soundAsset.assetId; + return runtime.audioEngine.decodeSound(Object.assign( + {}, + sound, + {data: soundAsset.data} + )).then(soundId => { + sound.soundId = soundId; + return sound; + }); +}; + +loadSound.loadSoundFromAsset = loadSoundFromAsset; module.exports = loadSound; diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index bbf85231c..653bc4efd 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -787,6 +787,33 @@ class RenderedTarget extends Target { return newClone; } + /** + * Make a duplicate using a duplicate sprite. + * @return {RenderedTarget} New clone. + */ + duplicate () { + return this.sprite.duplicate().then(newSprite => { + const newTarget = newSprite.createClone(); + // Copy all properties. + // @todo refactor with clone methods + newTarget.x = Math.random() * 400 / 2; + newTarget.y = Math.random() * 300 / 2; + newTarget.direction = this.direction; + newTarget.draggable = this.draggable; + newTarget.visible = this.visible; + newTarget.size = this.size; + newTarget.currentCostume = this.currentCostume; + newTarget.rotationStyle = this.rotationStyle; + newTarget.effects = JSON.parse(JSON.stringify(this.effects)); + newTarget.variables = JSON.parse(JSON.stringify(this.variables)); + newTarget.lists = JSON.parse(JSON.stringify(this.lists)); + newTarget.initDrawable(); + newTarget.updateAllDrawableProperties(); + newTarget.goBehindOther(this); + return newTarget; + }); + } + /** * Called when the project receives a "green flag." * For a rendered target, this clears graphic effects. diff --git a/src/sprites/sprite.js b/src/sprites/sprite.js index 9e36c8fea..ee5b9394c 100644 --- a/src/sprites/sprite.js +++ b/src/sprites/sprite.js @@ -1,5 +1,8 @@ const RenderedTarget = require('./rendered-target'); const Blocks = require('../engine/blocks'); +const {loadSoundFromAsset} = require('../import/load-sound'); +const {loadCostumeFromAsset} = require('../import/load-costume'); +const StringUtil = require('../util/string-util'); class Sprite { /** @@ -73,6 +76,33 @@ class Sprite { this.clones.splice(cloneIndex, 1); } } + + duplicate () { + const newSprite = new Sprite(null, this.runtime); + + newSprite.blocks = this.blocks.duplicate(); + + const allNames = this.runtime.targets.map(t => t.name); + newSprite.name = StringUtil.unusedName(this.name, allNames); + + const assetPromises = []; + + newSprite.costumes = this.costumes.map(costume => { + const newCostume = Object.assign({}, costume); + const costumeAsset = this.runtime.storage.get(costume.assetId); + assetPromises.push(loadCostumeFromAsset(newCostume, costumeAsset, this.runtime)); + return newCostume; + }); + + newSprite.sounds = this.sounds.map(sound => { + const newSound = Object.assign({}, sound); + const soundAsset = this.runtime.storage.get(sound.assetId); + assetPromises.push(loadSoundFromAsset(newSound, soundAsset, this.runtime)); + return newSound; + }); + + return Promise.all(assetPromises).then(() => newSprite); + } } module.exports = Sprite; diff --git a/src/virtual-machine.js b/src/virtual-machine.js index 762ef8c01..0109f9c26 100644 --- a/src/virtual-machine.js +++ b/src/virtual-machine.js @@ -439,6 +439,31 @@ class VirtualMachine extends EventEmitter { } } + /** + * Duplicate a sprite. + * @param {string} targetId ID of a target whose sprite to duplicate. + */ + duplicateSprite (targetId) { + const target = this.runtime.getTargetById(targetId); + + if (target) { + if (!target.isSprite()) { + throw new Error('Cannot duplicate non-sprite targets.'); + } + const sprite = target.sprite; + if (!sprite) { + throw new Error('No sprite associated with this target.'); + } + + target.duplicate().then(newTarget => { + this.runtime.targets.push(newTarget); + this.setEditingTarget(newTarget.id); + }); + } else { + throw new Error('No target with the provided id.'); + } + } + /** * Set the audio engine for the VM/runtime * @param {!AudioEngine} audioEngine The audio engine to attach