@scratch/scratch-vm 11.0.0-UEPR-176
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +12 -0
- package/README.md +120 -0
- package/dist/node/extension-worker.js +2 -0
- package/dist/node/extension-worker.js.map +1 -0
- package/dist/node/scratch-vm.js +3 -0
- package/dist/node/scratch-vm.js.LICENSE.txt +205 -0
- package/dist/node/scratch-vm.js.map +1 -0
- package/dist/web/extension-worker.js +2 -0
- package/dist/web/extension-worker.js.map +1 -0
- package/dist/web/scratch-vm.js +3 -0
- package/dist/web/scratch-vm.js.LICENSE.txt +42 -0
- package/dist/web/scratch-vm.js.map +1 -0
- package/package.json +108 -0
- package/src/.eslintrc.js +7 -0
- package/src/blocks/scratch3_control.js +206 -0
- package/src/blocks/scratch3_core_example.js +69 -0
- package/src/blocks/scratch3_data.js +257 -0
- package/src/blocks/scratch3_event.js +137 -0
- package/src/blocks/scratch3_looks.js +612 -0
- package/src/blocks/scratch3_motion.js +288 -0
- package/src/blocks/scratch3_operators.js +154 -0
- package/src/blocks/scratch3_procedures.js +79 -0
- package/src/blocks/scratch3_sensing.js +336 -0
- package/src/blocks/scratch3_sound.js +350 -0
- package/src/dispatch/central-dispatch.js +143 -0
- package/src/dispatch/shared-dispatch.js +235 -0
- package/src/dispatch/worker-dispatch.js +110 -0
- package/src/engine/adapter.js +176 -0
- package/src/engine/block-utility.js +242 -0
- package/src/engine/blocks-execute-cache.js +19 -0
- package/src/engine/blocks-runtime-cache.js +78 -0
- package/src/engine/blocks.js +1299 -0
- package/src/engine/comment.js +56 -0
- package/src/engine/execute.js +580 -0
- package/src/engine/monitor-record.js +23 -0
- package/src/engine/mutation-adapter.js +48 -0
- package/src/engine/profiler.js +390 -0
- package/src/engine/runtime.js +2686 -0
- package/src/engine/scratch-blocks-constants.js +27 -0
- package/src/engine/sequencer.js +361 -0
- package/src/engine/stage-layering.js +29 -0
- package/src/engine/target.js +799 -0
- package/src/engine/thread.js +404 -0
- package/src/engine/variable.js +70 -0
- package/src/extension-support/argument-type.js +47 -0
- package/src/extension-support/block-type.js +50 -0
- package/src/extension-support/define-messages.js +18 -0
- package/src/extension-support/extension-manager.js +440 -0
- package/src/extension-support/extension-metadata.js +64 -0
- package/src/extension-support/extension-worker.js +59 -0
- package/src/extension-support/reporter-scope.js +18 -0
- package/src/extension-support/target-type.js +17 -0
- package/src/extensions/scratch3_boost/index.js +2113 -0
- package/src/extensions/scratch3_ev3/index.js +1355 -0
- package/src/extensions/scratch3_gdx_for/index.js +981 -0
- package/src/extensions/scratch3_gdx_for/scratch-link-device-adapter.js +44 -0
- package/src/extensions/scratch3_makeymakey/index.js +396 -0
- package/src/extensions/scratch3_microbit/index.js +984 -0
- package/src/extensions/scratch3_music/assets/drums/1-snare.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/10-wood-block.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/11-cowbell.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/12-triangle.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/13-bongo.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/14-conga.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/15-cabasa.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/16-guiro.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/17-vibraslap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/18-cuica.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/2-bass-drum.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/3-side-stick.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/4-crash-cymbal.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/5-open-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/6-closed-hi-hat.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/7-tambourine.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/8-hand-clap.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/drums/9-claves.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/108.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/24.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/1-piano/96.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/10-clarinet/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/11-saxophone/84.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/12-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/13-wooden-flute/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/14-bassoon/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/15-choir/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/16-vibraphone/72.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/17-music-box/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/18-steel-drum/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/19-marimba/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/2-electric-piano/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/20-synth-lead/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/21-synth-pad/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/3-organ/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/4-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/5-electric-guitar/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/6-bass/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/7-pizzicato/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/8-cello/60.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/36.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/48.mp3 +0 -0
- package/src/extensions/scratch3_music/assets/instruments/9-trombone/60.mp3 +0 -0
- package/src/extensions/scratch3_music/index.js +1333 -0
- package/src/extensions/scratch3_music/manifest.js +63 -0
- package/src/extensions/scratch3_pen/index.js +770 -0
- package/src/extensions/scratch3_speech2text/index.js +700 -0
- package/src/extensions/scratch3_text2speech/index.js +766 -0
- package/src/extensions/scratch3_translate/index.js +286 -0
- package/src/extensions/scratch3_video_sensing/debug.js +13 -0
- package/src/extensions/scratch3_video_sensing/index.js +594 -0
- package/src/extensions/scratch3_video_sensing/library.js +384 -0
- package/src/extensions/scratch3_video_sensing/math.js +76 -0
- package/src/extensions/scratch3_video_sensing/view.js +509 -0
- package/src/extensions/scratch3_wedo2/index.js +1616 -0
- package/src/import/load-costume.js +416 -0
- package/src/import/load-sound.js +116 -0
- package/src/index.js +10 -0
- package/src/io/ble.js +256 -0
- package/src/io/bt.js +202 -0
- package/src/io/clock.js +39 -0
- package/src/io/cloud.js +169 -0
- package/src/io/keyboard.js +146 -0
- package/src/io/mouse.js +145 -0
- package/src/io/mouseWheel.js +28 -0
- package/src/io/userData.js +24 -0
- package/src/io/video.js +210 -0
- package/src/playground/benchmark.css +83 -0
- package/src/playground/benchmark.js +753 -0
- package/src/playground/index.html +111 -0
- package/src/playground/suite.css +78 -0
- package/src/playground/suite.html +25 -0
- package/src/playground/suite.js +544 -0
- package/src/playground/video-sensing.html +18 -0
- package/src/playground/video-sensing.js +133 -0
- package/src/serialization/deserialize-assets.js +178 -0
- package/src/serialization/sb2.js +1295 -0
- package/src/serialization/sb2_specmap.js +1818 -0
- package/src/serialization/sb3.js +1320 -0
- package/src/serialization/serialize-assets.js +60 -0
- package/src/sprites/rendered-target.js +1115 -0
- package/src/sprites/sprite.js +177 -0
- package/src/util/base64-util.js +48 -0
- package/src/util/cast.js +218 -0
- package/src/util/clone.js +17 -0
- package/src/util/color.js +204 -0
- package/src/util/fetch-with-timeout.js +53 -0
- package/src/util/get-monitor-id.js +33 -0
- package/src/util/jsonrpc.js +114 -0
- package/src/util/log.js +4 -0
- package/src/util/math-util.js +122 -0
- package/src/util/maybe-format-message.js +18 -0
- package/src/util/new-block-ids.js +33 -0
- package/src/util/rateLimiter.js +73 -0
- package/src/util/scratch-link-websocket.js +133 -0
- package/src/util/string-util.js +94 -0
- package/src/util/task-queue.js +203 -0
- package/src/util/timer.js +103 -0
- package/src/util/uid.js +29 -0
- package/src/util/variable-util.js +48 -0
- package/src/util/xml-escape.js +33 -0
- package/src/virtual-machine.js +1586 -0
|
@@ -0,0 +1,1586 @@
|
|
|
1
|
+
let _TextEncoder;
|
|
2
|
+
if (typeof TextEncoder === 'undefined') {
|
|
3
|
+
_TextEncoder = require('text-encoding').TextEncoder;
|
|
4
|
+
} else {
|
|
5
|
+
_TextEncoder = TextEncoder;
|
|
6
|
+
}
|
|
7
|
+
const EventEmitter = require('events');
|
|
8
|
+
const JSZip = require('jszip');
|
|
9
|
+
|
|
10
|
+
const Buffer = require('buffer').Buffer;
|
|
11
|
+
const centralDispatch = require('./dispatch/central-dispatch');
|
|
12
|
+
const ExtensionManager = require('./extension-support/extension-manager');
|
|
13
|
+
const log = require('./util/log');
|
|
14
|
+
const MathUtil = require('./util/math-util');
|
|
15
|
+
const Runtime = require('./engine/runtime');
|
|
16
|
+
const StringUtil = require('./util/string-util');
|
|
17
|
+
const formatMessage = require('format-message');
|
|
18
|
+
|
|
19
|
+
const Variable = require('./engine/variable');
|
|
20
|
+
const newBlockIds = require('./util/new-block-ids');
|
|
21
|
+
|
|
22
|
+
const {loadCostume} = require('./import/load-costume.js');
|
|
23
|
+
const {loadSound} = require('./import/load-sound.js');
|
|
24
|
+
const {serializeSounds, serializeCostumes} = require('./serialization/serialize-assets');
|
|
25
|
+
require('canvas-toBlob');
|
|
26
|
+
|
|
27
|
+
const RESERVED_NAMES = ['_mouse_', '_stage_', '_edge_', '_myself_', '_random_'];
|
|
28
|
+
|
|
29
|
+
const CORE_EXTENSIONS = [
|
|
30
|
+
// 'motion',
|
|
31
|
+
// 'looks',
|
|
32
|
+
// 'sound',
|
|
33
|
+
// 'events',
|
|
34
|
+
// 'control',
|
|
35
|
+
// 'sensing',
|
|
36
|
+
// 'operators',
|
|
37
|
+
// 'variables',
|
|
38
|
+
// 'myBlocks'
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Handles connections between blocks, stage, and extensions.
|
|
43
|
+
* @constructor
|
|
44
|
+
*/
|
|
45
|
+
class VirtualMachine extends EventEmitter {
|
|
46
|
+
constructor () {
|
|
47
|
+
super();
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* VM runtime, to store blocks, I/O devices, sprites/targets, etc.
|
|
51
|
+
* @type {!Runtime}
|
|
52
|
+
*/
|
|
53
|
+
this.runtime = new Runtime();
|
|
54
|
+
centralDispatch.setService('runtime', this.runtime).catch(e => {
|
|
55
|
+
log.error(`Failed to register runtime service: ${JSON.stringify(e)}`);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The "currently editing"/selected target ID for the VM.
|
|
60
|
+
* Block events from any Blockly workspace are routed to this target.
|
|
61
|
+
* @type {Target}
|
|
62
|
+
*/
|
|
63
|
+
this.editingTarget = null;
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The currently dragging target, for redirecting IO data.
|
|
67
|
+
* @type {Target}
|
|
68
|
+
*/
|
|
69
|
+
this._dragTarget = null;
|
|
70
|
+
|
|
71
|
+
// Runtime emits are passed along as VM emits.
|
|
72
|
+
this.runtime.on(Runtime.SCRIPT_GLOW_ON, glowData => {
|
|
73
|
+
this.emit(Runtime.SCRIPT_GLOW_ON, glowData);
|
|
74
|
+
});
|
|
75
|
+
this.runtime.on(Runtime.SCRIPT_GLOW_OFF, glowData => {
|
|
76
|
+
this.emit(Runtime.SCRIPT_GLOW_OFF, glowData);
|
|
77
|
+
});
|
|
78
|
+
this.runtime.on(Runtime.BLOCK_GLOW_ON, glowData => {
|
|
79
|
+
this.emit(Runtime.BLOCK_GLOW_ON, glowData);
|
|
80
|
+
});
|
|
81
|
+
this.runtime.on(Runtime.BLOCK_GLOW_OFF, glowData => {
|
|
82
|
+
this.emit(Runtime.BLOCK_GLOW_OFF, glowData);
|
|
83
|
+
});
|
|
84
|
+
this.runtime.on(Runtime.PROJECT_START, () => {
|
|
85
|
+
this.emit(Runtime.PROJECT_START);
|
|
86
|
+
});
|
|
87
|
+
this.runtime.on(Runtime.PROJECT_RUN_START, () => {
|
|
88
|
+
this.emit(Runtime.PROJECT_RUN_START);
|
|
89
|
+
});
|
|
90
|
+
this.runtime.on(Runtime.PROJECT_RUN_STOP, () => {
|
|
91
|
+
this.emit(Runtime.PROJECT_RUN_STOP);
|
|
92
|
+
});
|
|
93
|
+
this.runtime.on(Runtime.PROJECT_CHANGED, () => {
|
|
94
|
+
this.emit(Runtime.PROJECT_CHANGED);
|
|
95
|
+
});
|
|
96
|
+
this.runtime.on(Runtime.VISUAL_REPORT, visualReport => {
|
|
97
|
+
this.emit(Runtime.VISUAL_REPORT, visualReport);
|
|
98
|
+
});
|
|
99
|
+
this.runtime.on(Runtime.TARGETS_UPDATE, emitProjectChanged => {
|
|
100
|
+
this.emitTargetsUpdate(emitProjectChanged);
|
|
101
|
+
});
|
|
102
|
+
this.runtime.on(Runtime.MONITORS_UPDATE, monitorList => {
|
|
103
|
+
this.emit(Runtime.MONITORS_UPDATE, monitorList);
|
|
104
|
+
});
|
|
105
|
+
this.runtime.on(Runtime.BLOCK_DRAG_UPDATE, areBlocksOverGui => {
|
|
106
|
+
this.emit(Runtime.BLOCK_DRAG_UPDATE, areBlocksOverGui);
|
|
107
|
+
});
|
|
108
|
+
this.runtime.on(Runtime.BLOCK_DRAG_END, (blocks, topBlockId) => {
|
|
109
|
+
this.emit(Runtime.BLOCK_DRAG_END, blocks, topBlockId);
|
|
110
|
+
});
|
|
111
|
+
this.runtime.on(Runtime.EXTENSION_ADDED, categoryInfo => {
|
|
112
|
+
this.emit(Runtime.EXTENSION_ADDED, categoryInfo);
|
|
113
|
+
});
|
|
114
|
+
this.runtime.on(Runtime.EXTENSION_FIELD_ADDED, (fieldName, fieldImplementation) => {
|
|
115
|
+
this.emit(Runtime.EXTENSION_FIELD_ADDED, fieldName, fieldImplementation);
|
|
116
|
+
});
|
|
117
|
+
this.runtime.on(Runtime.BLOCKSINFO_UPDATE, categoryInfo => {
|
|
118
|
+
this.emit(Runtime.BLOCKSINFO_UPDATE, categoryInfo);
|
|
119
|
+
});
|
|
120
|
+
this.runtime.on(Runtime.BLOCKS_NEED_UPDATE, () => {
|
|
121
|
+
this.emitWorkspaceUpdate();
|
|
122
|
+
});
|
|
123
|
+
this.runtime.on(Runtime.TOOLBOX_EXTENSIONS_NEED_UPDATE, () => {
|
|
124
|
+
this.extensionManager.refreshBlocks();
|
|
125
|
+
});
|
|
126
|
+
this.runtime.on(Runtime.PERIPHERAL_LIST_UPDATE, info => {
|
|
127
|
+
this.emit(Runtime.PERIPHERAL_LIST_UPDATE, info);
|
|
128
|
+
});
|
|
129
|
+
this.runtime.on(Runtime.USER_PICKED_PERIPHERAL, info => {
|
|
130
|
+
this.emit(Runtime.USER_PICKED_PERIPHERAL, info);
|
|
131
|
+
});
|
|
132
|
+
this.runtime.on(Runtime.PERIPHERAL_CONNECTED, () =>
|
|
133
|
+
this.emit(Runtime.PERIPHERAL_CONNECTED)
|
|
134
|
+
);
|
|
135
|
+
this.runtime.on(Runtime.PERIPHERAL_REQUEST_ERROR, () =>
|
|
136
|
+
this.emit(Runtime.PERIPHERAL_REQUEST_ERROR)
|
|
137
|
+
);
|
|
138
|
+
this.runtime.on(Runtime.PERIPHERAL_DISCONNECTED, () =>
|
|
139
|
+
this.emit(Runtime.PERIPHERAL_DISCONNECTED)
|
|
140
|
+
);
|
|
141
|
+
this.runtime.on(Runtime.PERIPHERAL_CONNECTION_LOST_ERROR, data =>
|
|
142
|
+
this.emit(Runtime.PERIPHERAL_CONNECTION_LOST_ERROR, data)
|
|
143
|
+
);
|
|
144
|
+
this.runtime.on(Runtime.PERIPHERAL_SCAN_TIMEOUT, () =>
|
|
145
|
+
this.emit(Runtime.PERIPHERAL_SCAN_TIMEOUT)
|
|
146
|
+
);
|
|
147
|
+
this.runtime.on(Runtime.MIC_LISTENING, listening => {
|
|
148
|
+
this.emit(Runtime.MIC_LISTENING, listening);
|
|
149
|
+
});
|
|
150
|
+
this.runtime.on(Runtime.RUNTIME_STARTED, () => {
|
|
151
|
+
this.emit(Runtime.RUNTIME_STARTED);
|
|
152
|
+
});
|
|
153
|
+
this.runtime.on(Runtime.HAS_CLOUD_DATA_UPDATE, hasCloudData => {
|
|
154
|
+
this.emit(Runtime.HAS_CLOUD_DATA_UPDATE, hasCloudData);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
this.extensionManager = new ExtensionManager(this.runtime);
|
|
158
|
+
|
|
159
|
+
// Load core extensions
|
|
160
|
+
for (const id of CORE_EXTENSIONS) {
|
|
161
|
+
this.extensionManager.loadExtensionIdSync(id);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
this.blockListener = this.blockListener.bind(this);
|
|
165
|
+
this.flyoutBlockListener = this.flyoutBlockListener.bind(this);
|
|
166
|
+
this.monitorBlockListener = this.monitorBlockListener.bind(this);
|
|
167
|
+
this.variableListener = this.variableListener.bind(this);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Start running the VM - do this before anything else.
|
|
172
|
+
*/
|
|
173
|
+
start () {
|
|
174
|
+
this.runtime.start();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Quit the VM, clearing any handles which might keep the process alive.
|
|
179
|
+
* Do not use the runtime after calling this method. This method is meant for test shutdown.
|
|
180
|
+
*/
|
|
181
|
+
quit () {
|
|
182
|
+
this.runtime.quit();
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* "Green flag" handler - start all threads starting with a green flag.
|
|
187
|
+
*/
|
|
188
|
+
greenFlag () {
|
|
189
|
+
this.runtime.greenFlag();
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Set whether the VM is in "turbo mode."
|
|
194
|
+
* When true, loops don't yield to redraw.
|
|
195
|
+
* @param {boolean} turboModeOn Whether turbo mode should be set.
|
|
196
|
+
*/
|
|
197
|
+
setTurboMode (turboModeOn) {
|
|
198
|
+
this.runtime.turboMode = !!turboModeOn;
|
|
199
|
+
if (this.runtime.turboMode) {
|
|
200
|
+
this.emit(Runtime.TURBO_MODE_ON);
|
|
201
|
+
} else {
|
|
202
|
+
this.emit(Runtime.TURBO_MODE_OFF);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Set whether the VM is in 2.0 "compatibility mode."
|
|
208
|
+
* When true, ticks go at 2.0 speed (30 TPS).
|
|
209
|
+
* @param {boolean} compatibilityModeOn Whether compatibility mode is set.
|
|
210
|
+
*/
|
|
211
|
+
setCompatibilityMode (compatibilityModeOn) {
|
|
212
|
+
this.runtime.setCompatibilityMode(!!compatibilityModeOn);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Stop all threads and running activities.
|
|
217
|
+
*/
|
|
218
|
+
stopAll () {
|
|
219
|
+
this.runtime.stopAll();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Clear out current running project data.
|
|
224
|
+
*/
|
|
225
|
+
clear () {
|
|
226
|
+
this.runtime.dispose();
|
|
227
|
+
this.editingTarget = null;
|
|
228
|
+
this.emitTargetsUpdate(false /* Don't emit project change */);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Get data for playground. Data comes back in an emitted event.
|
|
233
|
+
*/
|
|
234
|
+
getPlaygroundData () {
|
|
235
|
+
const instance = this;
|
|
236
|
+
// Only send back thread data for the current editingTarget.
|
|
237
|
+
const threadData = this.runtime.threads.filter(thread => thread.target === instance.editingTarget);
|
|
238
|
+
// Remove the target key, since it's a circular reference.
|
|
239
|
+
const filteredThreadData = JSON.stringify(threadData, (key, value) => {
|
|
240
|
+
if (key === 'target' || key === 'blockContainer') return;
|
|
241
|
+
return value;
|
|
242
|
+
}, 2);
|
|
243
|
+
this.emit('playgroundData', {
|
|
244
|
+
blocks: this.editingTarget.blocks,
|
|
245
|
+
threads: filteredThreadData
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Post I/O data to the virtual devices.
|
|
251
|
+
* @param {?string} device Name of virtual I/O device.
|
|
252
|
+
* @param {object} data Any data object to post to the I/O device.
|
|
253
|
+
*/
|
|
254
|
+
postIOData (device, data) {
|
|
255
|
+
if (this.runtime.ioDevices[device]) {
|
|
256
|
+
this.runtime.ioDevices[device].postData(data);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
setVideoProvider (videoProvider) {
|
|
261
|
+
this.runtime.ioDevices.video.setProvider(videoProvider);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
setCloudProvider (cloudProvider) {
|
|
265
|
+
this.runtime.ioDevices.cloud.setProvider(cloudProvider);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Tell the specified extension to scan for a peripheral.
|
|
270
|
+
* @param {string} extensionId - the id of the extension.
|
|
271
|
+
*/
|
|
272
|
+
scanForPeripheral (extensionId) {
|
|
273
|
+
this.runtime.scanForPeripheral(extensionId);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Connect to the extension's specified peripheral.
|
|
278
|
+
* @param {string} extensionId - the id of the extension.
|
|
279
|
+
* @param {number} peripheralId - the id of the peripheral.
|
|
280
|
+
*/
|
|
281
|
+
connectPeripheral (extensionId, peripheralId) {
|
|
282
|
+
this.runtime.connectPeripheral(extensionId, peripheralId);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Disconnect from the extension's connected peripheral.
|
|
287
|
+
* @param {string} extensionId - the id of the extension.
|
|
288
|
+
*/
|
|
289
|
+
disconnectPeripheral (extensionId) {
|
|
290
|
+
this.runtime.disconnectPeripheral(extensionId);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Returns whether the extension has a currently connected peripheral.
|
|
295
|
+
* @param {string} extensionId - the id of the extension.
|
|
296
|
+
* @return {boolean} - whether the extension has a connected peripheral.
|
|
297
|
+
*/
|
|
298
|
+
getPeripheralIsConnected (extensionId) {
|
|
299
|
+
return this.runtime.getPeripheralIsConnected(extensionId);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/**
|
|
303
|
+
* Load a Scratch project from a .sb, .sb2, .sb3 or json string.
|
|
304
|
+
* @param {string | object} input A json string, object, or ArrayBuffer representing the project to load.
|
|
305
|
+
* @return {!Promise} Promise that resolves after targets are installed.
|
|
306
|
+
*/
|
|
307
|
+
loadProject (input) {
|
|
308
|
+
if (typeof input === 'object' && !(input instanceof ArrayBuffer) &&
|
|
309
|
+
!ArrayBuffer.isView(input)) {
|
|
310
|
+
// If the input is an object and not any ArrayBuffer
|
|
311
|
+
// or an ArrayBuffer view (this includes all typed arrays and DataViews)
|
|
312
|
+
// turn the object into a JSON string, because we suspect
|
|
313
|
+
// this is a project.json as an object
|
|
314
|
+
// validate expects a string or buffer as input
|
|
315
|
+
// TODO not sure if we need to check that it also isn't a data view
|
|
316
|
+
input = JSON.stringify(input);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
const validationPromise = new Promise((resolve, reject) => {
|
|
320
|
+
const validate = require('scratch-parser');
|
|
321
|
+
// The second argument of false below indicates to the validator that the
|
|
322
|
+
// input should be parsed/validated as an entire project (and not a single sprite)
|
|
323
|
+
validate(input, false, (error, res) => {
|
|
324
|
+
if (error) return reject(error);
|
|
325
|
+
resolve(res);
|
|
326
|
+
});
|
|
327
|
+
})
|
|
328
|
+
.catch(error => {
|
|
329
|
+
const {SB1File, ValidationError} = require('scratch-sb1-converter');
|
|
330
|
+
|
|
331
|
+
try {
|
|
332
|
+
const sb1 = new SB1File(input);
|
|
333
|
+
const json = sb1.json;
|
|
334
|
+
json.projectVersion = 2;
|
|
335
|
+
return Promise.resolve([json, sb1.zip]);
|
|
336
|
+
} catch (sb1Error) {
|
|
337
|
+
if (sb1Error instanceof ValidationError) {
|
|
338
|
+
// The input does not validate as a Scratch 1 file.
|
|
339
|
+
} else {
|
|
340
|
+
// The project appears to be a Scratch 1 file but it
|
|
341
|
+
// could not be successfully translated into a Scratch 2
|
|
342
|
+
// project.
|
|
343
|
+
return Promise.reject(sb1Error);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
// Throw original error since the input does not appear to be
|
|
347
|
+
// an SB1File.
|
|
348
|
+
return Promise.reject(error);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
return validationPromise
|
|
352
|
+
.then(validatedInput => this.deserializeProject(validatedInput[0], validatedInput[1]))
|
|
353
|
+
.then(() => this.runtime.handleProjectLoaded())
|
|
354
|
+
.catch(error => {
|
|
355
|
+
// Intentionally rejecting here (want errors to be handled by caller)
|
|
356
|
+
if (Object.prototype.hasOwnProperty.call(error, 'validationError')) {
|
|
357
|
+
return Promise.reject(JSON.stringify(error));
|
|
358
|
+
}
|
|
359
|
+
return Promise.reject(error);
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Load a project from the Scratch web site, by ID.
|
|
365
|
+
* @param {string} id - the ID of the project to download, as a string.
|
|
366
|
+
*/
|
|
367
|
+
downloadProjectId (id) {
|
|
368
|
+
const storage = this.runtime.storage;
|
|
369
|
+
if (!storage) {
|
|
370
|
+
log.error('No storage module present; cannot load project: ', id);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
const vm = this;
|
|
374
|
+
const promise = storage.load(storage.AssetType.Project, id);
|
|
375
|
+
promise.then(projectAsset => {
|
|
376
|
+
if (!projectAsset) {
|
|
377
|
+
log.error(`Failed to fetch project with id: ${id}`);
|
|
378
|
+
return null;
|
|
379
|
+
}
|
|
380
|
+
return vm.loadProject(projectAsset.data);
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
/**
|
|
385
|
+
* @returns {string} Project in a Scratch 3.0 JSON representation.
|
|
386
|
+
*/
|
|
387
|
+
saveProjectSb3 () {
|
|
388
|
+
const soundDescs = serializeSounds(this.runtime);
|
|
389
|
+
const costumeDescs = serializeCostumes(this.runtime);
|
|
390
|
+
const projectJson = this.toJSON();
|
|
391
|
+
|
|
392
|
+
// TODO want to eventually move zip creation out of here, and perhaps
|
|
393
|
+
// into scratch-storage
|
|
394
|
+
const zip = new JSZip();
|
|
395
|
+
|
|
396
|
+
// Put everything in a zip file
|
|
397
|
+
zip.file('project.json', projectJson);
|
|
398
|
+
this._addFileDescsToZip(soundDescs.concat(costumeDescs), zip);
|
|
399
|
+
|
|
400
|
+
return zip.generateAsync({
|
|
401
|
+
type: 'blob',
|
|
402
|
+
mimeType: 'application/x.scratch.sb3',
|
|
403
|
+
compression: 'DEFLATE',
|
|
404
|
+
compressionOptions: {
|
|
405
|
+
level: 6 // Tradeoff between best speed (1) and best compression (9)
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/*
|
|
411
|
+
* @type {Array<object>} Array of all costumes and sounds currently in the runtime
|
|
412
|
+
*/
|
|
413
|
+
get assets () {
|
|
414
|
+
return this.runtime.targets.reduce((acc, target) => (
|
|
415
|
+
acc
|
|
416
|
+
.concat(target.sprite.sounds.map(sound => sound.asset))
|
|
417
|
+
.concat(target.sprite.costumes.map(costume => costume.asset))
|
|
418
|
+
), []);
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
_addFileDescsToZip (fileDescs, zip) {
|
|
422
|
+
for (let i = 0; i < fileDescs.length; i++) {
|
|
423
|
+
const currFileDesc = fileDescs[i];
|
|
424
|
+
zip.file(currFileDesc.fileName, currFileDesc.fileContent);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Exports a sprite in the sprite3 format.
|
|
430
|
+
* @param {string} targetId ID of the target to export
|
|
431
|
+
* @param {string=} optZipType Optional type that the resulting
|
|
432
|
+
* zip should be outputted in. Options are: base64, binarystring,
|
|
433
|
+
* array, uint8array, arraybuffer, blob, or nodebuffer. Defaults to
|
|
434
|
+
* blob if argument not provided.
|
|
435
|
+
* See https://stuk.github.io/jszip/documentation/api_jszip/generate_async.html#type-option
|
|
436
|
+
* for more information about these options.
|
|
437
|
+
* @return {object} A generated zip of the sprite and its assets in the format
|
|
438
|
+
* specified by optZipType or blob by default.
|
|
439
|
+
*/
|
|
440
|
+
exportSprite (targetId, optZipType) {
|
|
441
|
+
const soundDescs = serializeSounds(this.runtime, targetId);
|
|
442
|
+
const costumeDescs = serializeCostumes(this.runtime, targetId);
|
|
443
|
+
const spriteJson = this.toJSON(targetId);
|
|
444
|
+
|
|
445
|
+
const zip = new JSZip();
|
|
446
|
+
zip.file('sprite.json', spriteJson);
|
|
447
|
+
this._addFileDescsToZip(soundDescs.concat(costumeDescs), zip);
|
|
448
|
+
|
|
449
|
+
return zip.generateAsync({
|
|
450
|
+
type: typeof optZipType === 'string' ? optZipType : 'blob',
|
|
451
|
+
mimeType: 'application/x.scratch.sprite3',
|
|
452
|
+
compression: 'DEFLATE',
|
|
453
|
+
compressionOptions: {
|
|
454
|
+
level: 6
|
|
455
|
+
}
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* Export project or sprite as a Scratch 3.0 JSON representation.
|
|
461
|
+
* @param {string=} optTargetId - Optional id of a sprite to serialize
|
|
462
|
+
* @return {string} Serialized state of the runtime.
|
|
463
|
+
*/
|
|
464
|
+
toJSON (optTargetId) {
|
|
465
|
+
const sb3 = require('./serialization/sb3');
|
|
466
|
+
return StringUtil.stringify(sb3.serialize(this.runtime, optTargetId));
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
// TODO do we still need this function? Keeping it here so as not to introduce
|
|
470
|
+
// a breaking change.
|
|
471
|
+
/**
|
|
472
|
+
* Load a project from a Scratch JSON representation.
|
|
473
|
+
* @param {string} json JSON string representing a project.
|
|
474
|
+
* @returns {Promise} Promise that resolves after the project has loaded
|
|
475
|
+
*/
|
|
476
|
+
fromJSON (json) {
|
|
477
|
+
log.warning('fromJSON is now just a wrapper around loadProject, please use that function instead.');
|
|
478
|
+
return this.loadProject(json);
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Load a project from a Scratch JSON representation.
|
|
483
|
+
* @param {string} projectJSON JSON string representing a project.
|
|
484
|
+
* @param {?JSZip} zip Optional zipped project containing assets to be loaded.
|
|
485
|
+
* @returns {Promise} Promise that resolves after the project has loaded
|
|
486
|
+
*/
|
|
487
|
+
deserializeProject (projectJSON, zip) {
|
|
488
|
+
// Clear the current runtime
|
|
489
|
+
this.clear();
|
|
490
|
+
|
|
491
|
+
if (typeof performance !== 'undefined') {
|
|
492
|
+
performance.mark('scratch-vm-deserialize-start');
|
|
493
|
+
}
|
|
494
|
+
const runtime = this.runtime;
|
|
495
|
+
const deserializePromise = function () {
|
|
496
|
+
const projectVersion = projectJSON.projectVersion;
|
|
497
|
+
if (projectVersion === 2) {
|
|
498
|
+
const sb2 = require('./serialization/sb2');
|
|
499
|
+
return sb2.deserialize(projectJSON, runtime, false, zip);
|
|
500
|
+
}
|
|
501
|
+
if (projectVersion === 3) {
|
|
502
|
+
const sb3 = require('./serialization/sb3');
|
|
503
|
+
return sb3.deserialize(projectJSON, runtime, zip);
|
|
504
|
+
}
|
|
505
|
+
// TODO: reject with an Error (possible breaking API change!)
|
|
506
|
+
// eslint-disable-next-line prefer-promise-reject-errors
|
|
507
|
+
return Promise.reject('Unable to verify Scratch Project version.');
|
|
508
|
+
};
|
|
509
|
+
return deserializePromise()
|
|
510
|
+
.then(({targets, extensions}) => {
|
|
511
|
+
if (typeof performance !== 'undefined') {
|
|
512
|
+
performance.mark('scratch-vm-deserialize-end');
|
|
513
|
+
performance.measure('scratch-vm-deserialize',
|
|
514
|
+
'scratch-vm-deserialize-start', 'scratch-vm-deserialize-end');
|
|
515
|
+
}
|
|
516
|
+
return this.installTargets(targets, extensions, true);
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Install `deserialize` results: zero or more targets after the extensions (if any) used by those targets.
|
|
522
|
+
* @param {Array.<Target>} targets - the targets to be installed
|
|
523
|
+
* @param {ImportedExtensionsInfo} extensions - metadata about extensions used by these targets
|
|
524
|
+
* @param {boolean} wholeProject - set to true if installing a whole project, as opposed to a single sprite.
|
|
525
|
+
* @returns {Promise} resolved once targets have been installed
|
|
526
|
+
*/
|
|
527
|
+
installTargets (targets, extensions, wholeProject) {
|
|
528
|
+
const extensionPromises = [];
|
|
529
|
+
|
|
530
|
+
extensions.extensionIDs.forEach(extensionID => {
|
|
531
|
+
if (!this.extensionManager.isExtensionLoaded(extensionID)) {
|
|
532
|
+
const extensionURL = extensions.extensionURLs.get(extensionID) || extensionID;
|
|
533
|
+
extensionPromises.push(this.extensionManager.loadExtensionURL(extensionURL));
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
targets = targets.filter(target => !!target);
|
|
538
|
+
|
|
539
|
+
return Promise.all(extensionPromises).then(() => {
|
|
540
|
+
targets.forEach(target => {
|
|
541
|
+
this.runtime.addTarget(target);
|
|
542
|
+
(/** @type RenderedTarget */ target).updateAllDrawableProperties();
|
|
543
|
+
// Ensure unique sprite name
|
|
544
|
+
if (target.isSprite()) this.renameSprite(target.id, target.getName());
|
|
545
|
+
});
|
|
546
|
+
// Sort the executable targets by layerOrder.
|
|
547
|
+
// Remove layerOrder property after use.
|
|
548
|
+
this.runtime.executableTargets.sort((a, b) => a.layerOrder - b.layerOrder);
|
|
549
|
+
targets.forEach(target => {
|
|
550
|
+
delete target.layerOrder;
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
// Select the first target for editing, e.g., the first sprite.
|
|
554
|
+
if (wholeProject && (targets.length > 1)) {
|
|
555
|
+
this.editingTarget = targets[1];
|
|
556
|
+
} else {
|
|
557
|
+
this.editingTarget = targets[0];
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (!wholeProject) {
|
|
561
|
+
this.editingTarget.fixUpVariableReferences();
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// Update the VM user's knowledge of targets and blocks on the workspace.
|
|
565
|
+
this.emitTargetsUpdate(false /* Don't emit project change */);
|
|
566
|
+
this.emitWorkspaceUpdate();
|
|
567
|
+
this.runtime.setEditingTarget(this.editingTarget);
|
|
568
|
+
this.runtime.ioDevices.cloud.setStage(this.runtime.getTargetForStage());
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/**
|
|
573
|
+
* Add a sprite, this could be .sprite2 or .sprite3. Unpack and validate
|
|
574
|
+
* such a file first.
|
|
575
|
+
* @param {string | object} input A json string, object, or ArrayBuffer representing the project to load.
|
|
576
|
+
* @return {!Promise} Promise that resolves after targets are installed.
|
|
577
|
+
*/
|
|
578
|
+
addSprite (input) {
|
|
579
|
+
const errorPrefix = 'Sprite Upload Error:';
|
|
580
|
+
if (typeof input === 'object' && !(input instanceof ArrayBuffer) &&
|
|
581
|
+
!ArrayBuffer.isView(input)) {
|
|
582
|
+
// If the input is an object and not any ArrayBuffer
|
|
583
|
+
// or an ArrayBuffer view (this includes all typed arrays and DataViews)
|
|
584
|
+
// turn the object into a JSON string, because we suspect
|
|
585
|
+
// this is a project.json as an object
|
|
586
|
+
// validate expects a string or buffer as input
|
|
587
|
+
// TODO not sure if we need to check that it also isn't a data view
|
|
588
|
+
input = JSON.stringify(input);
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
const validationPromise = new Promise((resolve, reject) => {
|
|
592
|
+
const validate = require('scratch-parser');
|
|
593
|
+
// The second argument of true below indicates to the parser/validator
|
|
594
|
+
// that the given input should be treated as a single sprite and not
|
|
595
|
+
// an entire project
|
|
596
|
+
validate(input, true, (error, res) => {
|
|
597
|
+
if (error) return reject(error);
|
|
598
|
+
resolve(res);
|
|
599
|
+
});
|
|
600
|
+
});
|
|
601
|
+
|
|
602
|
+
return validationPromise
|
|
603
|
+
.then(validatedInput => {
|
|
604
|
+
const projectVersion = validatedInput[0].projectVersion;
|
|
605
|
+
if (projectVersion === 2) {
|
|
606
|
+
return this._addSprite2(validatedInput[0], validatedInput[1]);
|
|
607
|
+
}
|
|
608
|
+
if (projectVersion === 3) {
|
|
609
|
+
return this._addSprite3(validatedInput[0], validatedInput[1]);
|
|
610
|
+
}
|
|
611
|
+
// TODO: reject with an Error (possible breaking API change!)
|
|
612
|
+
// eslint-disable-next-line prefer-promise-reject-errors
|
|
613
|
+
return Promise.reject(`${errorPrefix} Unable to verify sprite version.`);
|
|
614
|
+
})
|
|
615
|
+
.then(() => this.runtime.emitProjectChanged())
|
|
616
|
+
.catch(error => {
|
|
617
|
+
// Intentionally rejecting here (want errors to be handled by caller)
|
|
618
|
+
if (Object.prototype.hasOwnProperty.call(error, 'validationError')) {
|
|
619
|
+
return Promise.reject(JSON.stringify(error));
|
|
620
|
+
}
|
|
621
|
+
// TODO: reject with an Error (possible breaking API change!)
|
|
622
|
+
// eslint-disable-next-line prefer-promise-reject-errors
|
|
623
|
+
return Promise.reject(`${errorPrefix} ${error}`);
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Add a single sprite from the "Sprite2" (i.e., SB2 sprite) format.
|
|
629
|
+
* @param {object} sprite Object representing 2.0 sprite to be added.
|
|
630
|
+
* @param {?ArrayBuffer} zip Optional zip of assets being referenced by json
|
|
631
|
+
* @returns {Promise} Promise that resolves after the sprite is added
|
|
632
|
+
*/
|
|
633
|
+
_addSprite2 (sprite, zip) {
|
|
634
|
+
// Validate & parse
|
|
635
|
+
|
|
636
|
+
const sb2 = require('./serialization/sb2');
|
|
637
|
+
return sb2.deserialize(sprite, this.runtime, true, zip)
|
|
638
|
+
.then(({targets, extensions}) =>
|
|
639
|
+
this.installTargets(targets, extensions, false));
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* Add a single sb3 sprite.
|
|
644
|
+
* @param {object} sprite Object rperesenting 3.0 sprite to be added.
|
|
645
|
+
* @param {?ArrayBuffer} zip Optional zip of assets being referenced by target json
|
|
646
|
+
* @returns {Promise} Promise that resolves after the sprite is added
|
|
647
|
+
*/
|
|
648
|
+
_addSprite3 (sprite, zip) {
|
|
649
|
+
// Validate & parse
|
|
650
|
+
const sb3 = require('./serialization/sb3');
|
|
651
|
+
return sb3
|
|
652
|
+
.deserialize(sprite, this.runtime, zip, true)
|
|
653
|
+
.then(({targets, extensions}) => this.installTargets(targets, extensions, false));
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* Add a costume to the current editing target.
|
|
658
|
+
* @param {string} md5ext - the MD5 and extension of the costume to be loaded.
|
|
659
|
+
* @param {!object} costumeObject Object representing the costume.
|
|
660
|
+
* @property {int} skinId - the ID of the costume's render skin, once installed.
|
|
661
|
+
* @property {number} rotationCenterX - the X component of the costume's origin.
|
|
662
|
+
* @property {number} rotationCenterY - the Y component of the costume's origin.
|
|
663
|
+
* @property {number} [bitmapResolution] - the resolution scale for a bitmap costume.
|
|
664
|
+
* @param {string} optTargetId - the id of the target to add to, if not the editing target.
|
|
665
|
+
* @param {string} optVersion - if this is 2, load costume as sb2, otherwise load costume as sb3.
|
|
666
|
+
* @returns {?Promise} - a promise that resolves when the costume has been added
|
|
667
|
+
*/
|
|
668
|
+
addCostume (md5ext, costumeObject, optTargetId, optVersion) {
|
|
669
|
+
const target = optTargetId ? this.runtime.getTargetById(optTargetId) :
|
|
670
|
+
this.editingTarget;
|
|
671
|
+
if (target) {
|
|
672
|
+
return loadCostume(md5ext, costumeObject, this.runtime, optVersion).then(() => {
|
|
673
|
+
target.addCostume(costumeObject);
|
|
674
|
+
target.setCostume(
|
|
675
|
+
target.getCostumes().length - 1
|
|
676
|
+
);
|
|
677
|
+
this.runtime.emitProjectChanged();
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
// If the target cannot be found by id, return a rejected promise
|
|
681
|
+
// TODO: reject with an Error (possible breaking API change!)
|
|
682
|
+
// eslint-disable-next-line prefer-promise-reject-errors
|
|
683
|
+
return Promise.reject();
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/**
|
|
687
|
+
* Add a costume loaded from the library to the current editing target.
|
|
688
|
+
* @param {string} md5ext - the MD5 and extension of the costume to be loaded.
|
|
689
|
+
* @param {!object} costumeObject Object representing the costume.
|
|
690
|
+
* @property {int} skinId - the ID of the costume's render skin, once installed.
|
|
691
|
+
* @property {number} rotationCenterX - the X component of the costume's origin.
|
|
692
|
+
* @property {number} rotationCenterY - the Y component of the costume's origin.
|
|
693
|
+
* @property {number} [bitmapResolution] - the resolution scale for a bitmap costume.
|
|
694
|
+
* @returns {?Promise} - a promise that resolves when the costume has been added
|
|
695
|
+
*/
|
|
696
|
+
addCostumeFromLibrary (md5ext, costumeObject) {
|
|
697
|
+
// TODO: reject with an Error (possible breaking API change!)
|
|
698
|
+
// eslint-disable-next-line prefer-promise-reject-errors
|
|
699
|
+
if (!this.editingTarget) return Promise.reject();
|
|
700
|
+
return this.addCostume(md5ext, costumeObject, this.editingTarget.id, 2 /* optVersion */);
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
/**
|
|
704
|
+
* Duplicate the costume at the given index. Add it at that index + 1.
|
|
705
|
+
* @param {!int} costumeIndex Index of costume to duplicate
|
|
706
|
+
* @returns {?Promise} - a promise that resolves when the costume has been decoded and added
|
|
707
|
+
*/
|
|
708
|
+
duplicateCostume (costumeIndex) {
|
|
709
|
+
const originalCostume = this.editingTarget.getCostumes()[costumeIndex];
|
|
710
|
+
const clone = Object.assign({}, originalCostume);
|
|
711
|
+
const md5ext = `${clone.assetId}.${clone.dataFormat}`;
|
|
712
|
+
return loadCostume(md5ext, clone, this.runtime).then(() => {
|
|
713
|
+
this.editingTarget.addCostume(clone, costumeIndex + 1);
|
|
714
|
+
this.editingTarget.setCostume(costumeIndex + 1);
|
|
715
|
+
this.emitTargetsUpdate();
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Duplicate the sound at the given index. Add it at that index + 1.
|
|
721
|
+
* @param {!int} soundIndex Index of sound to duplicate
|
|
722
|
+
* @returns {?Promise} - a promise that resolves when the sound has been decoded and added
|
|
723
|
+
*/
|
|
724
|
+
duplicateSound (soundIndex) {
|
|
725
|
+
const originalSound = this.editingTarget.getSounds()[soundIndex];
|
|
726
|
+
const clone = Object.assign({}, originalSound);
|
|
727
|
+
return loadSound(clone, this.runtime, this.editingTarget.sprite.soundBank).then(() => {
|
|
728
|
+
this.editingTarget.addSound(clone, soundIndex + 1);
|
|
729
|
+
this.emitTargetsUpdate();
|
|
730
|
+
});
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/**
|
|
734
|
+
* Rename a costume on the current editing target.
|
|
735
|
+
* @param {int} costumeIndex - the index of the costume to be renamed.
|
|
736
|
+
* @param {string} newName - the desired new name of the costume (will be modified if already in use).
|
|
737
|
+
*/
|
|
738
|
+
renameCostume (costumeIndex, newName) {
|
|
739
|
+
this.editingTarget.renameCostume(costumeIndex, newName);
|
|
740
|
+
this.emitTargetsUpdate();
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/**
|
|
744
|
+
* Delete a costume from the current editing target.
|
|
745
|
+
* @param {int} costumeIndex - the index of the costume to be removed.
|
|
746
|
+
* @return {?function} A function to restore the deleted costume, or null,
|
|
747
|
+
* if no costume was deleted.
|
|
748
|
+
*/
|
|
749
|
+
deleteCostume (costumeIndex) {
|
|
750
|
+
const deletedCostume = this.editingTarget.deleteCostume(costumeIndex);
|
|
751
|
+
if (deletedCostume) {
|
|
752
|
+
const target = this.editingTarget;
|
|
753
|
+
this.runtime.emitProjectChanged();
|
|
754
|
+
return () => {
|
|
755
|
+
target.addCostume(deletedCostume);
|
|
756
|
+
this.emitTargetsUpdate();
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
return null;
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/**
|
|
763
|
+
* Add a sound to the current editing target.
|
|
764
|
+
* @param {!object} soundObject Object representing the costume.
|
|
765
|
+
* @param {string} optTargetId - the id of the target to add to, if not the editing target.
|
|
766
|
+
* @returns {?Promise} - a promise that resolves when the sound has been decoded and added
|
|
767
|
+
*/
|
|
768
|
+
addSound (soundObject, optTargetId) {
|
|
769
|
+
const target = optTargetId ? this.runtime.getTargetById(optTargetId) :
|
|
770
|
+
this.editingTarget;
|
|
771
|
+
if (target) {
|
|
772
|
+
return loadSound(soundObject, this.runtime, target.sprite.soundBank).then(() => {
|
|
773
|
+
target.addSound(soundObject);
|
|
774
|
+
this.emitTargetsUpdate();
|
|
775
|
+
});
|
|
776
|
+
}
|
|
777
|
+
// If the target cannot be found by id, return a rejected promise
|
|
778
|
+
return new Promise.reject();
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
/**
|
|
782
|
+
* Rename a sound on the current editing target.
|
|
783
|
+
* @param {int} soundIndex - the index of the sound to be renamed.
|
|
784
|
+
* @param {string} newName - the desired new name of the sound (will be modified if already in use).
|
|
785
|
+
*/
|
|
786
|
+
renameSound (soundIndex, newName) {
|
|
787
|
+
this.editingTarget.renameSound(soundIndex, newName);
|
|
788
|
+
this.emitTargetsUpdate();
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
/**
|
|
792
|
+
* Get a sound buffer from the audio engine.
|
|
793
|
+
* @param {int} soundIndex - the index of the sound to be got.
|
|
794
|
+
* @return {AudioBuffer} the sound's audio buffer.
|
|
795
|
+
*/
|
|
796
|
+
getSoundBuffer (soundIndex) {
|
|
797
|
+
const id = this.editingTarget.sprite.sounds[soundIndex].soundId;
|
|
798
|
+
if (id && this.runtime && this.runtime.audioEngine) {
|
|
799
|
+
return this.editingTarget.sprite.soundBank.getSoundPlayer(id).buffer;
|
|
800
|
+
}
|
|
801
|
+
return null;
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
/**
|
|
805
|
+
* Update a sound buffer.
|
|
806
|
+
* @param {int} soundIndex - the index of the sound to be updated.
|
|
807
|
+
* @param {AudioBuffer} newBuffer - new audio buffer for the audio engine.
|
|
808
|
+
* @param {ArrayBuffer} soundEncoding - the new (wav) encoded sound to be stored
|
|
809
|
+
*/
|
|
810
|
+
updateSoundBuffer (soundIndex, newBuffer, soundEncoding) {
|
|
811
|
+
const sound = this.editingTarget.sprite.sounds[soundIndex];
|
|
812
|
+
if (sound && sound.broken) delete sound.broken;
|
|
813
|
+
const id = sound ? sound.soundId : null;
|
|
814
|
+
if (id && this.runtime && this.runtime.audioEngine) {
|
|
815
|
+
this.editingTarget.sprite.soundBank.getSoundPlayer(id).buffer = newBuffer;
|
|
816
|
+
}
|
|
817
|
+
// Update sound in runtime
|
|
818
|
+
if (soundEncoding) {
|
|
819
|
+
// Now that we updated the sound, the format should also be updated
|
|
820
|
+
// so that the sound can eventually be decoded the right way.
|
|
821
|
+
// Sounds that were formerly 'adpcm', but were updated in sound editor
|
|
822
|
+
// will not get decoded by the audio engine correctly unless the format
|
|
823
|
+
// is updated as below.
|
|
824
|
+
sound.format = '';
|
|
825
|
+
const storage = this.runtime.storage;
|
|
826
|
+
sound.asset = storage.createAsset(
|
|
827
|
+
storage.AssetType.Sound,
|
|
828
|
+
storage.DataFormat.WAV,
|
|
829
|
+
soundEncoding,
|
|
830
|
+
null,
|
|
831
|
+
true // generate md5
|
|
832
|
+
);
|
|
833
|
+
sound.assetId = sound.asset.assetId;
|
|
834
|
+
sound.dataFormat = storage.DataFormat.WAV;
|
|
835
|
+
sound.md5 = `${sound.assetId}.${sound.dataFormat}`;
|
|
836
|
+
sound.sampleCount = newBuffer.length;
|
|
837
|
+
sound.rate = newBuffer.sampleRate;
|
|
838
|
+
}
|
|
839
|
+
// If soundEncoding is null, it's because gui had a problem
|
|
840
|
+
// encoding the updated sound. We don't want to store anything in this
|
|
841
|
+
// case, and gui should have logged an error.
|
|
842
|
+
|
|
843
|
+
this.emitTargetsUpdate();
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
/**
|
|
847
|
+
* Delete a sound from the current editing target.
|
|
848
|
+
* @param {int} soundIndex - the index of the sound to be removed.
|
|
849
|
+
* @return {?Function} A function to restore the sound that was deleted,
|
|
850
|
+
* or null, if no sound was deleted.
|
|
851
|
+
*/
|
|
852
|
+
deleteSound (soundIndex) {
|
|
853
|
+
const target = this.editingTarget;
|
|
854
|
+
const deletedSound = this.editingTarget.deleteSound(soundIndex);
|
|
855
|
+
if (deletedSound) {
|
|
856
|
+
this.runtime.emitProjectChanged();
|
|
857
|
+
const restoreFun = () => {
|
|
858
|
+
target.addSound(deletedSound);
|
|
859
|
+
this.emitTargetsUpdate();
|
|
860
|
+
};
|
|
861
|
+
return restoreFun;
|
|
862
|
+
}
|
|
863
|
+
return null;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
/**
|
|
867
|
+
* Get a string representation of the image from storage.
|
|
868
|
+
* @param {int} costumeIndex - the index of the costume to be got.
|
|
869
|
+
* @return {string} the costume's SVG string if it's SVG,
|
|
870
|
+
* a dataURI if it's a PNG or JPG, or null if it couldn't be found or decoded.
|
|
871
|
+
*/
|
|
872
|
+
getCostume (costumeIndex) {
|
|
873
|
+
const asset = this.editingTarget.getCostumes()[costumeIndex].asset;
|
|
874
|
+
if (!asset || !this.runtime || !this.runtime.storage) return null;
|
|
875
|
+
const format = asset.dataFormat;
|
|
876
|
+
if (format === this.runtime.storage.DataFormat.SVG) {
|
|
877
|
+
return asset.decodeText();
|
|
878
|
+
} else if (format === this.runtime.storage.DataFormat.PNG ||
|
|
879
|
+
format === this.runtime.storage.DataFormat.JPG) {
|
|
880
|
+
return asset.encodeDataURI();
|
|
881
|
+
}
|
|
882
|
+
log.error(`Unhandled format: ${asset.dataFormat}`);
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
/**
|
|
887
|
+
* Update a costume with the given bitmap
|
|
888
|
+
* @param {!int} costumeIndex - the index of the costume to be updated.
|
|
889
|
+
* @param {!ImageData} bitmap - new bitmap for the renderer.
|
|
890
|
+
* @param {!number} rotationCenterX x of point about which the costume rotates, relative to its upper left corner
|
|
891
|
+
* @param {!number} rotationCenterY y of point about which the costume rotates, relative to its upper left corner
|
|
892
|
+
* @param {!number} bitmapResolution 1 for bitmaps that have 1 pixel per unit of stage,
|
|
893
|
+
* 2 for double-resolution bitmaps
|
|
894
|
+
*/
|
|
895
|
+
updateBitmap (costumeIndex, bitmap, rotationCenterX, rotationCenterY, bitmapResolution) {
|
|
896
|
+
const costume = this.editingTarget.getCostumes()[costumeIndex];
|
|
897
|
+
if (!(costume && this.runtime && this.runtime.renderer)) return;
|
|
898
|
+
if (costume && costume.broken) delete costume.broken;
|
|
899
|
+
|
|
900
|
+
costume.rotationCenterX = rotationCenterX;
|
|
901
|
+
costume.rotationCenterY = rotationCenterY;
|
|
902
|
+
|
|
903
|
+
// If the bitmap originally had a zero width or height, use that value
|
|
904
|
+
const bitmapWidth = bitmap.sourceWidth === 0 ? 0 : bitmap.width;
|
|
905
|
+
const bitmapHeight = bitmap.sourceHeight === 0 ? 0 : bitmap.height;
|
|
906
|
+
// @todo: updateBitmapSkin does not take ImageData
|
|
907
|
+
const canvas = document.createElement('canvas');
|
|
908
|
+
canvas.width = bitmapWidth;
|
|
909
|
+
canvas.height = bitmapHeight;
|
|
910
|
+
const context = canvas.getContext('2d');
|
|
911
|
+
context.putImageData(bitmap, 0, 0);
|
|
912
|
+
|
|
913
|
+
// Divide by resolution because the renderer's definition of the rotation center
|
|
914
|
+
// is the rotation center divided by the bitmap resolution
|
|
915
|
+
this.runtime.renderer.updateBitmapSkin(
|
|
916
|
+
costume.skinId,
|
|
917
|
+
canvas,
|
|
918
|
+
bitmapResolution,
|
|
919
|
+
[rotationCenterX / bitmapResolution, rotationCenterY / bitmapResolution]
|
|
920
|
+
);
|
|
921
|
+
|
|
922
|
+
// @todo there should be a better way to get from ImageData to a decodable storage format
|
|
923
|
+
canvas.toBlob(blob => {
|
|
924
|
+
const reader = new FileReader();
|
|
925
|
+
reader.addEventListener('loadend', () => {
|
|
926
|
+
const storage = this.runtime.storage;
|
|
927
|
+
costume.dataFormat = storage.DataFormat.PNG;
|
|
928
|
+
costume.bitmapResolution = bitmapResolution;
|
|
929
|
+
costume.size = [bitmapWidth, bitmapHeight];
|
|
930
|
+
costume.asset = storage.createAsset(
|
|
931
|
+
storage.AssetType.ImageBitmap,
|
|
932
|
+
costume.dataFormat,
|
|
933
|
+
Buffer.from(reader.result),
|
|
934
|
+
null, // id
|
|
935
|
+
true // generate md5
|
|
936
|
+
);
|
|
937
|
+
costume.assetId = costume.asset.assetId;
|
|
938
|
+
costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
|
|
939
|
+
this.emitTargetsUpdate();
|
|
940
|
+
});
|
|
941
|
+
// Bitmaps with a zero width or height return null for their blob
|
|
942
|
+
if (blob){
|
|
943
|
+
reader.readAsArrayBuffer(blob);
|
|
944
|
+
}
|
|
945
|
+
});
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Update a costume with the given SVG
|
|
950
|
+
* @param {int} costumeIndex - the index of the costume to be updated.
|
|
951
|
+
* @param {string} svg - new SVG for the renderer.
|
|
952
|
+
* @param {number} rotationCenterX x of point about which the costume rotates, relative to its upper left corner
|
|
953
|
+
* @param {number} rotationCenterY y of point about which the costume rotates, relative to its upper left corner
|
|
954
|
+
*/
|
|
955
|
+
updateSvg (costumeIndex, svg, rotationCenterX, rotationCenterY) {
|
|
956
|
+
const costume = this.editingTarget.getCostumes()[costumeIndex];
|
|
957
|
+
if (costume && costume.broken) delete costume.broken;
|
|
958
|
+
if (costume && this.runtime && this.runtime.renderer) {
|
|
959
|
+
costume.rotationCenterX = rotationCenterX;
|
|
960
|
+
costume.rotationCenterY = rotationCenterY;
|
|
961
|
+
this.runtime.renderer.updateSVGSkin(costume.skinId, svg, [rotationCenterX, rotationCenterY]);
|
|
962
|
+
costume.size = this.runtime.renderer.getSkinSize(costume.skinId);
|
|
963
|
+
}
|
|
964
|
+
const storage = this.runtime.storage;
|
|
965
|
+
// If we're in here, we've edited an svg in the vector editor,
|
|
966
|
+
// so the dataFormat should be 'svg'
|
|
967
|
+
costume.dataFormat = storage.DataFormat.SVG;
|
|
968
|
+
costume.bitmapResolution = 1;
|
|
969
|
+
costume.asset = storage.createAsset(
|
|
970
|
+
storage.AssetType.ImageVector,
|
|
971
|
+
costume.dataFormat,
|
|
972
|
+
(new _TextEncoder()).encode(svg),
|
|
973
|
+
null,
|
|
974
|
+
true // generate md5
|
|
975
|
+
);
|
|
976
|
+
costume.assetId = costume.asset.assetId;
|
|
977
|
+
costume.md5 = `${costume.assetId}.${costume.dataFormat}`;
|
|
978
|
+
this.emitTargetsUpdate();
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Add a backdrop to the stage.
|
|
983
|
+
* @param {string} md5ext - the MD5 and extension of the backdrop to be loaded.
|
|
984
|
+
* @param {!object} backdropObject Object representing the backdrop.
|
|
985
|
+
* @property {int} skinId - the ID of the backdrop's render skin, once installed.
|
|
986
|
+
* @property {number} rotationCenterX - the X component of the backdrop's origin.
|
|
987
|
+
* @property {number} rotationCenterY - the Y component of the backdrop's origin.
|
|
988
|
+
* @property {number} [bitmapResolution] - the resolution scale for a bitmap backdrop.
|
|
989
|
+
* @returns {?Promise} - a promise that resolves when the backdrop has been added
|
|
990
|
+
*/
|
|
991
|
+
addBackdrop (md5ext, backdropObject) {
|
|
992
|
+
return loadCostume(md5ext, backdropObject, this.runtime).then(() => {
|
|
993
|
+
const stage = this.runtime.getTargetForStage();
|
|
994
|
+
stage.addCostume(backdropObject);
|
|
995
|
+
stage.setCostume(stage.getCostumes().length - 1);
|
|
996
|
+
this.runtime.emitProjectChanged();
|
|
997
|
+
});
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Rename a sprite.
|
|
1002
|
+
* @param {string} targetId ID of a target whose sprite to rename.
|
|
1003
|
+
* @param {string} newName New name of the sprite.
|
|
1004
|
+
*/
|
|
1005
|
+
renameSprite (targetId, newName) {
|
|
1006
|
+
const target = this.runtime.getTargetById(targetId);
|
|
1007
|
+
if (target) {
|
|
1008
|
+
if (!target.isSprite()) {
|
|
1009
|
+
throw new Error('Cannot rename non-sprite targets.');
|
|
1010
|
+
}
|
|
1011
|
+
const sprite = target.sprite;
|
|
1012
|
+
if (!sprite) {
|
|
1013
|
+
throw new Error('No sprite associated with this target.');
|
|
1014
|
+
}
|
|
1015
|
+
if (newName && RESERVED_NAMES.indexOf(newName) === -1) {
|
|
1016
|
+
const names = this.runtime.targets
|
|
1017
|
+
.filter(runtimeTarget => runtimeTarget.isSprite() && runtimeTarget.id !== target.id)
|
|
1018
|
+
.map(runtimeTarget => runtimeTarget.sprite.name);
|
|
1019
|
+
const oldName = sprite.name;
|
|
1020
|
+
const newUnusedName = StringUtil.unusedName(newName, names);
|
|
1021
|
+
sprite.name = newUnusedName;
|
|
1022
|
+
const allTargets = this.runtime.targets;
|
|
1023
|
+
for (let i = 0; i < allTargets.length; i++) {
|
|
1024
|
+
const currTarget = allTargets[i];
|
|
1025
|
+
currTarget.blocks.updateAssetName(oldName, newName, 'sprite');
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
if (newUnusedName !== oldName) this.emitTargetsUpdate();
|
|
1029
|
+
}
|
|
1030
|
+
} else {
|
|
1031
|
+
throw new Error('No target with the provided id.');
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
/**
|
|
1036
|
+
* Delete a sprite and all its clones.
|
|
1037
|
+
* @param {string} targetId ID of a target whose sprite to delete.
|
|
1038
|
+
* @return {Function} Returns a function to restore the sprite that was deleted
|
|
1039
|
+
*/
|
|
1040
|
+
deleteSprite (targetId) {
|
|
1041
|
+
const target = this.runtime.getTargetById(targetId);
|
|
1042
|
+
|
|
1043
|
+
if (target) {
|
|
1044
|
+
const targetIndexBeforeDelete = this.runtime.targets.map(t => t.id).indexOf(target.id);
|
|
1045
|
+
if (!target.isSprite()) {
|
|
1046
|
+
throw new Error('Cannot delete non-sprite targets.');
|
|
1047
|
+
}
|
|
1048
|
+
const sprite = target.sprite;
|
|
1049
|
+
if (!sprite) {
|
|
1050
|
+
throw new Error('No sprite associated with this target.');
|
|
1051
|
+
}
|
|
1052
|
+
const spritePromise = this.exportSprite(targetId, 'uint8array');
|
|
1053
|
+
const restoreSprite = () => spritePromise.then(spriteBuffer => this.addSprite(spriteBuffer));
|
|
1054
|
+
// Remove monitors from the runtime state and remove the
|
|
1055
|
+
// target-specific monitored blocks (e.g. local variables)
|
|
1056
|
+
target.deleteMonitors();
|
|
1057
|
+
const currentEditingTarget = this.editingTarget;
|
|
1058
|
+
for (let i = 0; i < sprite.clones.length; i++) {
|
|
1059
|
+
const clone = sprite.clones[i];
|
|
1060
|
+
this.runtime.stopForTarget(sprite.clones[i]);
|
|
1061
|
+
this.runtime.disposeTarget(sprite.clones[i]);
|
|
1062
|
+
// Ensure editing target is switched if we are deleting it.
|
|
1063
|
+
if (clone === currentEditingTarget) {
|
|
1064
|
+
const nextTargetIndex = Math.min(this.runtime.targets.length - 1, targetIndexBeforeDelete);
|
|
1065
|
+
if (this.runtime.targets.length > 0){
|
|
1066
|
+
this.setEditingTarget(this.runtime.targets[nextTargetIndex].id);
|
|
1067
|
+
} else {
|
|
1068
|
+
this.editingTarget = null;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
// Sprite object should be deleted by GC.
|
|
1073
|
+
this.emitTargetsUpdate();
|
|
1074
|
+
return restoreSprite;
|
|
1075
|
+
}
|
|
1076
|
+
|
|
1077
|
+
throw new Error('No target with the provided id.');
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
/**
|
|
1081
|
+
* Duplicate a sprite.
|
|
1082
|
+
* @param {string} targetId ID of a target whose sprite to duplicate.
|
|
1083
|
+
* @returns {Promise} Promise that resolves when duplicated target has
|
|
1084
|
+
* been added to the runtime.
|
|
1085
|
+
*/
|
|
1086
|
+
duplicateSprite (targetId) {
|
|
1087
|
+
const target = this.runtime.getTargetById(targetId);
|
|
1088
|
+
if (!target) {
|
|
1089
|
+
throw new Error('No target with the provided id.');
|
|
1090
|
+
} else if (!target.isSprite()) {
|
|
1091
|
+
throw new Error('Cannot duplicate non-sprite targets.');
|
|
1092
|
+
} else if (!target.sprite) {
|
|
1093
|
+
throw new Error('No sprite associated with this target.');
|
|
1094
|
+
}
|
|
1095
|
+
return target.duplicate().then(newTarget => {
|
|
1096
|
+
this.runtime.addTarget(newTarget);
|
|
1097
|
+
newTarget.goBehindOther(target);
|
|
1098
|
+
this.setEditingTarget(newTarget.id);
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
/**
|
|
1103
|
+
* Set the audio engine for the VM/runtime
|
|
1104
|
+
* @param {!AudioEngine} audioEngine The audio engine to attach
|
|
1105
|
+
*/
|
|
1106
|
+
attachAudioEngine (audioEngine) {
|
|
1107
|
+
this.runtime.attachAudioEngine(audioEngine);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
/**
|
|
1111
|
+
* Set the renderer for the VM/runtime
|
|
1112
|
+
* @param {!RenderWebGL} renderer The renderer to attach
|
|
1113
|
+
*/
|
|
1114
|
+
attachRenderer (renderer) {
|
|
1115
|
+
this.runtime.attachRenderer(renderer);
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
/**
|
|
1119
|
+
* @returns {RenderWebGL} The renderer attached to the vm
|
|
1120
|
+
*/
|
|
1121
|
+
get renderer () {
|
|
1122
|
+
return this.runtime && this.runtime.renderer;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
// @deprecated
|
|
1126
|
+
attachV2SVGAdapter () {
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
/**
|
|
1130
|
+
* Set the bitmap adapter for the VM/runtime, which converts scratch 2
|
|
1131
|
+
* bitmaps to scratch 3 bitmaps. (Scratch 3 bitmaps are all bitmap resolution 2)
|
|
1132
|
+
* @param {!function} bitmapAdapter The adapter to attach
|
|
1133
|
+
*/
|
|
1134
|
+
attachV2BitmapAdapter (bitmapAdapter) {
|
|
1135
|
+
this.runtime.attachV2BitmapAdapter(bitmapAdapter);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
/**
|
|
1139
|
+
* Set the storage module for the VM/runtime
|
|
1140
|
+
* @param {!ScratchStorage} storage The storage module to attach
|
|
1141
|
+
*/
|
|
1142
|
+
attachStorage (storage) {
|
|
1143
|
+
this.runtime.attachStorage(storage);
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* set the current locale and builtin messages for the VM
|
|
1148
|
+
* @param {!string} locale current locale
|
|
1149
|
+
* @param {!object} messages builtin messages map for current locale
|
|
1150
|
+
* @returns {Promise} Promise that resolves when all the blocks have been
|
|
1151
|
+
* updated for a new locale (or empty if locale hasn't changed.)
|
|
1152
|
+
*/
|
|
1153
|
+
setLocale (locale, messages) {
|
|
1154
|
+
if (locale !== formatMessage.setup().locale) {
|
|
1155
|
+
formatMessage.setup({locale: locale, translations: {[locale]: messages}});
|
|
1156
|
+
}
|
|
1157
|
+
return this.extensionManager.refreshBlocks();
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
/**
|
|
1161
|
+
* get the current locale for the VM
|
|
1162
|
+
* @returns {string} the current locale in the VM
|
|
1163
|
+
*/
|
|
1164
|
+
getLocale () {
|
|
1165
|
+
return formatMessage.setup().locale;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
/**
|
|
1169
|
+
* Handle a Blockly event for the current editing target.
|
|
1170
|
+
* @param {!Blockly.Event} e Any Blockly event.
|
|
1171
|
+
*/
|
|
1172
|
+
blockListener (e) {
|
|
1173
|
+
if (this.editingTarget) {
|
|
1174
|
+
this.editingTarget.blocks.blocklyListen(e);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
|
|
1178
|
+
/**
|
|
1179
|
+
* Handle a Blockly event for the flyout.
|
|
1180
|
+
* @param {!Blockly.Event} e Any Blockly event.
|
|
1181
|
+
*/
|
|
1182
|
+
flyoutBlockListener (e) {
|
|
1183
|
+
this.runtime.flyoutBlocks.blocklyListen(e);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
/**
|
|
1187
|
+
* Handle a Blockly event for the flyout to be passed to the monitor container.
|
|
1188
|
+
* @param {!Blockly.Event} e Any Blockly event.
|
|
1189
|
+
*/
|
|
1190
|
+
monitorBlockListener (e) {
|
|
1191
|
+
// Filter events by type, since monitor blocks only need to listen to these events.
|
|
1192
|
+
// Monitor blocks shouldn't be destroyed when flyout blocks are deleted.
|
|
1193
|
+
if (['create', 'change'].indexOf(e.type) !== -1) {
|
|
1194
|
+
this.runtime.monitorBlocks.blocklyListen(e);
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
/**
|
|
1199
|
+
* Handle a Blockly event for the variable map.
|
|
1200
|
+
* @param {!Blockly.Event} e Any Blockly event.
|
|
1201
|
+
*/
|
|
1202
|
+
variableListener (e) {
|
|
1203
|
+
// Filter events by type, since blocks only needs to listen to these
|
|
1204
|
+
// var events.
|
|
1205
|
+
if (['var_create', 'var_rename', 'var_delete'].indexOf(e.type) !== -1) {
|
|
1206
|
+
this.runtime.getTargetForStage().blocks.blocklyListen(e);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1210
|
+
/**
|
|
1211
|
+
* Delete all of the flyout blocks.
|
|
1212
|
+
*/
|
|
1213
|
+
clearFlyoutBlocks () {
|
|
1214
|
+
this.runtime.flyoutBlocks.deleteAllBlocks();
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
/**
|
|
1218
|
+
* Set an editing target. An editor UI can use this function to switch
|
|
1219
|
+
* between editing different targets, sprites, etc.
|
|
1220
|
+
* After switching the editing target, the VM may emit updates
|
|
1221
|
+
* to the list of targets and any attached workspace blocks
|
|
1222
|
+
* (see `emitTargetsUpdate` and `emitWorkspaceUpdate`).
|
|
1223
|
+
* @param {string} targetId Id of target to set as editing.
|
|
1224
|
+
*/
|
|
1225
|
+
setEditingTarget (targetId) {
|
|
1226
|
+
// Has the target id changed? If not, exit.
|
|
1227
|
+
if (this.editingTarget && targetId === this.editingTarget.id) {
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
const target = this.runtime.getTargetById(targetId);
|
|
1231
|
+
if (target) {
|
|
1232
|
+
this.editingTarget = target;
|
|
1233
|
+
// Emit appropriate UI updates.
|
|
1234
|
+
this.emitTargetsUpdate(false /* Don't emit project change */);
|
|
1235
|
+
this.emitWorkspaceUpdate();
|
|
1236
|
+
this.runtime.setEditingTarget(target);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
|
|
1240
|
+
/**
|
|
1241
|
+
* Called when blocks are dragged from one sprite to another. Adds the blocks to the
|
|
1242
|
+
* workspace of the given target.
|
|
1243
|
+
* @param {!Array<object>} blocks Blocks to add.
|
|
1244
|
+
* @param {!string} targetId Id of target to add blocks to.
|
|
1245
|
+
* @param {?string} optFromTargetId Optional target id indicating that blocks are being
|
|
1246
|
+
* shared from that target. This is needed for resolving any potential variable conflicts.
|
|
1247
|
+
* @return {!Promise} Promise that resolves when the extensions and blocks have been added.
|
|
1248
|
+
*/
|
|
1249
|
+
shareBlocksToTarget (blocks, targetId, optFromTargetId) {
|
|
1250
|
+
const sb3 = require('./serialization/sb3');
|
|
1251
|
+
|
|
1252
|
+
const copiedBlocks = JSON.parse(JSON.stringify(blocks));
|
|
1253
|
+
newBlockIds(copiedBlocks);
|
|
1254
|
+
const target = this.runtime.getTargetById(targetId);
|
|
1255
|
+
|
|
1256
|
+
if (optFromTargetId) {
|
|
1257
|
+
// If the blocks are being shared from another target,
|
|
1258
|
+
// resolve any possible variable conflicts that may arise.
|
|
1259
|
+
const fromTarget = this.runtime.getTargetById(optFromTargetId);
|
|
1260
|
+
fromTarget.resolveVariableSharingConflictsWithTarget(copiedBlocks, target);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
// Create a unique set of extensionIds that are not yet loaded
|
|
1264
|
+
const extensionIDs = new Set(copiedBlocks
|
|
1265
|
+
.map(b => sb3.getExtensionIdForOpcode(b.opcode))
|
|
1266
|
+
.filter(id => !!id) // Remove ids that do not exist
|
|
1267
|
+
.filter(id => !this.extensionManager.isExtensionLoaded(id)) // and remove loaded extensions
|
|
1268
|
+
);
|
|
1269
|
+
|
|
1270
|
+
// Create an array promises for extensions to load
|
|
1271
|
+
const extensionPromises = Array.from(extensionIDs,
|
|
1272
|
+
id => this.extensionManager.loadExtensionURL(id)
|
|
1273
|
+
);
|
|
1274
|
+
|
|
1275
|
+
return Promise.all(extensionPromises).then(() => {
|
|
1276
|
+
copiedBlocks.forEach(block => {
|
|
1277
|
+
target.blocks.createBlock(block);
|
|
1278
|
+
});
|
|
1279
|
+
target.blocks.updateTargetSpecificBlocks(target.isStage);
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
/**
|
|
1284
|
+
* Called when costumes are dragged from editing target to another target.
|
|
1285
|
+
* Sets the newly added costume as the current costume.
|
|
1286
|
+
* @param {!number} costumeIndex Index of the costume of the editing target to share.
|
|
1287
|
+
* @param {!string} targetId Id of target to add the costume.
|
|
1288
|
+
* @return {Promise} Promise that resolves when the new costume has been loaded.
|
|
1289
|
+
*/
|
|
1290
|
+
shareCostumeToTarget (costumeIndex, targetId) {
|
|
1291
|
+
const originalCostume = this.editingTarget.getCostumes()[costumeIndex];
|
|
1292
|
+
const clone = Object.assign({}, originalCostume);
|
|
1293
|
+
const md5ext = `${clone.assetId}.${clone.dataFormat}`;
|
|
1294
|
+
return loadCostume(md5ext, clone, this.runtime).then(() => {
|
|
1295
|
+
const target = this.runtime.getTargetById(targetId);
|
|
1296
|
+
if (target) {
|
|
1297
|
+
target.addCostume(clone);
|
|
1298
|
+
target.setCostume(
|
|
1299
|
+
target.getCostumes().length - 1
|
|
1300
|
+
);
|
|
1301
|
+
}
|
|
1302
|
+
});
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
/**
|
|
1306
|
+
* Called when sounds are dragged from editing target to another target.
|
|
1307
|
+
* @param {!number} soundIndex Index of the sound of the editing target to share.
|
|
1308
|
+
* @param {!string} targetId Id of target to add the sound.
|
|
1309
|
+
* @return {Promise} Promise that resolves when the new sound has been loaded.
|
|
1310
|
+
*/
|
|
1311
|
+
shareSoundToTarget (soundIndex, targetId) {
|
|
1312
|
+
const originalSound = this.editingTarget.getSounds()[soundIndex];
|
|
1313
|
+
const clone = Object.assign({}, originalSound);
|
|
1314
|
+
const target = this.runtime.getTargetById(targetId);
|
|
1315
|
+
return loadSound(clone, this.runtime, target.sprite.soundBank).then(() => {
|
|
1316
|
+
if (target) {
|
|
1317
|
+
target.addSound(clone);
|
|
1318
|
+
this.emitTargetsUpdate();
|
|
1319
|
+
}
|
|
1320
|
+
});
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1323
|
+
/**
|
|
1324
|
+
* Repopulate the workspace with the blocks of the current editingTarget. This
|
|
1325
|
+
* allows us to get around bugs like gui#413.
|
|
1326
|
+
*/
|
|
1327
|
+
refreshWorkspace () {
|
|
1328
|
+
if (this.editingTarget) {
|
|
1329
|
+
this.emitWorkspaceUpdate();
|
|
1330
|
+
this.runtime.setEditingTarget(this.editingTarget);
|
|
1331
|
+
this.emitTargetsUpdate(false /* Don't emit project change */);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
/**
|
|
1336
|
+
* Emit metadata about available targets.
|
|
1337
|
+
* An editor UI could use this to display a list of targets and show
|
|
1338
|
+
* the currently editing one.
|
|
1339
|
+
* @param {bool} triggerProjectChange If true, also emit a project changed event.
|
|
1340
|
+
* Disabled selectively by updates that don't affect project serialization.
|
|
1341
|
+
* Defaults to true.
|
|
1342
|
+
*/
|
|
1343
|
+
emitTargetsUpdate (triggerProjectChange) {
|
|
1344
|
+
if (typeof triggerProjectChange === 'undefined') triggerProjectChange = true;
|
|
1345
|
+
this.emit('targetsUpdate', {
|
|
1346
|
+
// [[target id, human readable target name], ...].
|
|
1347
|
+
targetList: this.runtime.targets
|
|
1348
|
+
.filter(
|
|
1349
|
+
// Don't report clones.
|
|
1350
|
+
target => !Object.prototype.hasOwnProperty.call(target, 'isOriginal') || target.isOriginal
|
|
1351
|
+
).map(
|
|
1352
|
+
target => target.toJSON()
|
|
1353
|
+
),
|
|
1354
|
+
// Currently editing target id.
|
|
1355
|
+
editingTarget: this.editingTarget ? this.editingTarget.id : null
|
|
1356
|
+
});
|
|
1357
|
+
if (triggerProjectChange) {
|
|
1358
|
+
this.runtime.emitProjectChanged();
|
|
1359
|
+
}
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
/**
|
|
1363
|
+
* Emit an Blockly/scratch-blocks compatible XML representation
|
|
1364
|
+
* of the current editing target's blocks.
|
|
1365
|
+
*/
|
|
1366
|
+
emitWorkspaceUpdate () {
|
|
1367
|
+
// Create a list of broadcast message Ids according to the stage variables
|
|
1368
|
+
const stageVariables = this.runtime.getTargetForStage().variables;
|
|
1369
|
+
let messageIds = [];
|
|
1370
|
+
for (const varId in stageVariables) {
|
|
1371
|
+
if (stageVariables[varId].type === Variable.BROADCAST_MESSAGE_TYPE) {
|
|
1372
|
+
messageIds.push(varId);
|
|
1373
|
+
}
|
|
1374
|
+
}
|
|
1375
|
+
// Go through all blocks on all targets, removing referenced
|
|
1376
|
+
// broadcast ids from the list.
|
|
1377
|
+
for (let i = 0; i < this.runtime.targets.length; i++) {
|
|
1378
|
+
const currTarget = this.runtime.targets[i];
|
|
1379
|
+
const currBlocks = currTarget.blocks._blocks;
|
|
1380
|
+
for (const blockId in currBlocks) {
|
|
1381
|
+
if (currBlocks[blockId].fields.BROADCAST_OPTION) {
|
|
1382
|
+
const id = currBlocks[blockId].fields.BROADCAST_OPTION.id;
|
|
1383
|
+
const index = messageIds.indexOf(id);
|
|
1384
|
+
if (index !== -1) {
|
|
1385
|
+
messageIds = messageIds.slice(0, index)
|
|
1386
|
+
.concat(messageIds.slice(index + 1));
|
|
1387
|
+
}
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
// Anything left in messageIds is not referenced by a block, so delete it.
|
|
1392
|
+
for (let i = 0; i < messageIds.length; i++) {
|
|
1393
|
+
const id = messageIds[i];
|
|
1394
|
+
delete this.runtime.getTargetForStage().variables[id];
|
|
1395
|
+
}
|
|
1396
|
+
const globalVarMap = Object.assign({}, this.runtime.getTargetForStage().variables);
|
|
1397
|
+
const localVarMap = this.editingTarget.isStage ?
|
|
1398
|
+
Object.create(null) :
|
|
1399
|
+
Object.assign({}, this.editingTarget.variables);
|
|
1400
|
+
|
|
1401
|
+
const globalVariables = Object.keys(globalVarMap).map(k => globalVarMap[k]);
|
|
1402
|
+
const localVariables = Object.keys(localVarMap).map(k => localVarMap[k]);
|
|
1403
|
+
const workspaceComments = Object.keys(this.editingTarget.comments)
|
|
1404
|
+
.map(k => this.editingTarget.comments[k])
|
|
1405
|
+
.filter(c => c.blockId === null);
|
|
1406
|
+
|
|
1407
|
+
const xmlString = `<xml xmlns="http://www.w3.org/1999/xhtml">
|
|
1408
|
+
<variables>
|
|
1409
|
+
${globalVariables.map(v => v.toXML()).join()}
|
|
1410
|
+
${localVariables.map(v => v.toXML(true)).join()}
|
|
1411
|
+
</variables>
|
|
1412
|
+
${workspaceComments.map(c => c.toXML()).join()}
|
|
1413
|
+
${this.editingTarget.blocks.toXML(this.editingTarget.comments)}
|
|
1414
|
+
</xml>`;
|
|
1415
|
+
|
|
1416
|
+
this.emit('workspaceUpdate', {xml: xmlString});
|
|
1417
|
+
}
|
|
1418
|
+
|
|
1419
|
+
/**
|
|
1420
|
+
* Get a target id for a drawable id. Useful for interacting with the renderer
|
|
1421
|
+
* @param {int} drawableId The drawable id to request the target id for
|
|
1422
|
+
* @returns {?string} The target id, if found. Will also be null if the target found is the stage.
|
|
1423
|
+
*/
|
|
1424
|
+
getTargetIdForDrawableId (drawableId) {
|
|
1425
|
+
const target = this.runtime.getTargetByDrawableId(drawableId);
|
|
1426
|
+
if (target &&
|
|
1427
|
+
Object.prototype.hasOwnProperty.call(target, 'id') &&
|
|
1428
|
+
Object.prototype.hasOwnProperty.call(target, 'isStage') &&
|
|
1429
|
+
!target.isStage) {
|
|
1430
|
+
return target.id;
|
|
1431
|
+
}
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
/**
|
|
1436
|
+
* Reorder target by index. Return whether a change was made.
|
|
1437
|
+
* @param {!string} targetIndex Index of the target.
|
|
1438
|
+
* @param {!number} newIndex index that the target should be moved to.
|
|
1439
|
+
* @returns {boolean} Whether a target was reordered.
|
|
1440
|
+
*/
|
|
1441
|
+
reorderTarget (targetIndex, newIndex) {
|
|
1442
|
+
let targets = this.runtime.targets;
|
|
1443
|
+
targetIndex = MathUtil.clamp(targetIndex, 0, targets.length - 1);
|
|
1444
|
+
newIndex = MathUtil.clamp(newIndex, 0, targets.length - 1);
|
|
1445
|
+
if (targetIndex === newIndex) return false;
|
|
1446
|
+
const target = targets[targetIndex];
|
|
1447
|
+
targets = targets.slice(0, targetIndex).concat(targets.slice(targetIndex + 1));
|
|
1448
|
+
targets.splice(newIndex, 0, target);
|
|
1449
|
+
this.runtime.targets = targets;
|
|
1450
|
+
this.emitTargetsUpdate();
|
|
1451
|
+
return true;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
/**
|
|
1455
|
+
* Reorder the costumes of a target if it exists. Return whether it succeeded.
|
|
1456
|
+
* @param {!string} targetId ID of the target which owns the costumes.
|
|
1457
|
+
* @param {!number} costumeIndex index of the costume to move.
|
|
1458
|
+
* @param {!number} newIndex index that the costume should be moved to.
|
|
1459
|
+
* @returns {boolean} Whether a costume was reordered.
|
|
1460
|
+
*/
|
|
1461
|
+
reorderCostume (targetId, costumeIndex, newIndex) {
|
|
1462
|
+
const target = this.runtime.getTargetById(targetId);
|
|
1463
|
+
if (target) {
|
|
1464
|
+
const reorderSuccessful = target.reorderCostume(costumeIndex, newIndex);
|
|
1465
|
+
if (reorderSuccessful) {
|
|
1466
|
+
this.runtime.emitProjectChanged();
|
|
1467
|
+
}
|
|
1468
|
+
return reorderSuccessful;
|
|
1469
|
+
}
|
|
1470
|
+
return false;
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
/**
|
|
1474
|
+
* Reorder the sounds of a target if it exists. Return whether it occured.
|
|
1475
|
+
* @param {!string} targetId ID of the target which owns the sounds.
|
|
1476
|
+
* @param {!number} soundIndex index of the sound to move.
|
|
1477
|
+
* @param {!number} newIndex index that the sound should be moved to.
|
|
1478
|
+
* @returns {boolean} Whether a sound was reordered.
|
|
1479
|
+
*/
|
|
1480
|
+
reorderSound (targetId, soundIndex, newIndex) {
|
|
1481
|
+
const target = this.runtime.getTargetById(targetId);
|
|
1482
|
+
if (target) {
|
|
1483
|
+
const reorderSuccessful = target.reorderSound(soundIndex, newIndex);
|
|
1484
|
+
if (reorderSuccessful) {
|
|
1485
|
+
this.runtime.emitProjectChanged();
|
|
1486
|
+
}
|
|
1487
|
+
return reorderSuccessful;
|
|
1488
|
+
}
|
|
1489
|
+
return false;
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
/**
|
|
1493
|
+
* Put a target into a "drag" state, during which its X/Y positions will be unaffected
|
|
1494
|
+
* by blocks.
|
|
1495
|
+
* @param {string} targetId The id for the target to put into a drag state
|
|
1496
|
+
*/
|
|
1497
|
+
startDrag (targetId) {
|
|
1498
|
+
const target = this.runtime.getTargetById(targetId);
|
|
1499
|
+
if (target) {
|
|
1500
|
+
this._dragTarget = target;
|
|
1501
|
+
target.startDrag();
|
|
1502
|
+
}
|
|
1503
|
+
}
|
|
1504
|
+
|
|
1505
|
+
/**
|
|
1506
|
+
* Remove a target from a drag state, so blocks may begin affecting X/Y position again
|
|
1507
|
+
* @param {string} targetId The id for the target to remove from the drag state
|
|
1508
|
+
*/
|
|
1509
|
+
stopDrag (targetId) {
|
|
1510
|
+
const target = this.runtime.getTargetById(targetId);
|
|
1511
|
+
if (target) {
|
|
1512
|
+
this._dragTarget = null;
|
|
1513
|
+
target.stopDrag();
|
|
1514
|
+
this.setEditingTarget(target.sprite && target.sprite.clones[0] ?
|
|
1515
|
+
target.sprite.clones[0].id : target.id);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
/**
|
|
1520
|
+
* Post/edit sprite info for the current editing target or the drag target.
|
|
1521
|
+
* @param {object} data An object with sprite info data to set.
|
|
1522
|
+
*/
|
|
1523
|
+
postSpriteInfo (data) {
|
|
1524
|
+
if (this._dragTarget) {
|
|
1525
|
+
this._dragTarget.postSpriteInfo(data);
|
|
1526
|
+
} else {
|
|
1527
|
+
this.editingTarget.postSpriteInfo(data);
|
|
1528
|
+
}
|
|
1529
|
+
// Post sprite info means the gui has changed something about a sprite,
|
|
1530
|
+
// either through the sprite info pane fields (e.g. direction, size) or
|
|
1531
|
+
// through dragging a sprite on the stage
|
|
1532
|
+
// Emit a project changed event.
|
|
1533
|
+
this.runtime.emitProjectChanged();
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
/**
|
|
1537
|
+
* Set a target's variable's value. Return whether it succeeded.
|
|
1538
|
+
* @param {!string} targetId ID of the target which owns the variable.
|
|
1539
|
+
* @param {!string} variableId ID of the variable to set.
|
|
1540
|
+
* @param {!*} value The new value of that variable.
|
|
1541
|
+
* @returns {boolean} whether the target and variable were found and updated.
|
|
1542
|
+
*/
|
|
1543
|
+
setVariableValue (targetId, variableId, value) {
|
|
1544
|
+
const target = this.runtime.getTargetById(targetId);
|
|
1545
|
+
if (target) {
|
|
1546
|
+
const variable = target.lookupVariableById(variableId);
|
|
1547
|
+
if (variable) {
|
|
1548
|
+
variable.value = value;
|
|
1549
|
+
|
|
1550
|
+
if (variable.isCloud) {
|
|
1551
|
+
this.runtime.ioDevices.cloud.requestUpdateVariable(variable.name, variable.value);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
return true;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
return false;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
/**
|
|
1561
|
+
* Get a target's variable's value. Return null if the target or variable does not exist.
|
|
1562
|
+
* @param {!string} targetId ID of the target which owns the variable.
|
|
1563
|
+
* @param {!string} variableId ID of the variable to set.
|
|
1564
|
+
* @returns {?*} The value of the variable, or null if it could not be looked up.
|
|
1565
|
+
*/
|
|
1566
|
+
getVariableValue (targetId, variableId) {
|
|
1567
|
+
const target = this.runtime.getTargetById(targetId);
|
|
1568
|
+
if (target) {
|
|
1569
|
+
const variable = target.lookupVariableById(variableId);
|
|
1570
|
+
if (variable) {
|
|
1571
|
+
return variable.value;
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
return null;
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
/**
|
|
1578
|
+
* Allow VM consumer to configure the ScratchLink socket creator.
|
|
1579
|
+
* @param {Function} factory The custom ScratchLink socket factory.
|
|
1580
|
+
*/
|
|
1581
|
+
configureScratchLinkSocketFactory (factory) {
|
|
1582
|
+
this.runtime.configureScratchLinkSocketFactory(factory);
|
|
1583
|
+
}
|
|
1584
|
+
}
|
|
1585
|
+
|
|
1586
|
+
module.exports = VirtualMachine;
|