diff --git a/src/sprites/rendered-target.js b/src/sprites/rendered-target.js index b0c1ebfed..495cc0a0f 100644 --- a/src/sprites/rendered-target.js +++ b/src/sprites/rendered-target.js @@ -505,18 +505,28 @@ class RenderedTarget extends Target { return null; } - const deletedCostume = this.sprite.deleteCostumeAt(index); + const deletedCostume = this.sprite.deleteCostumeAt(index, true); // Update current costume - if (index === this.currentCostume && index === originalCostumeCount - 1) { + this.runtime.requestTargetsUpdate(this); + return deletedCostume; + } + + /** + * Shift current costume index if needed and re-render this target. + * This needs to be called on all clones to prevent desync crash. (#2661) + * @param {number} index Index of deleted costume + * @param {boolean} isLastCostume Whether the deleted costume was the last costume or not + */ + shiftCurrentCostume (index, isLastCostume) { + if (index === this.currentCostume && isLastCostume) { this.setCostume(index - 1); } else if (index < this.currentCostume) { this.setCostume(this.currentCostume - 1); } else { + // Index is still the same; however, the costume itself may have changed. + // This causes the renderer to re-render. this.setCostume(this.currentCostume); } - - this.runtime.requestTargetsUpdate(this); - return deletedCostume; } /** @@ -630,14 +640,15 @@ class RenderedTarget extends Target { if (newIndex === costumeIndex) return false; - const currentCostume = this.getCurrentCostume(); + this.sprite.saveCurrentCostumeName(); const costume = this.sprite.costumes[costumeIndex]; // Use the sprite method for deleting costumes because setCostume is handled manually this.sprite.deleteCostumeAt(costumeIndex); this.addCostume(costume, newIndex); - this.currentCostume = this.getCostumeIndexByName(currentCostume.name); + // This sets currentCostume + this.sprite.restoreCurrentCostume(); return true; } diff --git a/src/sprites/sprite.js b/src/sprites/sprite.js index a38fa129c..904401b9b 100644 --- a/src/sprites/sprite.js +++ b/src/sprites/sprite.js @@ -92,12 +92,43 @@ class Sprite { } /** - * Delete a costume by index. + * Delete a costume by index, and optionally update current costume for all clones. * @param {number} index Costume index to be deleted + * @param {boolean} [optUpdateCurrentCostume] Whether or not to update current costume for all clones * @return {?object} The deleted costume */ - deleteCostumeAt (index) { - return this.costumes.splice(index, 1)[0]; + deleteCostumeAt (index, optUpdateCurrentCostume) { + const isLastCostume = index === this.costumes.length - 1; + const deletedCostume = this.costumes.splice(index, 1)[0]; + if (optUpdateCurrentCostume) { + for (const target of this.clones) { + target.shiftCurrentCostume(index, isLastCostume); + } + } + return deletedCostume; + } + + /** + * Save current costume temporarliy during reordering, for all clones. + */ + saveCurrentCostumeName () { + for (const target of this.clones) { + target._currentCostumeName = target.getCurrentCostume().name; + } + } + + /** + * Restore current costume from saved name. + */ + restoreCurrentCostume () { + for (const target of this.clones) { + if (typeof target._currentCostumeName === 'undefined') continue; + const costumeIndex = this.costumes.findIndex(c => c.name === target._currentCostumeName); + delete target._currentCostumeName; + if (costumeIndex !== target.currentCostume) { + target.setCostume(costumeIndex); + } + } } /** diff --git a/test/unit/sprites_rendered-target.js b/test/unit/sprites_rendered-target.js index 681a10d86..914272745 100644 --- a/test/unit/sprites_rendered-target.js +++ b/test/unit/sprites_rendered-target.js @@ -119,6 +119,7 @@ test('deleteCostume', t => { const a = new RenderedTarget(s, r); const renderer = new FakeRenderer(); a.renderer = renderer; + s.clones = [a]; // x* Costume 1 * Costume 2 // Costume 2 => Costume 3 @@ -211,6 +212,70 @@ test('deleteCostume', t => { t.end(); }); +test('deleteCostume shifts current costume (1/2)', t => { + const o1 = {id: 1}; + const o2 = {id: 2}; + const o3 = {id: 3}; + + const r = new Runtime(); + const s = new Sprite(null, r); + s.costumes = [o1, o2, o3]; + const renderer = new FakeRenderer(); + + const c1 = new RenderedTarget(s, r); + c1.renderer = renderer; + c1.currentCostume = 0; + + const c2 = new RenderedTarget(s, r); + c2.renderer = renderer; + c2.currentCostume = 1; + + const c3 = new RenderedTarget(s, r); + c3.renderer = renderer; + c3.currentCostume = 2; + + s.clones = [c1, c2, c3]; + + c1.deleteCostume(0); + t.equals(c1.currentCostume, 0); + t.equals(c2.currentCostume, 0); + t.equals(c3.currentCostume, 1); + + t.end(); +}); + +test('deleteCostume shifts current costume (2/2)', t => { + const o1 = {id: 1}; + const o2 = {id: 2}; + const o3 = {id: 3}; + + const r = new Runtime(); + const s = new Sprite(null, r); + s.costumes = [o1, o2, o3]; + const renderer = new FakeRenderer(); + + const c1 = new RenderedTarget(s, r); + c1.renderer = renderer; + c1.currentCostume = 0; + + const c2 = new RenderedTarget(s, r); + c2.renderer = renderer; + c2.currentCostume = 1; + + const c3 = new RenderedTarget(s, r); + c3.renderer = renderer; + c3.currentCostume = 2; + + s.clones = [c1, c2, c3]; + + c1.deleteCostume(2); + t.equals(c1.currentCostume, 0); + t.equals(c2.currentCostume, 1); + t.equals(c3.currentCostume, 1); + + t.end(); +}); + test('deleteSound', t => { const o1 = {id: 1}; const o2 = {id: 2}; @@ -474,12 +539,16 @@ test('#reorderCostume', t => { const r = new Runtime(); const s = new Sprite(null, r); s.costumes = [o1, o2, o3, o4, o5]; - const a = new RenderedTarget(s, r); const renderer = new FakeRenderer(); + const a = new RenderedTarget(s, r); a.renderer = renderer; + const b = new RenderedTarget(s, r); + b.renderer = renderer; + s.clones = [a, b]; const resetCostumes = () => { a.setCostume(0); + b.setCostume(0); s.costumes = [o1, o2, o3, o4, o5]; }; const costumeIds = () => a.sprite.costumes.map(c => c.id); @@ -487,6 +556,7 @@ test('#reorderCostume', t => { resetCostumes(); t.deepEquals(costumeIds(), [0, 1, 2, 3, 4]); t.equals(a.currentCostume, 0); + t.equals(b.currentCostume, 0); // Returns false if the costumes are the same and no change occurred t.equal(a.reorderCostume(3, 3), false); @@ -495,15 +565,19 @@ test('#reorderCostume', t => { // Make sure reordering up and down works and current costume follows resetCostumes(); + b.setCostume(4); t.equal(a.reorderCostume(0, 3), true); t.deepEquals(costumeIds(), [1, 2, 3, 0, 4]); t.equals(a.currentCostume, 3); // Index of id=0 + t.equals(b.currentCostume, 4); // Index of id=4 resetCostumes(); a.setCostume(1); + b.setCostume(3); t.equal(a.reorderCostume(3, 1), true); t.deepEquals(costumeIds(), [0, 3, 1, 2, 4]); t.equals(a.currentCostume, 2); // Index of id=1 + t.equals(b.currentCostume, 1); // Index of id=3 // Out of bounds indices get clamped resetCostumes();