mirror of
https://github.com/scratchfoundation/scratch-vm.git
synced 2024-12-23 14:32:59 -05:00
Merge pull request #1392 from kchadha/sprite-layer-save-load
Sprite Layer Ordering Save/Load
This commit is contained in:
commit
c77b1f25e2
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 StageLayering = require('../engine/stage-layering');
|
||||||
const log = require('../util/log');
|
const log = require('../util/log');
|
||||||
const uid = require('../util/uid');
|
const uid = require('../util/uid');
|
||||||
|
const MathUtil = require('../util/math-util');
|
||||||
|
|
||||||
const {loadCostume} = require('../import/load-costume.js');
|
const {loadCostume} = require('../import/load-costume.js');
|
||||||
const {loadSound} = require('../import/load-sound.js');
|
const {loadSound} = require('../import/load-sound.js');
|
||||||
|
@ -437,6 +438,7 @@ const serializeTarget = function (target, extensions) {
|
||||||
obj.costumes = target.costumes.map(serializeCostume);
|
obj.costumes = target.costumes.map(serializeCostume);
|
||||||
obj.sounds = target.sounds.map(serializeSound);
|
obj.sounds = target.sounds.map(serializeSound);
|
||||||
if (target.hasOwnProperty('volume')) obj.volume = target.volume;
|
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 (obj.isStage) { // Only the stage should have these properties
|
||||||
if (target.hasOwnProperty('tempo')) obj.tempo = target.tempo;
|
if (target.hasOwnProperty('tempo')) obj.tempo = target.tempo;
|
||||||
if (target.hasOwnProperty('videoTransparency')) obj.videoTransparency = target.videoTransparency;
|
if (target.hasOwnProperty('videoTransparency')) obj.videoTransparency = target.videoTransparency;
|
||||||
|
@ -458,6 +460,11 @@ const serializeTarget = function (target, extensions) {
|
||||||
return obj;
|
return obj;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getSimplifiedLayerOrdering = function (targets) {
|
||||||
|
const layerOrders = targets.map(t => t.getLayerOrder());
|
||||||
|
return MathUtil.reducedSortOrdering(layerOrders);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Serializes the specified VM runtime.
|
* Serializes the specified VM runtime.
|
||||||
* @param {!Runtime} runtime VM runtime instance to be serialized.
|
* @param {!Runtime} runtime VM runtime instance to be serialized.
|
||||||
|
@ -469,9 +476,20 @@ const serialize = function (runtime, targetId) {
|
||||||
const obj = Object.create(null);
|
const obj = Object.create(null);
|
||||||
// Create extension set to hold extension ids found while serializing targets
|
// Create extension set to hold extension ids found while serializing targets
|
||||||
const extensions = new Set();
|
const extensions = new Set();
|
||||||
const flattenedOriginalTargets = JSON.parse(JSON.stringify(targetId ?
|
|
||||||
|
const originalTargetsToSerialize = targetId ?
|
||||||
[runtime.getTargetById(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));
|
const serializedTargets = flattenedOriginalTargets.map(t => serializeTarget(t, extensions));
|
||||||
|
|
||||||
|
@ -930,6 +948,12 @@ const parseScratchObject = function (object, runtime, extensions, zip) {
|
||||||
if (object.hasOwnProperty('isStage')) {
|
if (object.hasOwnProperty('isStage')) {
|
||||||
target.isStage = object.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 => {
|
Promise.all(costumePromises).then(costumes => {
|
||||||
sprite.costumes = costumes;
|
sprite.costumes = costumes;
|
||||||
});
|
});
|
||||||
|
@ -952,10 +976,28 @@ const deserialize = function (json, runtime, zip, isSingleSprite) {
|
||||||
extensionIDs: new Set(),
|
extensionIDs: new Set(),
|
||||||
extensionURLs: new Map()
|
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(
|
return Promise.all(
|
||||||
((isSingleSprite ? [json] : json.targets) || []).map(target =>
|
targetObjects.map(target =>
|
||||||
parseScratchObject(target, runtime, extensions, zip))
|
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 => ({
|
.then(targets => ({
|
||||||
targets,
|
targets,
|
||||||
extensions
|
extensions
|
||||||
|
|
|
@ -879,6 +879,13 @@ class RenderedTarget extends Target {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getLayerOrder () {
|
||||||
|
if (this.renderer) {
|
||||||
|
return this.renderer.getDrawableOrder(this.drawableID);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Move to the front layer.
|
* Move to the front layer.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -63,6 +63,20 @@ class MathUtil {
|
||||||
return parseFloat(Math.tan((Math.PI * angle) / 180).toFixed(10));
|
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;
|
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;
|
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
|
FakeRenderer.prototype.getBounds = function (d) { // eslint-disable-line no-unused-vars
|
||||||
return {left: this.x, right: this.x, top: this.y, bottom: this.y};
|
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;
|
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
|
FakeRenderer.prototype.pick = function (x, y, a, b, c) { // eslint-disable-line no-unused-vars
|
||||||
return c[0];
|
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 commentsSB2ProjectPath = path.resolve(__dirname, '../fixtures/comments.sb2');
|
||||||
const commentsSB3ProjectPath = path.resolve(__dirname, '../fixtures/comments.sb3');
|
const commentsSB3ProjectPath = path.resolve(__dirname, '../fixtures/comments.sb3');
|
||||||
const commentsSB3NoDupeIds = path.resolve(__dirname, '../fixtures/comments_no_duplicate_id_serialization.sb3');
|
const commentsSB3NoDupeIds = path.resolve(__dirname, '../fixtures/comments_no_duplicate_id_serialization.sb3');
|
||||||
|
const FakeRenderer = require('../fixtures/fake-renderer');
|
||||||
|
|
||||||
test('serialize', t => {
|
test('serialize', t => {
|
||||||
const vm = new VirtualMachine();
|
const vm = new VirtualMachine();
|
||||||
|
@ -142,3 +143,42 @@ test('deserialize sb3 project with comments - no duplicate id serialization', t
|
||||||
t.end();
|
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();
|
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 => {
|
test('keepInFence', t => {
|
||||||
const s = new Sprite();
|
const s = new Sprite();
|
||||||
const r = new Runtime();
|
const r = new Runtime();
|
||||||
|
|
|
@ -42,3 +42,9 @@ test('tan', t => {
|
||||||
t.strictEqual(math.tan(33), 0.6494075932);
|
t.strictEqual(math.tan(33), 0.6494075932);
|
||||||
t.end();
|
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