mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-22 22:12:28 -05:00
Preserve sprite layer order information across saving and loading an sb3.
This commit is contained in:
parent
dc612fb4a1
commit
812e7a3772
7 changed files with 133 additions and 7 deletions
|
@ -12,6 +12,7 @@ const Comment = require('../engine/comment');
|
|||
const StageLayering = require('../engine/stage-layering');
|
||||
const log = require('../util/log');
|
||||
const uid = require('../util/uid');
|
||||
const MathUtil = require('../util/math-util');
|
||||
|
||||
const {loadCostume} = require('../import/load-costume.js');
|
||||
const {loadSound} = require('../import/load-sound.js');
|
||||
|
@ -437,6 +438,7 @@ const serializeTarget = function (target, extensions) {
|
|||
obj.costumes = target.costumes.map(serializeCostume);
|
||||
obj.sounds = target.sounds.map(serializeSound);
|
||||
if (target.hasOwnProperty('volume')) obj.volume = target.volume;
|
||||
if (target.hasOwnProperty('layerOrder')) obj.layerOrder = target.layerOrder;
|
||||
if (obj.isStage) { // Only the stage should have these properties
|
||||
if (target.hasOwnProperty('tempo')) obj.tempo = target.tempo;
|
||||
if (target.hasOwnProperty('videoTransparency')) obj.videoTransparency = target.videoTransparency;
|
||||
|
@ -458,6 +460,11 @@ const serializeTarget = function (target, extensions) {
|
|||
return obj;
|
||||
};
|
||||
|
||||
const getSimplifiedLayerOrdering = function (targets) {
|
||||
const layerOrders = targets.map(t => t.getLayerOrder());
|
||||
return MathUtil.reducedSortOrdering(layerOrders);
|
||||
};
|
||||
|
||||
/**
|
||||
* Serializes the specified VM runtime.
|
||||
* @param {!Runtime} runtime VM runtime instance to be serialized.
|
||||
|
@ -469,9 +476,20 @@ const serialize = function (runtime, targetId) {
|
|||
const obj = Object.create(null);
|
||||
// Create extension set to hold extension ids found while serializing targets
|
||||
const extensions = new Set();
|
||||
const flattenedOriginalTargets = JSON.parse(JSON.stringify(targetId ?
|
||||
|
||||
const originalTargetsToSerialize = targetId ?
|
||||
[runtime.getTargetById(targetId)] :
|
||||
runtime.targets.filter(target => target.isOriginal)));
|
||||
runtime.targets.filter(target => target.isOriginal);
|
||||
|
||||
const layerOrdering = getSimplifiedLayerOrdering(originalTargetsToSerialize);
|
||||
|
||||
const flattenedOriginalTargets = JSON.parse(JSON.stringify(originalTargetsToSerialize));
|
||||
|
||||
if (runtime.renderer) {
|
||||
flattenedOriginalTargets.forEach((t, index) => {
|
||||
t.layerOrder = layerOrdering[index];
|
||||
});
|
||||
}
|
||||
|
||||
const serializedTargets = flattenedOriginalTargets.map(t => serializeTarget(t, extensions));
|
||||
|
||||
|
@ -930,6 +948,12 @@ const parseScratchObject = function (object, runtime, extensions, zip) {
|
|||
if (object.hasOwnProperty('isStage')) {
|
||||
target.isStage = object.isStage;
|
||||
}
|
||||
if (object.hasOwnProperty('targetPaneOrder')) {
|
||||
// Temporarily store the 'targetPaneOrder' property
|
||||
// so that we can correctly order sprites in the target pane.
|
||||
// This will be deleted after we are done parsing and ordering the targets list.
|
||||
target.targetPaneOrder = object.targetPaneOrder;
|
||||
}
|
||||
Promise.all(costumePromises).then(costumes => {
|
||||
sprite.costumes = costumes;
|
||||
});
|
||||
|
@ -952,10 +976,28 @@ const deserialize = function (json, runtime, zip, isSingleSprite) {
|
|||
extensionIDs: new Set(),
|
||||
extensionURLs: new Map()
|
||||
};
|
||||
|
||||
// First keep track of the current target order in the json,
|
||||
// then sort by the layer order property before parsing the targets
|
||||
// so that their corresponding render drawables can be created in
|
||||
// their layer order (e.g. back to front)
|
||||
const targetObjects = ((isSingleSprite ? [json] : json.targets) || [])
|
||||
.map((t, i) => Object.assign(t, {targetPaneOrder: i}))
|
||||
.sort((a, b) => a.layerOrder - b.layerOrder);
|
||||
|
||||
return Promise.all(
|
||||
((isSingleSprite ? [json] : json.targets) || []).map(target =>
|
||||
targetObjects.map(target =>
|
||||
parseScratchObject(target, runtime, extensions, zip))
|
||||
)
|
||||
.then(targets => targets // Re-sort targets back into original sprite-pane ordering
|
||||
.sort((a, b) => a.targetPaneOrder - b.targetPaneOrder)
|
||||
.map(t => {
|
||||
// Delete the temporary properties used for
|
||||
// sprite pane ordering and stage layer ordering
|
||||
delete t.targetPaneOrder;
|
||||
delete t.layerOrder;
|
||||
return t;
|
||||
}))
|
||||
.then(targets => ({
|
||||
targets,
|
||||
extensions
|
||||
|
|
|
@ -879,6 +879,13 @@ class RenderedTarget extends Target {
|
|||
return false;
|
||||
}
|
||||
|
||||
getLayerOrder () {
|
||||
if (this.renderer) {
|
||||
return this.renderer.getDrawableOrder(this.drawableID);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move to the front layer.
|
||||
*/
|
||||
|
|
|
@ -63,6 +63,20 @@ class MathUtil {
|
|||
return parseFloat(Math.tan((Math.PI * angle) / 180).toFixed(10));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Given an array of unique numbers,
|
||||
* returns a reduced array such that each element of the reduced array
|
||||
* represents the position of that element in a sorted version of the
|
||||
* original array.
|
||||
* E.g. [5, 19. 13, 1] => [1, 3, 2, 0]
|
||||
* @param {Array<number>} elts The elements to sort and reduce
|
||||
* @return {Array<number>} The array of reduced orderings
|
||||
*/
|
||||
static reducedSortOrdering (elts) {
|
||||
const sorted = elts.slice(0).sort((a, b) => a - b);
|
||||
return elts.map(e => sorted.indexOf(e));
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = MathUtil;
|
||||
|
|
8
test/fixtures/fake-renderer.js
vendored
8
test/fixtures/fake-renderer.js
vendored
|
@ -34,10 +34,6 @@ FakeRenderer.prototype.isTouchingColor = function (d, c) { // eslint-disable-lin
|
|||
return true;
|
||||
};
|
||||
|
||||
FakeRenderer.prototype.setDrawableOrder = function (d, l, optA, optB) { // eslint-disable-line no-unused-vars
|
||||
return true;
|
||||
};
|
||||
|
||||
FakeRenderer.prototype.getBounds = function (d) { // eslint-disable-line no-unused-vars
|
||||
return {left: this.x, right: this.x, top: this.y, bottom: this.y};
|
||||
};
|
||||
|
@ -55,6 +51,10 @@ FakeRenderer.prototype.setDrawableOrder = function (d, a, optG, optA, optB) { //
|
|||
return this.order;
|
||||
};
|
||||
|
||||
FakeRenderer.prototype.getDrawableOrder = function (d) { // eslint-disable-line no-unused-vars
|
||||
return 'stub';
|
||||
};
|
||||
|
||||
FakeRenderer.prototype.pick = function (x, y, a, b, c) { // eslint-disable-line no-unused-vars
|
||||
return c[0];
|
||||
};
|
||||
|
|
|
@ -7,6 +7,7 @@ const exampleProjectPath = path.resolve(__dirname, '../fixtures/clone-cleanup.sb
|
|||
const commentsSB2ProjectPath = path.resolve(__dirname, '../fixtures/comments.sb2');
|
||||
const commentsSB3ProjectPath = path.resolve(__dirname, '../fixtures/comments.sb3');
|
||||
const commentsSB3NoDupeIds = path.resolve(__dirname, '../fixtures/comments_no_duplicate_id_serialization.sb3');
|
||||
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||
|
||||
test('serialize', t => {
|
||||
const vm = new VirtualMachine();
|
||||
|
@ -142,3 +143,42 @@ test('deserialize sb3 project with comments - no duplicate id serialization', t
|
|||
t.end();
|
||||
});
|
||||
});
|
||||
|
||||
test('serialize sb3 preserves sprite layer order', t => {
|
||||
const vm = new VirtualMachine();
|
||||
vm.attachRenderer(new FakeRenderer());
|
||||
vm.loadProject(readFileToBuffer(path.resolve(__dirname, '../fixtures/ordering.sb2')))
|
||||
.then(() => {
|
||||
// Target get layer order needs a renderer,
|
||||
// fake the numbers we would get back from the
|
||||
// renderer in order to test that they are serialized
|
||||
// correctly
|
||||
vm.runtime.targets[0].getLayerOrder = () => 0;
|
||||
vm.runtime.targets[1].getLayerOrder = () => 20;
|
||||
vm.runtime.targets[2].getLayerOrder = () => 10;
|
||||
vm.runtime.targets[3].getLayerOrder = () => 30;
|
||||
|
||||
const result = sb3.serialize(vm.runtime);
|
||||
|
||||
t.type(JSON.stringify(result), 'string');
|
||||
t.type(result.targets, 'object');
|
||||
t.equal(Array.isArray(result.targets), true);
|
||||
t.equal(result.targets.length, 4);
|
||||
|
||||
// First check that the sprites are ordered correctly (as they would
|
||||
// appear in the target pane)
|
||||
t.equal(result.targets[0].name, 'Stage');
|
||||
t.equal(result.targets[1].name, 'First');
|
||||
t.equal(result.targets[2].name, 'Second');
|
||||
t.equal(result.targets[3].name, 'Third');
|
||||
|
||||
// Check that they are in the correct layer order (as they would render
|
||||
// back to front on the stage)
|
||||
t.equal(result.targets[0].layerOrder, 0);
|
||||
t.equal(result.targets[1].layerOrder, 2);
|
||||
t.equal(result.targets[2].layerOrder, 1);
|
||||
t.equal(result.targets[3].layerOrder, 3);
|
||||
|
||||
t.end();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -326,6 +326,23 @@ test('layers', t => { // TODO this tests fake functionality. Move layering tests
|
|||
t.end();
|
||||
});
|
||||
|
||||
test('getLayerOrder returns result of renderer getDrawableOrder or null if renderer is not attached', t => {
|
||||
const s = new Sprite();
|
||||
const r = new Runtime();
|
||||
const a = new RenderedTarget(s, r);
|
||||
|
||||
// getLayerOrder should return null if there is no renderer attached to the runtime
|
||||
t.equal(a.getLayerOrder(), null);
|
||||
|
||||
const renderer = new FakeRenderer();
|
||||
r.attachRenderer(renderer);
|
||||
const b = new RenderedTarget(s, r);
|
||||
|
||||
t.equal(b.getLayerOrder(), 'stub');
|
||||
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('keepInFence', t => {
|
||||
const s = new Sprite();
|
||||
const r = new Runtime();
|
||||
|
|
|
@ -42,3 +42,9 @@ test('tan', t => {
|
|||
t.strictEqual(math.tan(33), 0.6494075932);
|
||||
t.end();
|
||||
});
|
||||
|
||||
test('reducedSortOrdering', t => {
|
||||
t.deepEqual(math.reducedSortOrdering([5, 18, 6, 3]), [1, 3, 2, 0]);
|
||||
t.deepEqual(math.reducedSortOrdering([5, 1, 56, 19]), [1, 0, 3, 2]);
|
||||
t.end();
|
||||
});
|
||||
|
|
Loading…
Reference in a new issue