@scratch/scratch-vm 11.0.0-beta.1
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/.jsdoc.json +20 -0
- package/.nvmrc +1 -0
- package/CHANGELOG.md +7004 -0
- package/LICENSE +12 -0
- package/README.md +120 -0
- package/TRADEMARK +1 -0
- package/commitlint.config.js +4 -0
- package/docs/extensions.md +527 -0
- package/package.json +103 -0
- package/release.config.js +13 -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
- package/webpack.config.js +134 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @callback FetchFunction
|
|
3
|
+
* @param {RequestInfo|URL} input
|
|
4
|
+
* @param {RequestInit|undefined} [init]
|
|
5
|
+
* @returns {Promise<Response>}
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @type {FetchFunction}
|
|
10
|
+
*/
|
|
11
|
+
let myFetch = global.fetch;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Tell `fetchWithTimeout` to use a specific `fetch` function.
|
|
15
|
+
* By default, `fetchWithTimeout` will use the global `fetch` function.
|
|
16
|
+
* If there is no global `fetch`, then `fetchWithTimeout` will fail unless provided with an alternative.
|
|
17
|
+
* @param {FetchFunction} newFetch The new `fetch` function to use within fetchWithTimeout.
|
|
18
|
+
*/
|
|
19
|
+
const setFetch = newFetch => {
|
|
20
|
+
myFetch = newFetch;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Fetch a remote resource like `fetch` does, but with a time limit.
|
|
25
|
+
* @param {Request|string} resource Remote resource to fetch.
|
|
26
|
+
* @param {?object} init An options object containing any custom settings that you want to apply to the request.
|
|
27
|
+
* @param {number} timeout The amount of time before the request is canceled, in milliseconds
|
|
28
|
+
* @returns {Promise<Response>} The response from the server.
|
|
29
|
+
*/
|
|
30
|
+
const fetchWithTimeout = (resource, init, timeout) => {
|
|
31
|
+
let timeoutID = null;
|
|
32
|
+
// Not supported in Safari <11
|
|
33
|
+
const controller = window.AbortController ? new window.AbortController() : null;
|
|
34
|
+
const signal = controller ? controller.signal : null;
|
|
35
|
+
// The fetch call races a timer.
|
|
36
|
+
return Promise.race([
|
|
37
|
+
myFetch(resource, Object.assign({signal}, init)).then(response => {
|
|
38
|
+
clearTimeout(timeoutID);
|
|
39
|
+
return response;
|
|
40
|
+
}),
|
|
41
|
+
new Promise((resolve, reject) => {
|
|
42
|
+
timeoutID = setTimeout(() => {
|
|
43
|
+
if (controller) controller.abort();
|
|
44
|
+
reject(new Error(`Fetch timed out after ${timeout} ms`));
|
|
45
|
+
}, timeout);
|
|
46
|
+
})
|
|
47
|
+
]);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
module.exports = {
|
|
51
|
+
fetchWithTimeout,
|
|
52
|
+
setFetch
|
|
53
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a string representing a unique id for a monitored block
|
|
3
|
+
* where a single reporter block can have more than one monitor
|
|
4
|
+
* (and therefore more than one monitor block) associated
|
|
5
|
+
* with it (e.g. when reporter blocks have inputs).
|
|
6
|
+
* @param {string} baseId The base id to use for the different monitor blocks
|
|
7
|
+
* @param {object} fields The monitor block's fields object.
|
|
8
|
+
*/
|
|
9
|
+
// TODO this function should eventually be the single place where all monitor
|
|
10
|
+
// IDs are obtained given an opcode for the reporter block and the list of
|
|
11
|
+
// selected parameters.
|
|
12
|
+
const getMonitorIdForBlockWithArgs = function (id, fields) {
|
|
13
|
+
let fieldString = '';
|
|
14
|
+
for (const fieldKey in fields) {
|
|
15
|
+
let fieldValue = fields[fieldKey].value;
|
|
16
|
+
if (fieldKey === 'CURRENTMENU') {
|
|
17
|
+
// The 'sensing_current' block has field values in all caps.
|
|
18
|
+
// However, when importing from scratch 2.0, these
|
|
19
|
+
// could have gotten imported as lower case field values.
|
|
20
|
+
// Normalize the field value here so that we don't ever
|
|
21
|
+
// end up with a different monitor ID representing the same
|
|
22
|
+
// block configuration
|
|
23
|
+
// Note: we are not doing this for every block field that comes into
|
|
24
|
+
// this function so as not to make the faulty assumption that block
|
|
25
|
+
// field values coming in would be unique after being made lower case
|
|
26
|
+
fieldValue = fieldValue.toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
fieldString += `_${fieldValue}`;
|
|
29
|
+
}
|
|
30
|
+
return `${id}${fieldString}`;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
module.exports = getMonitorIdForBlockWithArgs;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
class JSONRPC {
|
|
2
|
+
constructor () {
|
|
3
|
+
this._requestID = 0;
|
|
4
|
+
this._openRequests = {};
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Make an RPC request and retrieve the result.
|
|
9
|
+
* @param {string} method - the remote method to call.
|
|
10
|
+
* @param {object} params - the parameters to pass to the remote method.
|
|
11
|
+
* @returns {Promise} - a promise for the result of the call.
|
|
12
|
+
*/
|
|
13
|
+
sendRemoteRequest (method, params) {
|
|
14
|
+
const requestID = this._requestID++;
|
|
15
|
+
|
|
16
|
+
const promise = new Promise((resolve, reject) => {
|
|
17
|
+
this._openRequests[requestID] = {resolve, reject};
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
this._sendRequest(method, params, requestID);
|
|
21
|
+
|
|
22
|
+
return promise;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Make an RPC notification with no expectation of a result or callback.
|
|
27
|
+
* @param {string} method - the remote method to call.
|
|
28
|
+
* @param {object} params - the parameters to pass to the remote method.
|
|
29
|
+
*/
|
|
30
|
+
sendRemoteNotification (method, params) {
|
|
31
|
+
this._sendRequest(method, params);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Handle an RPC request from remote, should return a result or Promise for result, if appropriate.
|
|
36
|
+
* @param {string} method - the method requested by the remote caller.
|
|
37
|
+
* @param {object} params - the parameters sent with the remote caller's request.
|
|
38
|
+
*/
|
|
39
|
+
didReceiveCall (/* method , params */) {
|
|
40
|
+
throw new Error('Must override didReceiveCall');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_sendMessage (/* jsonMessageObject */) {
|
|
44
|
+
throw new Error('Must override _sendMessage');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
_sendRequest (method, params, id) {
|
|
48
|
+
const request = {
|
|
49
|
+
jsonrpc: '2.0',
|
|
50
|
+
method,
|
|
51
|
+
params
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (id !== null) {
|
|
55
|
+
request.id = id;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
this._sendMessage(request);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
_handleMessage (json) {
|
|
62
|
+
if (json.jsonrpc !== '2.0') {
|
|
63
|
+
throw new Error(`Bad or missing JSON-RPC version in message: ${json}`);
|
|
64
|
+
}
|
|
65
|
+
if (Object.prototype.hasOwnProperty.call(json, 'method')) {
|
|
66
|
+
this._handleRequest(json);
|
|
67
|
+
} else {
|
|
68
|
+
this._handleResponse(json);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
_sendResponse (id, result, error) {
|
|
73
|
+
const response = {
|
|
74
|
+
jsonrpc: '2.0',
|
|
75
|
+
id
|
|
76
|
+
};
|
|
77
|
+
if (error) {
|
|
78
|
+
response.error = error;
|
|
79
|
+
} else {
|
|
80
|
+
response.result = result || null;
|
|
81
|
+
}
|
|
82
|
+
this._sendMessage(response);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
_handleResponse (json) {
|
|
86
|
+
const {result, error, id} = json;
|
|
87
|
+
const openRequest = this._openRequests[id];
|
|
88
|
+
delete this._openRequests[id];
|
|
89
|
+
if (openRequest) {
|
|
90
|
+
if (error) {
|
|
91
|
+
openRequest.reject(error);
|
|
92
|
+
} else {
|
|
93
|
+
openRequest.resolve(result);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
_handleRequest (json) {
|
|
99
|
+
const {method, params, id} = json;
|
|
100
|
+
const rawResult = this.didReceiveCall(method, params);
|
|
101
|
+
if (id !== null && typeof id !== 'undefined') {
|
|
102
|
+
Promise.resolve(rawResult).then(
|
|
103
|
+
result => {
|
|
104
|
+
this._sendResponse(id, result);
|
|
105
|
+
},
|
|
106
|
+
error => {
|
|
107
|
+
this._sendResponse(id, null, error);
|
|
108
|
+
}
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = JSONRPC;
|
package/src/util/log.js
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
class MathUtil {
|
|
2
|
+
/**
|
|
3
|
+
* Convert a value from degrees to radians.
|
|
4
|
+
* @param {!number} deg Value in degrees.
|
|
5
|
+
* @return {!number} Equivalent value in radians.
|
|
6
|
+
*/
|
|
7
|
+
static degToRad (deg) {
|
|
8
|
+
return deg * Math.PI / 180;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert a value from radians to degrees.
|
|
13
|
+
* @param {!number} rad Value in radians.
|
|
14
|
+
* @return {!number} Equivalent value in degrees.
|
|
15
|
+
*/
|
|
16
|
+
static radToDeg (rad) {
|
|
17
|
+
return rad * 180 / Math.PI;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Clamp a number between two limits.
|
|
22
|
+
* If n < min, return min. If n > max, return max. Else, return n.
|
|
23
|
+
* @param {!number} n Number to clamp.
|
|
24
|
+
* @param {!number} min Minimum limit.
|
|
25
|
+
* @param {!number} max Maximum limit.
|
|
26
|
+
* @return {!number} Value of n clamped to min and max.
|
|
27
|
+
*/
|
|
28
|
+
static clamp (n, min, max) {
|
|
29
|
+
return Math.min(Math.max(n, min), max);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Keep a number between two limits, wrapping "extra" into the range.
|
|
34
|
+
* e.g., wrapClamp(7, 1, 5) == 2
|
|
35
|
+
* wrapClamp(0, 1, 5) == 5
|
|
36
|
+
* wrapClamp(-11, -10, 6) == 6, etc.
|
|
37
|
+
* @param {!number} n Number to wrap.
|
|
38
|
+
* @param {!number} min Minimum limit.
|
|
39
|
+
* @param {!number} max Maximum limit.
|
|
40
|
+
* @return {!number} Value of n wrapped between min and max.
|
|
41
|
+
*/
|
|
42
|
+
static wrapClamp (n, min, max) {
|
|
43
|
+
const range = (max - min) + 1;
|
|
44
|
+
return n - (Math.floor((n - min) / range) * range);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Convert a value from tan function in degrees.
|
|
50
|
+
* @param {!number} angle in degrees
|
|
51
|
+
* @return {!number} Correct tan value
|
|
52
|
+
*/
|
|
53
|
+
static tan (angle) {
|
|
54
|
+
angle = angle % 360;
|
|
55
|
+
switch (angle) {
|
|
56
|
+
case -270:
|
|
57
|
+
case 90:
|
|
58
|
+
return Infinity;
|
|
59
|
+
case -90:
|
|
60
|
+
case 270:
|
|
61
|
+
return -Infinity;
|
|
62
|
+
default:
|
|
63
|
+
return parseFloat(Math.tan((Math.PI * angle) / 180).toFixed(10));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Given an array of unique numbers,
|
|
69
|
+
* returns a reduced array such that each element of the reduced array
|
|
70
|
+
* represents the position of that element in a sorted version of the
|
|
71
|
+
* original array.
|
|
72
|
+
* E.g. [5, 19. 13, 1] => [1, 3, 2, 0]
|
|
73
|
+
* @param {Array<number>} elts The elements to sort and reduce
|
|
74
|
+
* @return {Array<number>} The array of reduced orderings
|
|
75
|
+
*/
|
|
76
|
+
static reducedSortOrdering (elts) {
|
|
77
|
+
const sorted = elts.slice(0).sort((a, b) => a - b);
|
|
78
|
+
return elts.map(e => sorted.indexOf(e));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Return a random number given an inclusive range and a number in that
|
|
83
|
+
* range that should be excluded.
|
|
84
|
+
*
|
|
85
|
+
* For instance, (1, 5, 3) will only pick 1, 2, 4, or 5 (with equal
|
|
86
|
+
* probability)
|
|
87
|
+
*
|
|
88
|
+
* @param {number} lower - The lower bound (inlcusive)
|
|
89
|
+
* @param {number} upper - The upper bound (inclusive), such that lower <= upper
|
|
90
|
+
* @param {number} excluded - The number to exclude (MUST be in the range)
|
|
91
|
+
* @return {number} A random integer in the range [lower, upper] that is not "excluded"
|
|
92
|
+
*/
|
|
93
|
+
static inclusiveRandIntWithout (lower, upper, excluded) {
|
|
94
|
+
// Note that subtraction is the number of items in the
|
|
95
|
+
// inclusive range [lower, upper] minus 1 already
|
|
96
|
+
// (e.g. in the set {3, 4, 5}, 5 - 3 = 2).
|
|
97
|
+
const possibleOptions = upper - lower;
|
|
98
|
+
|
|
99
|
+
const randInt = lower + Math.floor(Math.random() * possibleOptions);
|
|
100
|
+
if (randInt >= excluded) {
|
|
101
|
+
return randInt + 1;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return randInt;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Scales a number from one range to another.
|
|
109
|
+
* @param {number} i number to be scaled
|
|
110
|
+
* @param {number} iMin input range minimum
|
|
111
|
+
* @param {number} iMax input range maximum
|
|
112
|
+
* @param {number} oMin output range minimum
|
|
113
|
+
* @param {number} oMax output range maximum
|
|
114
|
+
* @return {number} scaled number
|
|
115
|
+
*/
|
|
116
|
+
static scale (i, iMin, iMax, oMin, oMax) {
|
|
117
|
+
const p = (i - iMin) / (iMax - iMin);
|
|
118
|
+
return (p * (oMax - oMin)) + oMin;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = MathUtil;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
const formatMessage = require('format-message');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Check if `maybeMessage` looks like a message object, and if so pass it to `formatMessage`.
|
|
5
|
+
* Otherwise, return `maybeMessage` as-is.
|
|
6
|
+
* @param {*} maybeMessage - something that might be a message descriptor object.
|
|
7
|
+
* @param {object} [args] - the arguments to pass to `formatMessage` if it gets called.
|
|
8
|
+
* @param {string} [locale] - the locale to pass to `formatMessage` if it gets called.
|
|
9
|
+
* @return {string|*} - the formatted message OR the original `maybeMessage` input.
|
|
10
|
+
*/
|
|
11
|
+
const maybeFormatMessage = function (maybeMessage, args, locale) {
|
|
12
|
+
if (maybeMessage && maybeMessage.id && maybeMessage.default) {
|
|
13
|
+
return formatMessage(maybeMessage, args, locale);
|
|
14
|
+
}
|
|
15
|
+
return maybeMessage;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
module.exports = maybeFormatMessage;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const uid = require('./uid');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mutate the given blocks to have new IDs and update all internal ID references.
|
|
5
|
+
* Does not return anything to make it clear that the blocks are updated in-place.
|
|
6
|
+
* @param {array} blocks - blocks to be mutated.
|
|
7
|
+
*/
|
|
8
|
+
module.exports = blocks => {
|
|
9
|
+
const oldToNew = {};
|
|
10
|
+
|
|
11
|
+
// First update all top-level IDs and create old-to-new mapping
|
|
12
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
13
|
+
const newId = uid();
|
|
14
|
+
const oldId = blocks[i].id;
|
|
15
|
+
blocks[i].id = oldToNew[oldId] = newId;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Then go back through and update inputs (block/shadow)
|
|
19
|
+
// and next/parent properties
|
|
20
|
+
for (let i = 0; i < blocks.length; i++) {
|
|
21
|
+
for (const key in blocks[i].inputs) {
|
|
22
|
+
const input = blocks[i].inputs[key];
|
|
23
|
+
input.block = oldToNew[input.block];
|
|
24
|
+
input.shadow = oldToNew[input.shadow];
|
|
25
|
+
}
|
|
26
|
+
if (blocks[i].parent) {
|
|
27
|
+
blocks[i].parent = oldToNew[blocks[i].parent];
|
|
28
|
+
}
|
|
29
|
+
if (blocks[i].next) {
|
|
30
|
+
blocks[i].next = oldToNew[blocks[i].next];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const Timer = require('../util/timer');
|
|
2
|
+
|
|
3
|
+
class RateLimiter {
|
|
4
|
+
/**
|
|
5
|
+
* A utility for limiting the rate of repetitive send operations, such as
|
|
6
|
+
* bluetooth messages being sent to hardware devices. It uses the token bucket
|
|
7
|
+
* strategy: a counter accumulates tokens at a steady rate, and each send costs
|
|
8
|
+
* a token. If no tokens remain, it's not okay to send.
|
|
9
|
+
* @param {number} maxRate the maximum number of sends allowed per second
|
|
10
|
+
* @constructor
|
|
11
|
+
*/
|
|
12
|
+
constructor (maxRate) {
|
|
13
|
+
/**
|
|
14
|
+
* The maximum number of tokens.
|
|
15
|
+
* @type {number}
|
|
16
|
+
*/
|
|
17
|
+
this._maxTokens = maxRate;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The interval in milliseconds for refilling one token. It is calculated
|
|
21
|
+
* so that the tokens will be filled to maximum in one second.
|
|
22
|
+
* @type {number}
|
|
23
|
+
*/
|
|
24
|
+
this._refillInterval = 1000 / maxRate;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The current number of tokens in the bucket.
|
|
28
|
+
* @type {number}
|
|
29
|
+
*/
|
|
30
|
+
this._count = this._maxTokens;
|
|
31
|
+
|
|
32
|
+
this._timer = new Timer();
|
|
33
|
+
this._timer.start();
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The last time in milliseconds when the token count was updated.
|
|
37
|
+
* @type {number}
|
|
38
|
+
*/
|
|
39
|
+
this._lastUpdateTime = this._timer.timeElapsed();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check if it is okay to send a message, by updating the token count,
|
|
44
|
+
* taking a token and then checking if we are still under the rate limit.
|
|
45
|
+
* @return {boolean} true if we are under the rate limit
|
|
46
|
+
*/
|
|
47
|
+
okayToSend () {
|
|
48
|
+
// Calculate the number of tokens to refill the bucket with, based on the
|
|
49
|
+
// amount of time since the last refill.
|
|
50
|
+
const now = this._timer.timeElapsed();
|
|
51
|
+
const timeSinceRefill = now - this._lastUpdateTime;
|
|
52
|
+
const refillCount = Math.floor(timeSinceRefill / this._refillInterval);
|
|
53
|
+
|
|
54
|
+
// If we're adding at least one token, reset _lastUpdateTime to now.
|
|
55
|
+
// Otherwise, don't reset it so that we can continue measuring time until
|
|
56
|
+
// the next refill.
|
|
57
|
+
if (refillCount > 0) {
|
|
58
|
+
this._lastUpdateTime = now;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Refill the tokens up to the maximum
|
|
62
|
+
this._count = Math.min(this._maxTokens, this._count + refillCount);
|
|
63
|
+
|
|
64
|
+
// If we have at least one token, use one, and it's okay to send.
|
|
65
|
+
if (this._count > 0) {
|
|
66
|
+
this._count--;
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = RateLimiter;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* This class provides a ScratchLinkSocket implementation using WebSockets,
|
|
3
|
+
* attempting to connect with the locally installed Scratch-Link.
|
|
4
|
+
*
|
|
5
|
+
* To connect with ScratchLink without WebSockets, you must implement all of the
|
|
6
|
+
* public methods in this class.
|
|
7
|
+
* - open()
|
|
8
|
+
* - close()
|
|
9
|
+
* - setOn[Open|Close|Error]
|
|
10
|
+
* - setHandleMessage
|
|
11
|
+
* - sendMessage(msgObj)
|
|
12
|
+
* - isOpen()
|
|
13
|
+
*/
|
|
14
|
+
class ScratchLinkWebSocket {
|
|
15
|
+
constructor (type) {
|
|
16
|
+
this._type = type;
|
|
17
|
+
this._onOpen = null;
|
|
18
|
+
this._onClose = null;
|
|
19
|
+
this._onError = null;
|
|
20
|
+
this._handleMessage = null;
|
|
21
|
+
|
|
22
|
+
this._ws = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
open () {
|
|
26
|
+
if (!(this._onOpen && this._onClose && this._onError && this._handleMessage)) {
|
|
27
|
+
throw new Error('Must set open, close, message and error handlers before calling open on the socket');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let pathname;
|
|
31
|
+
switch (this._type) {
|
|
32
|
+
case 'BLE':
|
|
33
|
+
pathname = 'scratch/ble';
|
|
34
|
+
break;
|
|
35
|
+
case 'BT':
|
|
36
|
+
pathname = 'scratch/bt';
|
|
37
|
+
break;
|
|
38
|
+
default:
|
|
39
|
+
throw new Error(`Unknown ScratchLink socket Type: ${this._type}`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Try ws:// (the new way) and wss:// (the old way) simultaneously. If either connects, close the other. If we
|
|
43
|
+
// were to try one and fall back to the other on failure, that could mean a delay of 30 seconds or more for
|
|
44
|
+
// those who need the fallback.
|
|
45
|
+
// If both connections fail we should report only one error.
|
|
46
|
+
|
|
47
|
+
const setSocket = (socketToUse, socketToClose) => {
|
|
48
|
+
socketToClose.onopen = socketToClose.onerror = null;
|
|
49
|
+
socketToClose.close();
|
|
50
|
+
|
|
51
|
+
this._ws = socketToUse;
|
|
52
|
+
this._ws.onopen = this._onOpen;
|
|
53
|
+
this._ws.onclose = this._onClose;
|
|
54
|
+
this._ws.onerror = this._onError;
|
|
55
|
+
this._ws.onmessage = this._onMessage.bind(this);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const ws = new WebSocket(`ws://127.0.0.1:20111/${pathname}`);
|
|
59
|
+
const wss = new WebSocket(`wss://device-manager.scratch.mit.edu:20110/${pathname}`);
|
|
60
|
+
|
|
61
|
+
const connectTimeout = setTimeout(() => {
|
|
62
|
+
// neither socket succeeded before the timeout
|
|
63
|
+
setSocket(ws, wss);
|
|
64
|
+
this._ws.onerror(new Event('timeout'));
|
|
65
|
+
}, 15 * 1000);
|
|
66
|
+
ws.onopen = openEvent => {
|
|
67
|
+
clearTimeout(connectTimeout);
|
|
68
|
+
setSocket(ws, wss);
|
|
69
|
+
this._ws.onopen(openEvent);
|
|
70
|
+
};
|
|
71
|
+
wss.onopen = openEvent => {
|
|
72
|
+
clearTimeout(connectTimeout);
|
|
73
|
+
setSocket(wss, ws);
|
|
74
|
+
this._ws.onopen(openEvent);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
let wsError;
|
|
78
|
+
let wssError;
|
|
79
|
+
const errorHandler = () => {
|
|
80
|
+
// if only one has received an error, we haven't overall failed yet
|
|
81
|
+
if (wsError && wssError) {
|
|
82
|
+
clearTimeout(connectTimeout);
|
|
83
|
+
setSocket(ws, wss);
|
|
84
|
+
this._ws.onerror(wsError);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
ws.onerror = errorEvent => {
|
|
88
|
+
wsError = errorEvent;
|
|
89
|
+
errorHandler();
|
|
90
|
+
};
|
|
91
|
+
wss.onerror = errorEvent => {
|
|
92
|
+
wssError = errorEvent;
|
|
93
|
+
errorHandler();
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
close () {
|
|
98
|
+
this._ws.close();
|
|
99
|
+
this._ws = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
sendMessage (message) {
|
|
103
|
+
const messageText = JSON.stringify(message);
|
|
104
|
+
this._ws.send(messageText);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
setOnOpen (fn) {
|
|
108
|
+
this._onOpen = fn;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
setOnClose (fn) {
|
|
112
|
+
this._onClose = fn;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
setOnError (fn) {
|
|
116
|
+
this._onError = fn;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
setHandleMessage (fn) {
|
|
120
|
+
this._handleMessage = fn;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
isOpen () {
|
|
124
|
+
return this._ws && this._ws.readyState === this._ws.OPEN;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
_onMessage (e) {
|
|
128
|
+
const json = JSON.parse(e.data);
|
|
129
|
+
this._handleMessage(json);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = ScratchLinkWebSocket;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
const log = require('./log');
|
|
2
|
+
|
|
3
|
+
class StringUtil {
|
|
4
|
+
static withoutTrailingDigits (s) {
|
|
5
|
+
let i = s.length - 1;
|
|
6
|
+
while ((i >= 0) && ('0123456789'.indexOf(s.charAt(i)) > -1)) i--;
|
|
7
|
+
return s.slice(0, i + 1);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
static unusedName (name, existingNames) {
|
|
11
|
+
if (existingNames.indexOf(name) < 0) return name;
|
|
12
|
+
name = StringUtil.withoutTrailingDigits(name);
|
|
13
|
+
let i = 2;
|
|
14
|
+
while (existingNames.indexOf(name + i) >= 0) i++;
|
|
15
|
+
return name + i;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Split a string on the first occurrence of a split character.
|
|
20
|
+
* @param {string} text - the string to split.
|
|
21
|
+
* @param {string} separator - split the text on this character.
|
|
22
|
+
* @returns {string[]} - the two parts of the split string, or [text, null] if no split character found.
|
|
23
|
+
* @example
|
|
24
|
+
* // returns ['foo', 'tar.gz']
|
|
25
|
+
* splitFirst('foo.tar.gz', '.');
|
|
26
|
+
* @example
|
|
27
|
+
* // returns ['foo', null]
|
|
28
|
+
* splitFirst('foo', '.');
|
|
29
|
+
* @example
|
|
30
|
+
* // returns ['foo', '']
|
|
31
|
+
* splitFirst('foo.', '.');
|
|
32
|
+
*/
|
|
33
|
+
static splitFirst (text, separator) {
|
|
34
|
+
const index = text.indexOf(separator);
|
|
35
|
+
if (index >= 0) {
|
|
36
|
+
return [text.substring(0, index), text.substring(index + 1)];
|
|
37
|
+
}
|
|
38
|
+
return [text, null];
|
|
39
|
+
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* A customized version of JSON.stringify that sets Infinity/NaN to 0,
|
|
44
|
+
* instead of the default (null).
|
|
45
|
+
* Needed because null is not of type number, but Infinity/NaN are, which
|
|
46
|
+
* can lead to serialization producing JSON that isn't valid based on the parser schema.
|
|
47
|
+
* It is also consistent with the behavior of saving 2.0 projects.
|
|
48
|
+
* This is only needed when stringifying an object for saving.
|
|
49
|
+
*
|
|
50
|
+
* @param {!object} obj - The object to serialize
|
|
51
|
+
* @return {!string} The JSON.stringified string with Infinity/NaN replaced with 0
|
|
52
|
+
*/
|
|
53
|
+
static stringify (obj) {
|
|
54
|
+
return JSON.stringify(obj, (_key, value) => {
|
|
55
|
+
if (typeof value === 'number' &&
|
|
56
|
+
(value === Infinity || value === -Infinity || isNaN(value))){
|
|
57
|
+
return 0;
|
|
58
|
+
}
|
|
59
|
+
return value;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* A function to replace unsafe characters (not allowed in XML) with safe ones. This is used
|
|
64
|
+
* in cases where we're replacing non-user facing strings (e.g. variable IDs).
|
|
65
|
+
* When replacing user facing strings, the xmlEscape utility function should be used
|
|
66
|
+
* instead so that the user facing string does not change how it displays.
|
|
67
|
+
* @param {!string | !Array.<string>} unsafe Unsafe string possibly containing unicode control characters.
|
|
68
|
+
* In some cases this argument may be an array (e.g. hacked inputs from 2.0)
|
|
69
|
+
* @return {string} String with control characters replaced.
|
|
70
|
+
*/
|
|
71
|
+
static replaceUnsafeChars (unsafe) {
|
|
72
|
+
if (typeof unsafe !== 'string') {
|
|
73
|
+
if (Array.isArray(unsafe)) {
|
|
74
|
+
// This happens when we have hacked blocks from 2.0
|
|
75
|
+
// See #1030
|
|
76
|
+
unsafe = String(unsafe);
|
|
77
|
+
} else {
|
|
78
|
+
log.error('Unexpected input recieved in replaceUnsafeChars');
|
|
79
|
+
return unsafe;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return unsafe.replace(/[<>&'"]/g, c => {
|
|
83
|
+
switch (c) {
|
|
84
|
+
case '<': return 'lt';
|
|
85
|
+
case '>': return 'gt';
|
|
86
|
+
case '&': return 'amp';
|
|
87
|
+
case '\'': return 'apos';
|
|
88
|
+
case '"': return 'quot';
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
module.exports = StringUtil;
|