diff --git a/packages/audio/package.json b/packages/audio/package.json index 15f3e2fcc..d81c454f2 100644 --- a/packages/audio/package.json +++ b/packages/audio/package.json @@ -34,6 +34,7 @@ "@babel/core": "7.29.0", "@babel/eslint-parser": "7.28.6", "@babel/preset-env": "7.29.2", + "@types/node": "^25.5.2", "babel-eslint": "10.0.3", "babel-loader": "^10.1.1", "eslint": "^9.39.2", diff --git a/packages/audio/src/AudioEngine.js b/packages/audio/src/AudioEngine.js index c8fee02f7..6a1d3c89b 100644 --- a/packages/audio/src/AudioEngine.js +++ b/packages/audio/src/AudioEngine.js @@ -140,7 +140,7 @@ class AudioEngine { * Decode a sound, decompressing it into audio samples. * @param {object} sound - an object containing audio data and metadata for * a sound - * @param {Buffer} sound.data - sound data loaded from scratch-storage + * @param {{buffer: ArrayBuffer}} sound.data - sound data loaded from scratch-storage * @returns {?Promise} - a promise which will resolve to the sound id and * buffer if decoded */ @@ -217,8 +217,8 @@ class AudioEngine { * * @param {object} sound - an object containing audio data and metadata for * a sound - * @param {Buffer} sound.data - sound data loaded from scratch-storage - * @returns {?Promise} - a promise which will resolve to the buffer + * @param {{buffer: ArrayBuffer}} sound.data - sound data loaded from scratch-storage + * @returns - a promise which will resolve to the buffer */ decodeSoundPlayer (sound) { return this._decodeSound(sound) diff --git a/packages/audio/src/SoundPlayer.js b/packages/audio/src/SoundPlayer.js index 05c8e2d2e..2ed8ebc94 100644 --- a/packages/audio/src/SoundPlayer.js +++ b/packages/audio/src/SoundPlayer.js @@ -41,7 +41,7 @@ class SoundPlayer extends EventEmitter { /** * Output audio node. - * @type {AudioNode} + * @type {AudioBufferSourceNode} */ this.outputNode = null; diff --git a/packages/block/src/blocks/control.ts b/packages/block/src/blocks/control.ts index 35b293458..8ec6cdfa8 100644 --- a/packages/block/src/blocks/control.ts +++ b/packages/block/src/blocks/control.ts @@ -36,7 +36,7 @@ Blockly.Blocks['control_forever'] = { message0: Blockly.Msg.CONTROL_FOREVER, message1: '%1', // Statement message2: '%1', // Icon - lastDummyAlign2: 'RIGHT', + implicitAlign2: 'RIGHT', args1: [ { type: 'input_statement', @@ -69,7 +69,7 @@ Blockly.Blocks['control_repeat'] = { message0: Blockly.Msg.CONTROL_REPEAT, message1: '%1', // Statement message2: '%1', // Icon - lastDummyAlign2: 'RIGHT', + implicitAlign2: 'RIGHT', args0: [ { type: 'input_value', @@ -263,7 +263,7 @@ Blockly.Blocks['control_repeat_until'] = { message0: Blockly.Msg.CONTROL_REPEATUNTIL, message1: '%1', message2: '%1', - lastDummyAlign2: 'RIGHT', + implicitAlign2: 'RIGHT', args0: [ { type: 'input_value', @@ -302,7 +302,7 @@ Blockly.Blocks['control_while'] = { message0: Blockly.Msg.CONTROL_WHILE, message1: '%1', message2: '%1', - lastDummyAlign2: 'RIGHT', + implicitAlign2: 'RIGHT', args0: [ { type: 'input_value', diff --git a/packages/block/src/blocks/data.ts b/packages/block/src/blocks/data.ts index 1e46f6252..bc2753cbe 100644 --- a/packages/block/src/blocks/data.ts +++ b/packages/block/src/blocks/data.ts @@ -29,7 +29,7 @@ Blockly.Blocks['data_variable'] = { init: function(this: Blockly.Block) { this.jsonInit({ message0: '%1', - lastDummyAlign0: 'CENTRE', + implicitAlign0: 'CENTRE', args0: [ { type: 'field_variable_getter', @@ -142,7 +142,7 @@ Blockly.Blocks['data_listcontents'] = { init: function(this: Blockly.Block) { this.jsonInit({ message0: '%1', - lastDummyAlign0: 'CENTRE', + implicitAlign0: 'CENTRE', args0: [ { type: 'field_variable_getter', diff --git a/packages/block/src/events/block_comment_collapse.ts b/packages/block/src/events/block_comment_collapse.ts index 5ce88bca1..5ee1e4c94 100644 --- a/packages/block/src/events/block_comment_collapse.ts +++ b/packages/block/src/events/block_comment_collapse.ts @@ -16,7 +16,7 @@ export class BlockCommentCollapse extends BlockCommentBase { override type = 'block_comment_collapse'; /** Whether the comment is collpased. */ - protected newCollapsed?: boolean; + newCollapsed?: boolean; /** * @param icon The comment icon this event corresponds to. diff --git a/packages/block/src/events/func_change.ts b/packages/block/src/events/func_change.ts index 80e6f59ca..9e5acd9f1 100644 --- a/packages/block/src/events/func_change.ts +++ b/packages/block/src/events/func_change.ts @@ -19,10 +19,10 @@ export class FuncChange extends FuncBase { override type = FuncChange.TYPE; /** The previous extra state of the procedure. */ - protected oldExtraState?: ProcedureExtraState; + oldExtraState?: ProcedureExtraState; /** The new extra state of the procedure. */ - protected newExtraState?: ProcedureExtraState; + newExtraState?: ProcedureExtraState; /** * @param procedure The procedure model. diff --git a/packages/block/src/index.ts b/packages/block/src/index.ts index b9f6b5fff..f275f2b43 100644 --- a/packages/block/src/index.ts +++ b/packages/block/src/index.ts @@ -239,8 +239,21 @@ export type * as variableModel from './variable_model'; export {reportValue} from './report_value'; export {Colours} from './theme'; export {BlockDragEnd} from './events/block_drag_end'; +export {BlockDragOutside} from './events/block_drag_outside'; +export {FuncChange} from './events/func_change'; +export {FuncCreate} from './events/func_create'; +export {FuncDelete} from './events/func_delete'; +export {BlockCommentCreate} from './events/block_comment_create'; +export {BlockCommentDelete} from './events/block_comment_delete'; +export {BlockCommentMove} from './events/block_comment_move'; +export {BlockCommentResize} from './events/block_comment_resize'; +export {BlockCommentCollapse} from './events/block_comment_collapse'; +export {BlockChange} from './events/block_change'; +export {VarCreate} from './events/var_create'; +export {VarDelete} from './events/var_delete'; export * as Theme from './theme'; export {glowStack} from './glow'; +export type {BlockCommentState} from './block_comment_icon'; export { FieldAngle, diff --git a/packages/gui/.babelrc b/packages/gui/.babelrc index aeea6e789..e48768524 100644 --- a/packages/gui/.babelrc +++ b/packages/gui/.babelrc @@ -9,6 +9,6 @@ "presets": [ ["@babel/preset-env", {"targets": {"browsers": ["last 3 versions", "Safari >= 8", "iOS >= 8"]}}], "@babel/preset-react", - "@babel/preset-typescript" + ["@babel/preset-typescript", {"optimizeConstEnums": true}] ] } diff --git a/packages/gui/src/global.d.ts b/packages/gui/src/global.d.ts new file mode 100644 index 000000000..c79c21b9f --- /dev/null +++ b/packages/gui/src/global.d.ts @@ -0,0 +1,13 @@ +import type ScratchLinkWebSocket from '../../vm/src/util/scratch-link-websocket'; + +type ScratchLinkSafariSocket = (new (type: 'BLE' | 'BT') => ScratchLinkWebSocket) & { + isSafariHelperCompatible: () => boolean; +}; + +declare global { + interface Window { + Scratch?: { + ScratchLinkSafariSocket?: ScratchLinkSafariSocket; + } + } +} diff --git a/packages/gui/tsconfig.json b/packages/gui/tsconfig.json index c5f3d5585..23174a142 100644 --- a/packages/gui/tsconfig.json +++ b/packages/gui/tsconfig.json @@ -1,7 +1,8 @@ { "include": [ "./src/**/*", - "./test/**/*" + "./test/**/*", + "types/**/*.d.ts" ], "compilerOptions": { /* Visit https://aka.ms/tsconfig to read more about this file */ diff --git a/packages/lint-config/index.js b/packages/lint-config/index.js index f9ae13b09..84ac0dc1b 100644 --- a/packages/lint-config/index.js +++ b/packages/lint-config/index.js @@ -32,6 +32,7 @@ module.exports = [ rules: { // Most packaages uses JSDoc to generate declaration files, which has lots of undefined types. 'jsdoc/no-undefined-types': 'warn', + 'jsdoc/require-returns-type': 'off', /** tsc can infer return types */ 'jsdoc/reject-function-type': 'warn', 'jsdoc/check-param-names': 'error', 'jsdoc/check-tag-names': 'error', diff --git a/packages/lint-config/ts.js b/packages/lint-config/ts.js index 5410c12b7..bbd7a5e1e 100644 --- a/packages/lint-config/ts.js +++ b/packages/lint-config/ts.js @@ -25,7 +25,7 @@ module.exports = [ // Disable JSDoc type requirements for TypeScript (it has its own types) 'jsdoc/require-jsdoc': 'off', 'jsdoc/require-param-type': 'off', - 'jsdoc/require-returns-type': 'off', + 'jsdoc/require-property-type': 'off', 'func-style': 'off', // Disable rules that conflict with TypeScript's type system diff --git a/packages/render/src/RenderWebGL.js b/packages/render/src/RenderWebGL.js index fe127004f..844b349aa 100644 --- a/packages/render/src/RenderWebGL.js +++ b/packages/render/src/RenderWebGL.js @@ -381,8 +381,8 @@ class RenderWebGL extends EventEmitter { /** * Create a new SVG skin. * @param {!string} svgData - new SVG to use. - * @param {?Array} rotationCenter Optional: rotation center of the skin. If not supplied, the center of the - * skin will be used + * @param {?Array} [rotationCenter] Optional: rotation center of the skin. If not supplied, + * the center of the skin will be used * @returns {!int} the ID for the new skin. */ createSVGSkin (svgData, rotationCenter) { @@ -443,7 +443,8 @@ class RenderWebGL extends EventEmitter { * @param {!int} skinId the ID for the skin to change. * @param {!ImageData|HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} imgData - new contents for this skin. * @param {!number} bitmapResolution - the resolution scale for a bitmap costume. - * @param {?Array} rotationCenter Optional: rotation center of the skin. If not supplied, the center of the + * @param {?Array} [rotationCenter] Optional: + * rotation center of the skin. If not supplied, the center of the * skin will be used */ updateBitmapSkin (skinId, imgData, bitmapResolution, rotationCenter) { @@ -508,7 +509,7 @@ class RenderWebGL extends EventEmitter { /** * Create a new Drawable and add it to the scene. * @param {string} group Layer group to add the drawable to - * @returns {int | void} The ID of the new Drawable. + * @returns The ID of the new Drawable. */ createDrawable (group) { if (!group || !Object.prototype.hasOwnProperty.call(this._layerGroups, group)) { @@ -800,7 +801,7 @@ class RenderWebGL extends EventEmitter { /** * Get the size of a skin by ID. * @param {int} skinID The ID of the Skin to measure. - * @returns {Array} Skin size, width and height. + * @returns {[number, number]} Skin size, width and height. */ getSkinSize (skinID) { const skin = this._allSkins[skinID]; @@ -810,7 +811,7 @@ class RenderWebGL extends EventEmitter { /** * Get the rotation center of a skin by ID. * @param {int} skinID The ID of the Skin - * @returns {Array} The rotationCenterX and rotationCenterY + * @returns {[number, number]} The rotationCenterX and rotationCenterY */ getSkinRotationCenter (skinID) { const skin = this._allSkins[skinID]; diff --git a/packages/render/src/Skin.js b/packages/render/src/Skin.js index 8c67aacad..c173490cb 100644 --- a/packages/render/src/Skin.js +++ b/packages/render/src/Skin.js @@ -85,7 +85,7 @@ class Skin extends EventEmitter { /** * @abstract - * @returns {Array} the "native" size, in texels, of this skin. + * @returns {[number, number]} the "native" size, in texels, of this skin. */ get size () { return [0, 0]; @@ -106,7 +106,7 @@ class Skin extends EventEmitter { /** * Get the center of the current bounding box - * @returns {Array} the center of the current bounding box + * @returns {[number, number]} the center of the current bounding box */ calculateRotationCenter () { return [this.size[0] / 2, this.size[1] / 2]; diff --git a/packages/storage/src/Asset.ts b/packages/storage/src/Asset.ts index afcbc89e1..10748ef97 100644 --- a/packages/storage/src/Asset.ts +++ b/packages/storage/src/Asset.ts @@ -32,7 +32,7 @@ export default class Asset { */ constructor ( public assetType: IAssetType, - public assetId?: AssetId, + public assetId?: AssetId |null, dataFormat?: DataFormat, data?: AssetData, generateId?: boolean diff --git a/packages/storage/src/ScratchStorage.ts b/packages/storage/src/ScratchStorage.ts index 57e614eb9..becb0ed93 100644 --- a/packages/storage/src/ScratchStorage.ts +++ b/packages/storage/src/ScratchStorage.ts @@ -104,7 +104,7 @@ export class ScratchStorage { * @param assetId - The id of the asset to fetch. * @returns The asset, if it exists. */ - get (assetId: string): Asset | null { + get (assetId: AssetId): Asset | null { return this.builtinHelper.get(assetId); } @@ -134,8 +134,8 @@ export class ScratchStorage { assetType: IAssetType, dataFormat: DataFormat, data: AssetData, - id: AssetId, - generateId: boolean + id?: AssetId | null, + generateId?: boolean ): Asset { if (!dataFormat) throw new Error('Tried to create asset without a dataFormat'); return new Asset(assetType, id, dataFormat, data, generateId); @@ -202,7 +202,7 @@ export class ScratchStorage { * If the promise is rejected, there was an error on at least one asset source. HTTP 404 does not count as an * error here, but (for example) HTTP 403 does. */ - load (assetType: IAssetType, assetId: AssetId, dataFormat: DataFormat): Promise { + load (assetType: IAssetType, assetId: AssetId, dataFormat?: DataFormat): Promise { const helpers = this._helpers.map(x => x.helper); const errors: unknown[] = []; dataFormat = dataFormat || assetType.runtimeFormat; diff --git a/packages/vm/.babelrc b/packages/vm/.babelrc deleted file mode 100644 index 19bebcf05..000000000 --- a/packages/vm/.babelrc +++ /dev/null @@ -1,10 +0,0 @@ -{ - "presets": [ - ["@babel/preset-env", { - "targets": { - "browsers": ["last 3 versions", "Safari >= 8", "iOS >= 8"] - } - }], - "@babel/preset-typescript" - ] -} diff --git a/packages/vm/.babelrc.js b/packages/vm/.babelrc.js new file mode 100644 index 000000000..60f3665c3 --- /dev/null +++ b/packages/vm/.babelrc.js @@ -0,0 +1,20 @@ +const TESTING = process.env.NODE_ENV === 'test'; + +const config = { + presets: [ + ['@babel/preset-env', { + targets: { + browsers: ['last 3 versions', 'Safari >= 8', 'iOS >= 8'] + } + }], + ['@babel/preset-typescript', {optimizeConstEnums: true}] + ] +}; + +if (TESTING) { + config.plugins = [ + ['babel-plugin-transform-import-meta'] + ]; +} + +module.exports = config; diff --git a/packages/vm/docs/extensions.md b/packages/vm/docs/extensions.md index 8bd802405..a30f3fe34 100644 --- a/packages/vm/docs/extensions.md +++ b/packages/vm/docs/extensions.md @@ -287,7 +287,7 @@ const TargetType = require('../../extension-support/target-type'); const formatMessage = require('format-message'); // Core, Team, and Official extension classes should be registered statically with the Extension Manager. -// See: scratch-vm/src/extension-support/extension-manager.js +// See: scratch-vm/src/extension-support/extension-manager.ts class SomeBlocks { constructor (runtime) { /** diff --git a/packages/vm/package.json b/packages/vm/package.json index 3107248e3..aacedf344 100644 --- a/packages/vm/package.json +++ b/packages/vm/package.json @@ -10,7 +10,7 @@ "url": "https://github.com/Clipteam/clipcc.git" }, "exports": { - "types": "./dist/types/index.d.ts", + "types": "./dist/types/src/index.d.ts", "webpack": "./src/index.ts", "node": "./dist/node/scratch-vm.js", "browser": "./dist/web/scratch-vm.min.js", @@ -66,6 +66,7 @@ "babel-eslint": "10.1.0", "babel-jest": "23.6.0", "babel-loader": "^10.1.1", + "babel-plugin-transform-import-meta": "^2.3.3", "callsite": "1.0.0", "clipcc-audio": "workspace:~", "clipcc-block": "workspace:~", @@ -73,7 +74,6 @@ "clipcc-render": "workspace:~", "clipcc-storage": "workspace:~", "clipcc-svg-renderer": "workspace:~", - "codingclip-worker-loader": "^3.0.10", "copy-webpack-plugin": "^14.0.0", "domhandler": "^5.0.3", "eslint": "^9.39.2", diff --git a/packages/vm/src/blocks/category_prototype.ts b/packages/vm/src/blocks/category_prototype.ts index 5d554f21c..d9d6c6fe0 100644 --- a/packages/vm/src/blocks/category_prototype.ts +++ b/packages/vm/src/blocks/category_prototype.ts @@ -1,6 +1,7 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import type BlockUtility from '../engine/block-utility'; import type {HatMetadata, MonitorBlockInfo} from '../engine/runtime'; +import type Runtime from '../engine/runtime'; export type BlockArgs = { [argName: string]: any; @@ -19,3 +20,5 @@ export interface CategoryPrototype { getMonitored?(): Record; getOrders?(): Record; } + +export type CategoryPrototypeConstructor = new (runtime: Runtime) => CategoryPrototype; diff --git a/packages/vm/src/blocks/scratch3_core_example.js b/packages/vm/src/blocks/scratch3_core_example.ts similarity index 86% rename from packages/vm/src/blocks/scratch3_core_example.js rename to packages/vm/src/blocks/scratch3_core_example.ts index 4dd27ca05..2f5cfe9e1 100644 --- a/packages/vm/src/blocks/scratch3_core_example.js +++ b/packages/vm/src/blocks/scratch3_core_example.ts @@ -1,6 +1,7 @@ import BlockType from '../extension-support/block-type'; import ArgumentType from '../extension-support/argument-type'; - +import type {ExtensionClass} from '../extension-support/extension-metadata'; +import type Runtime from '../engine/runtime'; const blockIconURI = 'data:image/svg+xml,%3Csvg id="rotate-counter-clockwise" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"%3E%3Cdefs%3E%3Cstyle%3E.cls-1%7Bfill:%233d79cc;%7D.cls-2%7Bfill:%23fff;%7D%3C/style%3E%3C/defs%3E%3Ctitle%3Erotate-counter-clockwise%3C/title%3E%3Cpath class="cls-1" d="M22.68,12.2a1.6,1.6,0,0,1-1.27.63H13.72a1.59,1.59,0,0,1-1.16-2.58l1.12-1.41a4.82,4.82,0,0,0-3.14-.77,4.31,4.31,0,0,0-2,.8,4.25,4.25,0,0,0-1.34,1.73,5.06,5.06,0,0,0,.54,4.62A5.58,5.58,0,0,0,12,17.74h0a2.26,2.26,0,0,1-.16,4.52A10.25,10.25,0,0,1,3.74,18,10.14,10.14,0,0,1,2.25,8.78,9.7,9.7,0,0,1,5.08,4.64,9.92,9.92,0,0,1,9.66,2.5a10.66,10.66,0,0,1,7.72,1.68l1.08-1.35a1.57,1.57,0,0,1,1.24-.6,1.6,1.6,0,0,1,1.54,1.21l1.7,7.37A1.57,1.57,0,0,1,22.68,12.2Z"/%3E%3Cpath class="cls-2" d="M21.38,11.83H13.77a.59.59,0,0,1-.43-1l1.75-2.19a5.9,5.9,0,0,0-4.7-1.58,5.07,5.07,0,0,0-4.11,3.17A6,6,0,0,0,7,15.77a6.51,6.51,0,0,0,5,2.92,1.31,1.31,0,0,1-.08,2.62,9.3,9.3,0,0,1-7.35-3.82A9.16,9.16,0,0,1,3.17,9.12,8.51,8.51,0,0,1,5.71,5.4,8.76,8.76,0,0,1,9.82,3.48a9.71,9.71,0,0,1,7.75,2.07l1.67-2.1a.59.59,0,0,1,1,.21L22,11.08A.59.59,0,0,1,21.38,11.83Z"/%3E%3C/svg%3E'; @@ -9,17 +10,17 @@ const blockIconURI = 'data:image/svg+xml,%3Csvg id="rotate-counter-clockwise" xm * This is not loaded as part of the core blocks in the VM but it is provided * and used as part of tests. */ -class Scratch3CoreExample { - constructor (runtime) { +class Scratch3CoreExample implements ExtensionClass { + constructor ( /** * The runtime instantiating this block package. - * @type {Runtime} */ - this.runtime = runtime; - } + public runtime: Runtime + ) {} /** - * @returns {object} metadata for this extension and its blocks. + * Get the metadata for this extension and its blocks. + * @returns metadata for this extension and its blocks. */ getInfo () { return { @@ -30,7 +31,7 @@ class Scratch3CoreExample { func: 'MAKE_A_VARIABLE', blockType: BlockType.BUTTON, text: 'make a variable (CoreEx)' - }, + } as const, { opcode: 'exampleOpcode', blockType: BlockType.REPORTER, @@ -53,7 +54,7 @@ class Scratch3CoreExample { /** * Example opcode just returns the name of the stage target. - * @returns {string} The name of the first target in the project. + * @returns The name of the first target in the project. */ exampleOpcode () { const stage = this.runtime.getTargetForStage(); diff --git a/packages/vm/src/blocks/scratch3_looks.ts b/packages/vm/src/blocks/scratch3_looks.ts index 7851a9b44..4ded504cd 100644 --- a/packages/vm/src/blocks/scratch3_looks.ts +++ b/packages/vm/src/blocks/scratch3_looks.ts @@ -1,6 +1,6 @@ import Cast from '../util/cast'; import Clone from '../util/clone'; -import RenderedTarget from '../sprites/rendered-target.js'; +import RenderedTarget from '../sprites/rendered-target'; import uid from '../util/uid'; import StageLayering from '../engine/stage-layering'; import getMonitorIdForBlockWithArgs from '../util/get-monitor-id'; @@ -8,7 +8,7 @@ import MathUtil from '../util/math-util'; import type {BlockArgs, CategoryPrototype} from './category_prototype'; import type Runtime from '../engine/runtime'; import type BlockUtility from '../engine/block-utility'; -import type Target from '../engine/target.js'; +import type Target from '../engine/target'; import type {MonitorBlockInfo} from '../engine/runtime'; import type Thread from '../engine/thread'; import type {BaseExecutionContext} from '../engine/block-utility'; @@ -81,38 +81,38 @@ class Scratch3LooksBlocks implements CategoryPrototype { /** * The key to load & store a target's bubble-related state. */ - static get STATE_KEY (): string { - return 'Scratch.looks'; + static get STATE_KEY () { + return 'Scratch.looks' as const; } /** * Event name for a text bubble being created or updated. */ - static get SAY_OR_THINK (): string { + static get SAY_OR_THINK () { // There are currently many places in the codebase which explicitly refer to this event by the string 'SAY', // so keep this as the string 'SAY' for now rather than changing it to 'SAY_OR_THINK' and breaking things. - return 'SAY'; + return 'SAY' as const; } /** * Limit for say bubble string. */ - static get SAY_BUBBLE_LIMIT (): number { - return 330; + static get SAY_BUBBLE_LIMIT () { + return 330 as const; } /** * Limit for ghost effect */ - static get EFFECT_GHOST_LIMIT (): {min: number, max: number} { - return {min: 0, max: 100}; + static get EFFECT_GHOST_LIMIT () { + return {min: 0, max: 100} as const; } /** * Limit for brightness effect */ - static get EFFECT_BRIGHTNESS_LIMIT (): {min: number, max: number} { - return {min: -100, max: 100}; + static get EFFECT_BRIGHTNESS_LIMIT () { + return {min: -100, max: 100} as const; } /** @@ -121,7 +121,7 @@ class Scratch3LooksBlocks implements CategoryPrototype { * @returns the mutable bubble state associated with that target. This will be created if necessary. */ _getBubbleState (target: Target): BubbleState { - let bubbleState = target.getCustomState(Scratch3LooksBlocks.STATE_KEY); + let bubbleState = target.getCustomState(Scratch3LooksBlocks.STATE_KEY); if (!bubbleState) { bubbleState = Clone.simple(Scratch3LooksBlocks.DEFAULT_BUBBLE_STATE); target.setCustomState(Scratch3LooksBlocks.STATE_KEY, bubbleState); diff --git a/packages/vm/src/blocks/scratch3_motion.ts b/packages/vm/src/blocks/scratch3_motion.ts index ccb049742..36413aeba 100644 --- a/packages/vm/src/blocks/scratch3_motion.ts +++ b/packages/vm/src/blocks/scratch3_motion.ts @@ -248,6 +248,7 @@ class Scratch3MotionBlocks implements CategoryPrototype { util.target.setDirection(newDirection); // Keep within the stage. const fencedPosition = util.target.keepInFence(util.target.x, util.target.y); + if (!fencedPosition) return; util.target.setXY(fencedPosition[0], fencedPosition[1]); } diff --git a/packages/vm/src/blocks/scratch3_sound.ts b/packages/vm/src/blocks/scratch3_sound.ts index 1fcf0367c..25ad99693 100644 --- a/packages/vm/src/blocks/scratch3_sound.ts +++ b/packages/vm/src/blocks/scratch3_sound.ts @@ -115,7 +115,7 @@ class Scratch3SoundBlocks implements CategoryPrototype { * @returns the mutable sound state associated with that target. This will be created if necessary. */ _getSoundState (target: RenderedTarget): SoundState { - let soundState: SoundState = target.getCustomState(Scratch3SoundBlocks.STATE_KEY); + let soundState = target.getCustomState(Scratch3SoundBlocks.STATE_KEY); if (!soundState) { soundState = Clone.simple(Scratch3SoundBlocks.DEFAULT_SOUND_STATE); target.setCustomState(Scratch3SoundBlocks.STATE_KEY, soundState); @@ -132,7 +132,7 @@ class Scratch3SoundBlocks implements CategoryPrototype { */ _onTargetCreated (newTarget: RenderedTarget, sourceTarget?: RenderedTarget) { if (sourceTarget) { - const soundState = sourceTarget.getCustomState(Scratch3SoundBlocks.STATE_KEY); + const soundState = sourceTarget.getCustomState(Scratch3SoundBlocks.STATE_KEY); if (soundState && newTarget) { newTarget.setCustomState(Scratch3SoundBlocks.STATE_KEY, Clone.simple(soundState)); this._syncEffectsForTarget(newTarget); diff --git a/packages/vm/src/dispatch/central-dispatch.ts b/packages/vm/src/dispatch/central-dispatch.ts index 6f7530377..92e6e3924 100644 --- a/packages/vm/src/dispatch/central-dispatch.ts +++ b/packages/vm/src/dispatch/central-dispatch.ts @@ -14,7 +14,7 @@ export class CentralDispatch extends SharedDispatch { * If the entry is a Worker, the service is provided by an object on that worker. * Otherwise, the service is provided locally and methods on the service will be called directly. */ - private services: Record = {}; + services: Record = {}; /** * The constructor we will use to recognize workers. diff --git a/packages/vm/src/dispatch/shared-dispatch.ts b/packages/vm/src/dispatch/shared-dispatch.ts index 6a93fb28a..4151ef562 100644 --- a/packages/vm/src/dispatch/shared-dispatch.ts +++ b/packages/vm/src/dispatch/shared-dispatch.ts @@ -135,7 +135,7 @@ export abstract class SharedDispatch { * @param method The name of the method. * @param transfer Objects to be transferred instead of copied. Must be present in `args` to be useful. * @param args The arguments to be copied to the method, if any. - * @returns {Promise} - a promise for the return value of the service method. + * @returns a promise for the return value of the service method. */ private remoteTransferCall ( provider: WorkerLike, diff --git a/packages/vm/src/engine/adapter.ts b/packages/vm/src/engine/adapter.ts index 3bfe5444b..d0a2d2fd6 100644 --- a/packages/vm/src/engine/adapter.ts +++ b/packages/vm/src/engine/adapter.ts @@ -20,7 +20,7 @@ const domToBlock = function ( blocks: Record, isTopBlock: boolean, parent: string | null -): void { +) { if (!blockDOM.attribs.id) { blockDOM.attribs.id = uid(); } @@ -184,7 +184,7 @@ const stateToBlock = function ( isTopBlock: boolean, parent: string | null, isShadow?: boolean -): void { +) { if (!blockState.id) { blockState.id = uid(); } @@ -306,7 +306,8 @@ const stateToBlocks = function (blocksState: BlockState | BlockState[]): VMBlock return blocksList; }; -type AdaptableEvents = (ClipCCBlocks.Events.BlockCreate | ClipCCBlocks.BlockDragEnd) & {xml?: { outerHTML: string }}; +export type AdaptableEvents = + (ClipCCBlocks.Events.BlockCreate | ClipCCBlocks.BlockDragEnd) & {xml?: { outerHTML: string }}; /** * Adapter between block creation events and block representation which can be diff --git a/packages/vm/src/engine/block-utility.ts b/packages/vm/src/engine/block-utility.ts index 8ddfc4e61..1fdcbbbd7 100644 --- a/packages/vm/src/engine/block-utility.ts +++ b/packages/vm/src/engine/block-utility.ts @@ -129,7 +129,7 @@ class BlockUtility { * Create and start a stack timer * @param duration - a duration in milliseconds to set the timer for. */ - startStackTimer (duration: number): void { + startStackTimer (duration: number) { if (this.nowObj) { this.stackFrame.timer = new Timer(this.nowObj); } else { @@ -142,14 +142,14 @@ class BlockUtility { /** * Set the thread to yield. */ - yield (): void { + yield () { this.thread!.status = Thread.STATUS_YIELD; } /** * Set the thread to yield until the next tick of the runtime. */ - yieldTick (): void { + yieldTick () { this.thread!.status = Thread.STATUS_YIELD_TICK; } @@ -158,14 +158,14 @@ class BlockUtility { * @param branchNum Which branch to step to (i.e., 1, 2). * @param isLoop Whether this block is a loop. */ - startBranch (branchNum: number, isLoop: boolean): void { + startBranch (branchNum: number, isLoop: boolean) { this.sequencer!.stepToBranch(this.thread!, branchNum, isLoop); } /** * Stop all threads. */ - stopAll (): void { + stopAll () { this.sequencer!.runtime.stopAll(); } @@ -173,14 +173,14 @@ class BlockUtility { * Stop threads other on this target other than the thread holding the * executed block. */ - stopOtherTargetThreads (): void { + stopOtherTargetThreads () { this.sequencer!.runtime.stopForTarget(this.thread!.target!, this.thread!); } /** * Stop this thread. */ - stopThisScript (): void { + stopThisScript () { this.thread!.stopThisScript(); } @@ -188,7 +188,7 @@ class BlockUtility { * Start a specified procedure on this thread. * @param procedureCode Procedure code for procedure to start. */ - startProcedure (procedureCode: string): void { + startProcedure (procedureCode: string) { this.sequencer!.stepToProcedure(this.thread!, procedureCode); } @@ -221,7 +221,7 @@ class BlockUtility { /** * Initialize procedure parameters in the thread before pushing parameters. */ - initParams (): void { + initParams () { this.thread!.initParams(); } @@ -230,7 +230,7 @@ class BlockUtility { * @param paramName The procedure's parameter name. * @param paramValue The procedure's parameter value. */ - pushParam (paramName: string, paramValue: unknown): void { + pushParam (paramName: string, paramValue: unknown) { this.thread!.pushParam(paramName, paramValue); } @@ -250,7 +250,7 @@ class BlockUtility { * @param optTarget Optionally, a target to restrict to. * @returns List of threads started by this function. */ - startHats (requestedHat: string, optMatchFields?: object, optTarget?: RenderedTarget) { + startHats (requestedHat: string, optMatchFields?: Record, optTarget?: RenderedTarget) { // Store thread and sequencer to ensure we can return to the calling block's context. // startHats may execute further blocks and dirty the BlockUtility's execution context // and confuse the calling block when we return to it. @@ -290,4 +290,5 @@ class BlockUtility { } } +export type {BlockUtility, ExecutionContext}; export default BlockUtility; diff --git a/packages/vm/src/engine/blocks-execute-cache.ts b/packages/vm/src/engine/blocks-execute-cache.ts index 0f06808dc..043258ff1 100644 --- a/packages/vm/src/engine/blocks-execute-cache.ts +++ b/packages/vm/src/engine/blocks-execute-cache.ts @@ -7,7 +7,7 @@ import type Blocks from './blocks'; import type {VMInput, VMField, VMMutation} from '../serialization/schema'; -interface CachedBlockData { +export interface CachedBlockData { id: string; opcode: string; fields: Record; @@ -15,7 +15,7 @@ interface CachedBlockData { mutation?: VMMutation; } -type CacheType = new (blocks: Blocks, cached: CachedBlockData) => object; +export type CacheType = new (blocks: Blocks, cached: CachedBlockData) => CachedBlockData; /** * A private method shared with execute to build an object containing the block @@ -26,12 +26,16 @@ type CacheType = new (blocks: Blocks, cached: CachedBlockData) => object; * @param CacheType constructor for cached block information * @returns execute cache object */ -const getCached = function (blocks: Blocks, blockId: string, CacheType?: CacheType): object | null { - const executeCache = blocks._cache._executeCached as Record; +const getCached = function ( + blocks: Blocks, + blockId: string, + CacheType?: T +): (T extends never ? CachedBlockData : InstanceType) | null { + const executeCache = blocks._cache._executeCached; let cached = executeCache[blockId]; if (typeof cached !== 'undefined') { - return cached; + return cached as T extends never ? CachedBlockData : InstanceType; } const block = blocks.getBlock(blockId); @@ -43,7 +47,7 @@ const getCached = function (blocks: Blocks, blockId: string, CacheType?: CacheTy id: blockId, opcode: blocks.getOpcode(block)!, fields: blocks.getFields(block)!, - inputs: blocks.getInputs(block), + inputs: blocks.getInputs(block)!, mutation: blocks.getMutation(block)! }; @@ -52,7 +56,7 @@ const getCached = function (blocks: Blocks, blockId: string, CacheType?: CacheTy new CacheType(blocks, cachedBlockData); executeCache[blockId] = cached; - return cached; + return cached as T extends never ? CachedBlockData : InstanceType; }; export { diff --git a/packages/vm/src/engine/blocks-runtime-cache.ts b/packages/vm/src/engine/blocks-runtime-cache.ts index dac0cbdca..5be12d56f 100644 --- a/packages/vm/src/engine/blocks-runtime-cache.ts +++ b/packages/vm/src/engine/blocks-runtime-cache.ts @@ -61,9 +61,7 @@ class RuntimeScriptCache { } for (const key in this.fieldsOfInputs) { const field = this.fieldsOfInputs[key] = Object.assign({}, this.fieldsOfInputs[key]); - if (field.value?.toUpperCase) { - field.value = field.value.toUpperCase(); - } + field.value = field.value?.toUpperCase(); } } } diff --git a/packages/vm/src/engine/blocks.js b/packages/vm/src/engine/blocks.ts similarity index 71% rename from packages/vm/src/engine/blocks.js rename to packages/vm/src/engine/blocks.ts index 0a53eafc1..aa79ad8b2 100644 --- a/packages/vm/src/engine/blocks.js +++ b/packages/vm/src/engine/blocks.ts @@ -1,4 +1,4 @@ -import adapter from './adapter'; +import adapter, {type AdaptableEvents} from './adapter'; import xmlEscape from '../util/xml-escape'; import MonitorRecord from './monitor-record'; import Clone from '../util/clone'; @@ -6,6 +6,13 @@ import {Map} from 'immutable'; import log from '../util/log'; import Variable from './variable'; import getMonitorIdForBlockWithArgs from '../util/get-monitor-id'; +import type Runtime from './runtime'; +import type {ProcedureMutation, VMBlock, VMInput, VMMutation} from '../serialization/schema'; +import type {RuntimeScriptCache} from './blocks-runtime-cache'; +import type * as ClipCCBlock from 'clipcc-block'; +import type {CachedBlockData, CacheType} from './blocks-execute-cache'; +import type RenderedTarget from '../sprites/rendered-target'; +import type Comment from './comment'; /** * @fileoverview @@ -13,13 +20,58 @@ import getMonitorIdForBlockWithArgs from '../util/get-monitor-id'; * and handle updates from Scratch Blocks events. */ -/** - * @typedef {import('./runtime').default} Runtime - * @typedef {import('../serialization/schema').VMBlock} VMBlock - * @typedef {import('../serialization/schema').VMInput} VMInput - * @typedef {import('./blocks-runtime-cache').RuntimeScriptCache} RuntimeScriptCache - * @import * as ClipCCBlock from 'clipcc-block' - */ +interface CacheState { + /** + * A cache of hat opcodes to collection of threads to execute + */ + scripts: Record; + /** + * Cache block inputs by block id + */ + inputs: Record>; + /** + * Cache procedure Param Names by block id. + * Tuple for [names, ids, defaults] + */ + procedureParamNames: Record; + /** + * Cache procedure definitions by block id + */ + procedureDefinitions: Record; + /** + * A cache for execute to use and store on by block id. + */ + _executeCached: Record; + /** + * A cache of block IDs and targets to start threads on as they are + * actively monitored. + */ + _monitored: null | Array<{blockId: string, target: RenderedTarget | null | undefined}>; +} + +type ListenableBlocklyEvents = + ClipCCBlock.Events.BlockCreate + | ClipCCBlock.Events.BlockChange + | ClipCCBlock.Events.BlockMove + | ClipCCBlock.BlockDragOutside + | ClipCCBlock.BlockDragEnd + | ClipCCBlock.Events.BlockDelete + | ClipCCBlock.VarCreate + | ClipCCBlock.Events.VarRename + | ClipCCBlock.VarDelete + | ClipCCBlock.BlockCommentCreate + | ClipCCBlock.Events.CommentCreate + | ClipCCBlock.Events.CommentChange + | ClipCCBlock.BlockCommentMove + | ClipCCBlock.Events.CommentMove + | ClipCCBlock.BlockCommentCollapse + | ClipCCBlock.Events.CommentCollapse + | ClipCCBlock.BlockCommentResize + | ClipCCBlock.Events.CommentResize + | ClipCCBlock.BlockCommentDelete + | ClipCCBlock.Events.CommentDelete + | ClipCCBlock.FuncChange + | ClipCCBlock.Events.Click; /** * Create a fresh set of derived block caches. @@ -27,80 +79,41 @@ import getMonitorIdForBlockWithArgs from '../util/get-monitor-id'; * managed by their own modules without relying on module side effects. * @returns Newly initialized cache state */ -const createCacheState = function () { +const createCacheState = function (): CacheState { return { - /** - * Cache block inputs by block id - * @type {Record>} - */ inputs: {}, - - /** - * Cache procedure Param Names by block id - * @type {Record>} - */ procedureParamNames: {}, - - /** - * Cache procedure definitions by block id - * @type {Record} - */ procedureDefinitions: {}, - - /** - * A cache for execute to use and store on by block id. - * @type {Record} - */ _executeCached: {}, - - /** - * A cache of block IDs and targets to start threads on as they are - * actively monitored. - * @type {?Array<{blockId: string, target: Target}>} - */ _monitored: null, - - /** - * A cache of hat opcodes to collection of threads to execute - * @type {Record>} - */ scripts: {} }; }; /** * Create a block container. - * @param {Runtime} runtime The runtime this block container operates within - * @param {boolean} optNoGlow Optional flag to indicate that blocks in this container - * should not request glows. This does not affect glows when clicking on a block to execute it. */ class Blocks { - constructor (runtime, optNoGlow) { - /** @type {Runtime} */ - this.runtime = runtime; - - /** - * All blocks in the workspace. - * Keys are block IDs, values are metadata about the block. - * @type {Record} - */ - this._blocks = {}; - - /** - * All top-level scripts in the workspace. - * A list of block IDs that represent scripts (i.e., first block in script). - * @type {Array.} - */ - this._scripts = []; + /** + * All blocks in the workspace. + * Keys are block IDs, values are metadata about the block. + */ + _blocks: Record = {}; + /** + * All top-level scripts in the workspace. + * A list of block IDs that represent scripts (i.e., first block in script). + */ + _scripts: string[] = []; + /** + * Derived block caches invalidated together when block state changes. + */ + _cache = createCacheState(); + constructor ( /** - * Derived block caches invalidated together when block state changes. - * @type {object} - * @private + * The runtime this block container operates within */ - Object.defineProperty(this, '_cache', {writable: true, enumerable: false}); - this._cache = createCacheState(); - + public runtime: Runtime, /** * Flag which indicates that blocks in this container should not glow. * Blocks will still glow when clicked on, but this flag is used to control @@ -108,32 +121,31 @@ class Blocks { * a running stack. E.g. the flyout block container and the monitor block container * should not be able to request a glow, but blocks containers belonging to * sprites should. - * @type {boolean} */ - this.forceNoGlow = optNoGlow || false; + public forceNoGlow = false + ) { + Object.defineProperty(this, '_cache', {writable: true, enumerable: false}); } /** * Blockly inputs that represent statements/branch. * are prefixed with this string. - * @returns {string} */ static get BRANCH_INPUT_PREFIX () { - return 'SUBSTACK'; + return 'SUBSTACK' as const; } /** * Provide an object with metadata for the requested block ID. - * @param {string | null} [blockId] ID of block we have stored. - * @returns {VMBlock | undefined} Metadata about the block, if it exists. + * @param blockId ID of block we have stored. + * @returns Metadata about the block, if it exists. */ - getBlock (blockId) { - return this._blocks[blockId]; + getBlock (blockId?: string | null) { + return blockId ? this._blocks[blockId] : undefined; } /** * Get all known top-level blocks that start scripts. - * @returns {Array.} List of block IDs. */ getScripts () { return this._scripts; @@ -141,21 +153,23 @@ class Blocks { /** * Get the next block for a particular block - * @param {?string} id ID of block to get the next block for - * @returns {?string} ID of next block in the sequence + * @param id ID of block to get the next block for + * @returns ID of next block in the sequence */ - getNextBlock (id) { + getNextBlock (id: string | null) { + if (!id) return null; const block = this._blocks[id]; return (typeof block === 'undefined') ? null : block.next; } /** * Get the branch for a particular C-shaped block. - * @param {?string} id ID for block to get the branch for. - * @param {?number} branchNum Which branch to select (e.g. for if-else). - * @returns {?string} ID of block in the branch. + * @param id ID for block to get the branch for. + * @param branchNum Which branch to select (e.g. for if-else). + * @returns ID of block in the branch. */ - getBranch (id, branchNum) { + getBranch (id: string | null, branchNum: number | null) { + if (!id) return null; const block = this._blocks[id]; if (typeof block === 'undefined') return null; if (!branchNum) branchNum = 1; @@ -172,28 +186,28 @@ class Blocks { /** * Get the opcode for a particular block - * @param {?VMBlock} block The block to query + * @param block The block to query * @returns the opcode corresponding to that block */ - getOpcode (block) { + getOpcode (block: VMBlock | undefined) { return (typeof block === 'undefined') ? null : block.opcode; } /** * Get all fields and their values for a block. - * @param {?VMBlock} block The block to query. + * @param block The block to query. * @returns All fields and their values. */ - getFields (block) { + getFields (block: VMBlock | undefined) { return (typeof block === 'undefined') ? null : block.fields; } /** * Get all non-branch inputs for a block. - * @param {?VMBlock} block the block to query. - * @returns {Record} All non-branch inputs and their associated blocks. + * @param block the block to query. + * @returns All non-branch inputs and their associated blocks. */ - getInputs (block) { + getInputs (block: VMBlock | undefined) { if (typeof block === 'undefined') return null; let inputs = this._cache.inputs[block.id]; if (typeof inputs !== 'undefined') { @@ -215,19 +229,20 @@ class Blocks { /** * Get mutation data for a block. - * @param {?VMBlock} block The block to query. + * @param block The block to query. * @returns Mutation for the block. */ - getMutation (block) { + getMutation (block: VMBlock | undefined) { return (typeof block === 'undefined') ? null : block.mutation; } /** * Get the top-level script for a given block. - * @param {?string} id ID of block to query. - * @returns {?string} ID of top-level script block. + * @param id ID of block to query. + * @returns ID of top-level script block. */ - getTopLevelScript (id) { + getTopLevelScript (id?: string | null) { + if (!id) return null; let block = this._blocks[id]; if (typeof block === 'undefined') return null; while (block.parent !== null) { @@ -238,20 +253,20 @@ class Blocks { /** * Get all procedure definitions. - * @param {?boolean} globalOnly True if only get global procedures. - * @returns {Array} Procedure states. + * @param globalOnly True if only get global procedures. + * @returns Procedure states. */ - getAllProcedureDefinitions (globalOnly) { + getAllProcedureDefinitions (globalOnly: boolean | null): ProcedureMutation[] { const procedures = []; for (const id in this._blocks) { if (!Object.prototype.hasOwnProperty.call(this._blocks, id)) continue; const block = this._blocks[id]; if (block.opcode === 'procedures_definition') { const internal = this._getCustomBlockInternal(block); - if (internal && (!globalOnly || internal.mutation.global)) { - this._cache.procedureDefinitions[internal.mutation.proccode] = id; // The outer define block id + if (internal && (!globalOnly || internal.mutation!.global)) { + this._cache.procedureDefinitions[internal.mutation!.proccode!] = id; // The outer define block id - const mutation = internal.mutation; + const mutation = internal.mutation!; procedures.push({ proccode: mutation.proccode, @@ -271,16 +286,16 @@ class Blocks { /** * Get the procedure definition for a given name. - * @param {?string} name Name of procedure to query. - * @param {?boolean} [globalOnly] True if only find global procedures. - * @returns {?string} ID of procedure definition. + * @param name Name of procedure to query. + * @param globalOnly True if only find global procedures. + * @returns ID of procedure definition. */ - getProcedureDefinition (name, globalOnly) { + getProcedureDefinition (name: string, globalOnly?: boolean) { const blockID = this._cache.procedureDefinitions[name]; if (typeof blockID !== 'undefined') { if (blockID) { - const internal = blockID && this._getCustomBlockInternal(this._blocks[blockID]); - if (!globalOnly || internal.mutation.global) { + const internal = this._getCustomBlockInternal(this._blocks[blockID]); + if (!globalOnly || internal?.mutation?.global) { return blockID; } } @@ -293,10 +308,10 @@ class Blocks { const block = this._blocks[id]; if (block.opcode === 'procedures_definition') { const internal = this._getCustomBlockInternal(block); - if (internal && internal.mutation.proccode === name) { + if (internal && internal.mutation!.proccode === name) { this._cache.procedureDefinitions[name] = id; // The outer define block id // suppose procedure proccode is unique in one target - if (!globalOnly || internal.mutation.global) { + if (!globalOnly || internal.mutation!.global) { return id; } return null; @@ -310,19 +325,19 @@ class Blocks { /** * Get names and ids of parameters for the given procedure. - * @param {?string} name Name of procedure to query. - * @returns {?Array.} List of param names for a procedure. + * @param name Name of procedure to query. + * @returns List of param names for a procedure. */ - getProcedureParamNamesAndIds (name) { - return this.getProcedureParamNamesIdsAndDefaults(name).slice(0, 2); + getProcedureParamNamesAndIds (name: string) { + return this.getProcedureParamNamesIdsAndDefaults(name)?.slice(0, 2) ?? null; } /** * Get names, ids, and defaults of parameters for the given procedure. - * @param {?string} name Name of procedure to query. - * @returns {?Array.} List of param names for a procedure. + * @param name Name of procedure to query. + * @returns List of param names for a procedure. */ - getProcedureParamNamesIdsAndDefaults (name) { + getProcedureParamNamesIdsAndDefaults (name: string) { const cachedNames = this._cache.procedureParamNames[name]; if (typeof cachedNames !== 'undefined') { return cachedNames; @@ -332,10 +347,10 @@ class Blocks { if (!Object.prototype.hasOwnProperty.call(this._blocks, id)) continue; const block = this._blocks[id]; if (block.opcode === 'procedures_prototype' && - block.mutation.proccode === name) { - const names = block.mutation.argumentnames; - const ids = block.mutation.argumentids; - const defaults = block.mutation.argumentdefaults; + block.mutation!.proccode === name) { + const names = block.mutation!.argumentnames!; + const ids = block.mutation!.argumentids!; + const defaults = block.mutation!.argumentdefaults!; this._cache.procedureParamNames[name] = [names, ids, defaults]; return this._cache.procedureParamNames[name]; @@ -358,15 +373,15 @@ class Blocks { * Create event listener for blocks, variables, and comments. Handles validation and * serves as a generic adapter between the blocks, variables, and the * runtime interface. - * @param {ClipCCBlock.Events.Abstract} e Blockly "block" or "variable" event + * @param event Blockly "block" or "variable" event */ - blocklyListen (e) { + blocklyListen (event: ListenableBlocklyEvents) { // Validate event - if (typeof e !== 'object') return; + if (typeof event !== 'object') return; if ( - typeof e.blockId !== 'string' && - typeof e.varId !== 'string' && - typeof e.commentId !== 'string' + typeof (event as ClipCCBlock.Events.BlockBase).blockId !== 'string' && + typeof (event as ClipCCBlock.Events.VarBase).varId !== 'string' && + typeof (event as ClipCCBlock.Events.CommentBase).commentId !== 'string' ) { return; } @@ -374,15 +389,15 @@ class Blocks { const editingTarget = this.runtime.getEditingTarget(); // Block create/update/destroy - switch (e.type) { + switch (event.type) { case 'create': { - const newBlocks = adapter(e); - /** @type {Record = {}; // A create event can create many blocks. Add them all. for (const block of newBlocks) { if (Object.prototype.hasOwnProperty.call(block, 'commentData')) { - comments[block.id] = block.commentData; + comments[block.id] = block.commentData!; delete block.commentData; } this.createBlock(block); @@ -391,7 +406,7 @@ class Blocks { const currTarget = this.runtime.getEditingTarget(); for (const blockId in comments) { const commentData = comments[blockId]; - currTarget.createComment( + currTarget?.createComment( commentData.id, blockId, commentData.text ?? '', @@ -405,23 +420,27 @@ class Blocks { } break; } - case 'change': + case 'change': { + const e = event as ClipCCBlock.Events.BlockChange; if (e.element === 'comment') { const commentId = e.name; if (!commentId) break; - this.changeCommentText(commentId, e.newValue); + this.changeCommentText(commentId, e.newValue as string); + this.emitProjectChanged(); break; } this.changeBlock({ - id: e.blockId, - element: e.element, - name: e.name, + id: e.blockId!, + element: e.element!, + name: e.name!, value: e.newValue }); break; - case 'move': + } + case 'move': { + const e = event as ClipCCBlock.Events.BlockMove; this.moveBlock({ - id: e.blockId, + id: e.blockId!, oldParent: e.oldParentId, oldInput: e.oldInputName, newParent: e.newParentId, @@ -429,32 +448,40 @@ class Blocks { newCoordinate: e.newCoordinate }); break; - case 'block_drag_outside': + } + case 'block_drag_outside': { + const e = event as ClipCCBlock.BlockDragOutside; this.runtime.emitBlockDragUpdate(e.isOutside); break; - case 'block_drag_end': + } + case 'block_drag_end': { + const e = event as ClipCCBlock.BlockDragEnd; this.runtime.emitBlockDragUpdate(false /* areBlocksOverGui */); // Drag blocks onto another sprite if (e.isOutside) { - const newBlocks = adapter(e); - this.runtime.emitBlockEndDrag(newBlocks, e.blockId); + const newBlocks = adapter(e)!; + this.runtime.emitBlockEndDrag(newBlocks, e.blockId!); } break; - case 'delete': + } + case 'delete': { + const e = event as ClipCCBlock.Events.BlockDelete; // Don't accept delete events for missing blocks, // or shadow blocks being obscured. - if (!Object.prototype.hasOwnProperty.call(this._blocks, e.blockId) || - this._blocks[e.blockId].shadow) { + if (!Object.prototype.hasOwnProperty.call(this._blocks, e.blockId!) || + this._blocks[e.blockId!].shadow) { return; } // Inform any runtime to forget about glows on this script. - if (this._blocks[e.blockId].topLevel) { - this.runtime.quietGlow(e.blockId); + if (this._blocks[e.blockId!].topLevel) { + this.runtime.quietGlow(e.blockId!); } - this.deleteBlock(e.blockId); + this.deleteBlock(e.blockId!); break; - case 'var_create': + } + case 'var_create': { + const e = event as ClipCCBlock.VarCreate; // Check if the variable being created is global or local // If local, create a local var on the current editing target, as long // as there are no conflicts, and the current target is actually a sprite @@ -463,130 +490,144 @@ class Blocks { // create a stage (global) var after checking for name conflicts // on all the sprites. if (e.isLocal && editingTarget && !editingTarget.isStage && !e.isCloud) { - if (!editingTarget.lookupVariableById(e.varId)) { - editingTarget.createVariable(e.varId, e.varName, e.varType); + if (!editingTarget.lookupVariableById(e.varId!)) { + editingTarget.createVariable(e.varId!, e.varName!, e.varType!); this.emitProjectChanged(); } - } else { - if (stage.lookupVariableById(e.varId)) { + } else if (stage) { + if (stage.lookupVariableById(e.varId!)) { // Do not re-create a variable if it already exists return; } // Check for name conflicts in all of the targets const allTargets = this.runtime.targets.filter(t => t.isOriginal); for (const target of allTargets) { - if (target.lookupVariableByNameAndType(e.varName, e.varType, true)) { + if (target.lookupVariableByNameAndType(e.varName!, e.varType!, true)) { return; } } - stage.createVariable(e.varId, e.varName, e.varType, e.isCloud); + stage.createVariable(e.varId!, e.varName!, e.varType!, e.isCloud); this.emitProjectChanged(); } break; - case 'var_rename': - if (editingTarget && Object.prototype.hasOwnProperty.call(editingTarget.variables, e.varId)) { + } + case 'var_rename': { + const e = event as ClipCCBlock.Events.VarRename; + if (editingTarget && Object.prototype.hasOwnProperty.call(editingTarget.variables, e.varId!)) { // This is a local variable, rename on the current target - editingTarget.renameVariable(e.varId, e.newName); + editingTarget.renameVariable(e.varId!, e.newName!); // Update all the blocks on the current target that use // this variable - editingTarget.blocks.updateBlocksAfterVarRename(e.varId, e.newName); - } else { + editingTarget.blocks.updateBlocksAfterVarRename(e.varId!, e.newName!); + } else if (stage) { // This is a global variable - stage.renameVariable(e.varId, e.newName); + stage.renameVariable(e.varId!, e.newName!); // Update all blocks on all targets that use the renamed variable const targets = this.runtime.targets; for (let i = 0; i < targets.length; i++) { const currTarget = targets[i]; - currTarget.blocks.updateBlocksAfterVarRename(e.varId, e.newName); + currTarget.blocks.updateBlocksAfterVarRename(e.varId!, e.newName!); } } this.emitProjectChanged(); break; + } case 'var_delete': { - const target = (editingTarget && Object.prototype.hasOwnProperty.call(editingTarget.variables, e.varId)) ? + const e = event as ClipCCBlock.VarDelete; + const target = (editingTarget && Object.prototype.hasOwnProperty.call(editingTarget.variables, e.varId!)) ? editingTarget : stage; - target.deleteVariable(e.varId); + if (!target) break; + target.deleteVariable(e.varId!); this.emitProjectChanged(); break; } case 'block_comment_create': - case 'comment_create': + case 'comment_create': { + const e = event as ClipCCBlock.BlockCommentCreate | ClipCCBlock.Events.CommentCreate; if (this.runtime.getEditingTarget()) { - const currTarget = this.runtime.getEditingTarget(); + const currTarget = this.runtime.getEditingTarget()!; currTarget.createComment( - e.commentId, - e.blockId, + e.commentId!, + (e as ClipCCBlock.BlockCommentCreate).blockId, '', - e.x ?? 0, - e.y ?? 0, - e.width ?? 200, - e.height ?? 200, - false + (e as ClipCCBlock.Events.CommentCreate).json?.x ?? 0, + (e as ClipCCBlock.Events.CommentCreate).json?.y ?? 0, + (e as ClipCCBlock.Events.CommentCreate).json?.width ?? 200, + (e as ClipCCBlock.Events.CommentCreate).json?.height ?? 200, + (e as ClipCCBlock.Events.CommentCreate).json?.collapsed ?? false ); - if (currTarget.comments[e.commentId].x === null && - currTarget.comments[e.commentId].y === null) { + if (currTarget.comments[e.commentId!].x === null && + currTarget.comments[e.commentId!].y === null) { // Block comments imported from 2.0 projects are imported with their // x and y coordinates set to null so that scratch-blocks can // auto-position them. If we are receiving a create event for these // comments, then the auto positioning should have taken place. // Update the x and y position of these comments to match the // one from the event. - currTarget.comments[e.commentId].x = e.x; - currTarget.comments[e.commentId].y = e.y; + currTarget.comments[e.commentId!].x = (e as ClipCCBlock.Events.CommentCreate).json?.x ?? 0; + currTarget.comments[e.commentId!].y = (e as ClipCCBlock.Events.CommentCreate).json?.y ?? 0; } } this.emitProjectChanged(); break; - case 'comment_change': - this.changeCommentText(e.commentId, e.newContents_); + } + case 'comment_change': { + const e = event as ClipCCBlock.Events.CommentChange; + this.changeCommentText(e.commentId!, e.newContents_); break; + } case 'block_comment_move': - case 'comment_move': + case 'comment_move': { + const e = event as ClipCCBlock.BlockCommentMove | ClipCCBlock.Events.CommentMove; if (this.runtime.getEditingTarget()) { const currTarget = this.runtime.getEditingTarget(); - if (currTarget && !Object.prototype.hasOwnProperty.call(currTarget.comments, e.commentId)) { + if (currTarget && !Object.prototype.hasOwnProperty.call(currTarget.comments, e.commentId!)) { log.warn(`Cannot change comment with id ${e.commentId} because it does not exist.`); return; } - const comment = currTarget.comments[e.commentId]; + const comment = currTarget!.comments[e.commentId!]; const newCoord = e.newCoordinate_; - comment.x = newCoord.x; - comment.y = newCoord.y; + comment.x = newCoord!.x; + comment.y = newCoord!.y; this.emitProjectChanged(); } break; + } case 'block_comment_collapse': - case 'comment_collapse': + case 'comment_collapse': { + const e = event as ClipCCBlock.BlockCommentCollapse | ClipCCBlock.Events.CommentCollapse; if (this.runtime.getEditingTarget()) { const currTarget = this.runtime.getEditingTarget(); if ( currTarget && !Object.prototype.hasOwnProperty.call( currTarget.comments, - e.commentId + e.commentId! ) ) { log.warn( - `Cannot collapse comment with id ${e.commentId} because it does not exist.` + `Cannot collapse comment with id ${e.commentId!} because it does not exist.` ); return; } - const comment = currTarget.comments[e.commentId]; - comment.minimized = e.newCollapsed; + const comment = currTarget!.comments[e.commentId!]; + comment.minimized = !!e.newCollapsed; this.emitProjectChanged(); } break; + } case 'block_comment_resize': - case 'comment_resize': + case 'comment_resize': { + const e = event as ClipCCBlock.BlockCommentResize | ClipCCBlock.Events.CommentResize; if (this.runtime.getEditingTarget()) { const currTarget = this.runtime.getEditingTarget(); if ( currTarget && !Object.prototype.hasOwnProperty.call( currTarget.comments, - e.commentId + e.commentId! ) ) { log.warn( @@ -594,26 +635,28 @@ class Blocks { ); return; } - const comment = currTarget.comments[e.commentId]; - comment.width = e.newSize.width; - comment.height = e.newSize.height; + const comment = currTarget!.comments[e.commentId!]; + comment.width = e.newSize!.width; + comment.height = e.newSize!.height; this.emitProjectChanged(); } break; + } case 'block_comment_delete': - case 'comment_delete': + case 'comment_delete': { + const e = event as ClipCCBlock.BlockCommentDelete | ClipCCBlock.Events.CommentDelete; if (this.runtime.getEditingTarget()) { const currTarget = this.runtime.getEditingTarget(); - if (!Object.prototype.hasOwnProperty.call(currTarget.comments, e.commentId)) { + if (!Object.prototype.hasOwnProperty.call(currTarget!.comments, e.commentId!)) { // If we're in this state, we have probably received // a delete event from a workspace that we switched from // (e.g. a delete event for a comment on sprite a's workspace // when switching from sprite a to sprite b) return; } - delete currTarget.comments[e.commentId]; - if (e.blockId) { - const block = currTarget.blocks.getBlock(e.blockId); + delete currTarget!.comments[e.commentId!]; + if ('blockId' in e) { + const block = currTarget!.blocks.getBlock(e.blockId); if (!block) { log.warn(`Could not find block referenced by comment with id: ${e.commentId}`); return; @@ -624,29 +667,36 @@ class Blocks { this.emitProjectChanged(); } break; + } case 'func_change': { + const e = event as ClipCCBlock.FuncChange; const {oldExtraState, newExtraState} = e; - const procCode = oldExtraState.proccode; + const procCode = oldExtraState?.proccode; + if (!procCode) break; if (oldExtraState.global) { for (const target of this.runtime.targets) { - target.blocks.updateBlocksAfterFuncUpdate(procCode, newExtraState); + target.blocks.updateBlocksAfterFuncUpdate(procCode, newExtraState!); } } else { - editingTarget.blocks.updateBlocksAfterFuncUpdate(procCode, newExtraState); + editingTarget?.blocks.updateBlocksAfterFuncUpdate(procCode, newExtraState!); } this.emitProjectChanged(); break; } - case 'click': + case 'click': { + const e = event as ClipCCBlock.Events.Click; // UI event: clicked scripts toggle in the runtime. if (e.targetType === 'block') { + const topBlockId = this.getTopLevelScript(e.blockId); + if (!topBlockId) break; this.runtime.toggleScript( - this.getTopLevelScript(e.blockId), + topBlockId, {stackClick: true} ); } break; } + } } // --------------------------------------------------------------------- @@ -670,9 +720,9 @@ class Blocks { /** * Block management: create blocks and scripts from a `create` event - * @param {!object} block Blockly create event to be processed + * @param block Blockly create event to be processed */ - createBlock (block) { + createBlock (block: VMBlock) { // Does the block already exist? // Could happen, e.g., for an unobscured shadow. if (Object.prototype.hasOwnProperty.call(this._blocks, block.id)) { @@ -696,9 +746,19 @@ class Blocks { /** * Block management: change block field values - * @param {!object} args Blockly change event to be processed + * @param args Blockly change event to be processed + * @param args.id The ID of the block associated with this event. + * @param args.element The element that changed; + * one of 'field', 'comment', 'collapsed', 'disabled', 'inline', or 'mutation' + * @param args.name The name of the field that changed, if this is a change to a field. + * @param args.value The new value of the element. */ - changeBlock (args) { + changeBlock (args: { + id: string, + element: string, + name?: string, + value: unknown + }) { // Validate if (['field', 'mutation', 'checkbox'].indexOf(args.element) === -1) return; let block = this._blocks[args.id]; @@ -716,27 +776,27 @@ class Blocks { // Update block value - if (!block.fields[args.name]) return; + if (!block.fields[args.name!]) return; if (args.name === 'VARIABLE' || args.name === 'LIST' || args.name === 'BROADCAST_OPTION') { // Get variable name using the id in args.value. - const variable = this.runtime.getEditingTarget().lookupVariableById(args.value); + const variable = this.runtime.getEditingTarget()?.lookupVariableById(args.value as string); if (variable) { block.fields[args.name].value = variable.name; - block.fields[args.name].id = args.value; + block.fields[args.name].id = args.value as string; } } else { // Changing the value in a dropdown - block.fields[args.name].value = args.value; + block.fields[args.name!].value = args.value as string; // The selected item in the sensing of block menu needs to change based on the // selected target. Set it to the first item in the menu list. // TODO: (#1787) if (block.opcode === 'sensing_of_object_menu') { if (block.fields.OBJECT.value === '_stage_') { - this._blocks[block.parent].fields.PROPERTY.value = 'backdrop #'; + this._blocks[block.parent!].fields.PROPERTY.value = 'backdrop #'; } else { - this._blocks[block.parent].fields.PROPERTY.value = 'x position'; + this._blocks[block.parent!].fields.PROPERTY.value = 'x position'; } this.runtime.requestBlocksUpdate(); } @@ -751,7 +811,7 @@ class Blocks { } break; case 'mutation': - block.mutation = JSON.parse(args.value); + block.mutation = JSON.parse(args.value as string); break; case 'checkbox': { // A checkbox usually has a one to one correspondence with the monitor @@ -770,22 +830,22 @@ class Blocks { let newBlock = this.runtime.monitorBlocks.getBlock(newId); if (!newBlock) { newBlock = JSON.parse(JSON.stringify(block)); - newBlock.id = newId; - this.runtime.monitorBlocks.createBlock(newBlock); + newBlock!.id = newId; + this.runtime.monitorBlocks.createBlock(newBlock!); } - block = newBlock; // Carry on through the rest of this code with newBlock + block = newBlock!; // Carry on through the rest of this code with newBlock } const wasMonitored = block.isMonitored; - block.isMonitored = args.value; + block.isMonitored = args.value as boolean; // Variable blocks may be sprite specific depending on the owner of the variable let isSpriteLocalVariable = false; if (block.opcode === 'data_variable') { - isSpriteLocalVariable = !(this.runtime.getTargetForStage().variables[block.fields.VARIABLE.id]); + isSpriteLocalVariable = !(this.runtime.getTargetForStage()?.variables[block.fields.VARIABLE.id!]); } else if (block.opcode === 'data_listcontents') { - isSpriteLocalVariable = !(this.runtime.getTargetForStage().variables[block.fields.LIST.id]); + isSpriteLocalVariable = !(this.runtime.getTargetForStage()?.variables[block.fields.LIST.id!]); } const isSpriteSpecific = isSpriteLocalVariable || @@ -795,7 +855,7 @@ class Blocks { // If creating a new sprite specific monitor, the only possible target is // the current editing one b/c you cannot dynamically create monitors. // Also, do not change the targetId if it has already been assigned - block.targetId = block.targetId || this.runtime.getEditingTarget().id; + block.targetId = block.targetId || this.runtime.getEditingTarget()?.id; } else { block.targetId = null; } @@ -808,7 +868,9 @@ class Blocks { this.runtime.requestAddMonitor(MonitorRecord({ id: block.id, targetId: block.targetId, - spriteName: block.targetId ? this.runtime.getTargetById(block.targetId).getName() : null, + spriteName: block.targetId ? + this.runtime.getTargetById(block.targetId)?.getName() ?? null : + null, opcode: block.opcode, params: this._getBlockParams(block), // @todo(vm#565) for numerical values with decimals, some countries use comma @@ -828,9 +890,23 @@ class Blocks { /** * Block management: move blocks from parent to parent - * @param {!object} e Blockly move event to be processed + * @param e Blockly move event to be processed + * @param e.id The ID of the block associated with this event. + * @param e.oldParent The ID of the old parent block. Undefined if it was a top-level block. + * @param e.oldInput The name of the old input. Undefined if it was a top-level block or the parent's next block. + * @param e.newParent The ID of the new parent block. Undefined if it is a top-level block. + * @param e.newInput The name of the new input. Undefined if it is a top-level block or the parent's next block. + * @param e.newCoordinate The new X and Y workspace coordinates of the block if it is a top-level block. + * Undefined if it is not a top level block. */ - moveBlock (e) { + moveBlock (e: { + id: string, + oldParent?: string, + oldInput?: string, + newParent?: string, + newInput?: string, + newCoordinate?: ClipCCBlock.utils.Coordinate + }) { if (!Object.prototype.hasOwnProperty.call(this._blocks, e.id)) { return; } @@ -917,14 +993,14 @@ class Blocks { /** * Block management: run all blocks. - * @param {!object} runtime Runtime to run all blocks in. + * @param runtime Runtime to run all blocks in. */ - runAllMonitored (runtime) { + runAllMonitored (runtime: Runtime) { if (this._cache._monitored === null) { this._cache._monitored = Object.keys(this._blocks) - .filter(blockId => this.getBlock(blockId).isMonitored) + .filter(blockId => this.getBlock(blockId)?.isMonitored) .map(blockId => { - const targetId = this.getBlock(blockId).targetId; + const targetId = this.getBlock(blockId)!.targetId; return { blockId, target: targetId ? runtime.getTargetById(targetId) : null @@ -942,9 +1018,9 @@ class Blocks { /** * Block management: delete blocks and their associated scripts. Does nothing if a block * with the given ID does not exist. - * @param {!string} blockId Id of block to delete + * @param blockId Id of block to delete */ - deleteBlock (blockId) { + deleteBlock (blockId: string) { // @todo In runtime, stop threads running on this script. // Get block @@ -992,10 +1068,13 @@ class Blocks { /** * Change comment text based on id and text. - * @param {string} commentId Id of comment to change - * @param {string} newText New text for comment + * @param commentId Id of comment to change + * @param newText New text for comment */ - changeCommentText (commentId, newText) { + changeCommentText (commentId: string, newText: string | undefined) { + // if newText is undefined, it's indicates that the comment is being deleted + // it will be handled by `block_comment_delete` event, so we can ignore it here. + if (!newText) return; const currTarget = this.runtime.getEditingTarget(); if (!currTarget) return; if (!Object.prototype.hasOwnProperty.call(currTarget.comments, commentId)) { @@ -1010,15 +1089,15 @@ class Blocks { /** * Returns a map of all references to variables or lists from blocks * in this block container. - * @param {Array | null} optBlocks Optional list of blocks to constrain the search to. + * @param optBlocks Optional list of blocks to constrain the search to. * This is useful for getting variable/list references for a stack of blocks instead * of all blocks on the workspace - * @param {boolean=} optIncludeBroadcast Optional whether to include broadcast fields. + * @param optIncludeBroadcast Optional whether to include broadcast fields. * @returns A map of variable ID to a list of all variable references * for that ID. A variable reference contains the field referencing that variable * and also the type of the variable being referenced. */ - getAllVariableAndListReferences (optBlocks, optIncludeBroadcast) { + getAllVariableAndListReferences (optBlocks?: Record | null, optIncludeBroadcast?: boolean) { const blocks = optBlocks ? optBlocks : this._blocks; const allReferences = Object.create(null); for (const blockId in blocks) { @@ -1036,13 +1115,13 @@ class Blocks { } if (varOrListField) { const currVarId = varOrListField.id; - if (allReferences[currVarId]) { - allReferences[currVarId].push({ + if (allReferences[currVarId!]) { + allReferences[currVarId!].push({ referencingField: varOrListField, type: varType }); } else { - allReferences[currVarId] = [{ + allReferences[currVarId!] = [{ referencingField: varOrListField, type: varType }]; @@ -1054,10 +1133,10 @@ class Blocks { /** * Keep blocks up to date after a variable gets renamed. - * @param {string} varId The id of the variable that was renamed - * @param {string} newName The new name of the variable that was renamed + * @param varId The id of the variable that was renamed + * @param newName The new name of the variable that was renamed */ - updateBlocksAfterVarRename (varId, newName) { + updateBlocksAfterVarRename (varId: string, newName: string) { const blocks = this._blocks; for (const blockId in blocks) { let varOrListField = null; @@ -1077,15 +1156,18 @@ class Blocks { /** * Keep blocks up to date after a procedure gets updated. - * @param {string} procCode The procCode of procedure to update - * @param {object} newExtraState The new extra state of procedure + * @param procCode The procCode of procedure to update + * @param newExtraState The new extra state of procedure */ - updateBlocksAfterFuncUpdate (procCode, newExtraState) { + updateBlocksAfterFuncUpdate ( + procCode: string, + newExtraState: ClipCCBlock.proceduresSerializer.ProcedureExtraState + ) { const blocks = this._blocks; for (const blockId in blocks) { const block = blocks[blockId]; if (block.opcode === 'procedures_prototype') { - if (block.mutation.proccode === procCode) { + if (block.mutation?.proccode === procCode) { block.mutation.proccode = newExtraState.proccode; block.mutation.argumentids = newExtraState.argumentids; block.mutation.argumentnames = newExtraState.argumentnames; @@ -1095,7 +1177,7 @@ class Blocks { block.mutation.return = newExtraState.return; } } else if (block.opcode === 'procedures_call') { - if (block.mutation.proccode === procCode) { + if (block.mutation?.proccode === procCode) { block.mutation.proccode = newExtraState.proccode; block.mutation.argumentids = newExtraState.argumentids; block.mutation.warp = newExtraState.warp; @@ -1109,9 +1191,9 @@ class Blocks { /** * Keep blocks up to date after they are shared between targets. - * @param {boolean} isStage If the new target is a stage. + * @param isStage If the new target is a stage. */ - updateTargetSpecificBlocks (isStage) { + updateTargetSpecificBlocks (isStage?: boolean) { const blocks = this._blocks; for (const blockId in blocks) { if (isStage && blocks[blockId].opcode === 'event_whenthisspriteclicked') { @@ -1126,13 +1208,13 @@ class Blocks { * Update blocks after a sound, costume, or backdrop gets renamed. * Any block referring to the old name of the asset should get updated * to refer to the new name. - * @param {string} oldName The old name of the asset that was renamed. - * @param {string} newName The new name of the asset that was renamed. - * @param {string} assetType String representation of the kind of asset + * @param oldName The old name of the asset that was renamed. + * @param newName The new name of the asset that was renamed. + * @param assetType String representation of the kind of asset * that was renamed. This can be one of 'sprite','costume', 'sound', or * 'backdrop'. */ - updateAssetName (oldName, newName, assetType) { + updateAssetName (oldName: string, newName: string, assetType: string) { let getAssetField; if (assetType === 'costume') { getAssetField = this._getCostumeField.bind(this); @@ -1156,12 +1238,12 @@ class Blocks { /** * Update sensing_of blocks after a variable gets renamed. - * @param {string} oldName The old name of the variable that was renamed. - * @param {string} newName The new name of the variable that was renamed. - * @param {string} targetName The name of the target the variable belongs to. - * @returns {boolean} Returns true if any of the blocks were updated. + * @param oldName The old name of the variable that was renamed. + * @param newName The new name of the variable that was renamed. + * @param targetName The name of the target the variable belongs to. + * @returns Returns true if any of the blocks were updated. */ - updateSensingOfReference (oldName, newName, targetName) { + updateSensingOfReference (oldName: string, newName: string, targetName: string) { const blocks = this._blocks; let blockUpdated = false; for (const blockId in blocks) { @@ -1171,7 +1253,7 @@ class Blocks { // If block and shadow are different, it means a block is inserted to OBJECT, and should be ignored. block.inputs.OBJECT.block === block.inputs.OBJECT.shadow) { const inputBlock = this.getBlock(block.inputs.OBJECT.block); - if (inputBlock.fields.OBJECT.value === targetName) { + if (inputBlock?.fields.OBJECT.value === targetName) { block.fields.PROPERTY.value = newName; blockUpdated = true; } @@ -1183,12 +1265,12 @@ class Blocks { /** * Helper function to retrieve a costume menu field from a block given its id. - * @param {string} blockId A unique identifier for a block - * @returns {?object} The costume menu field of the block with the given block id. + * @param blockId A unique identifier for a block + * @returns The costume menu field of the block with the given block id. * Null if either a block with the given id doesn't exist or if a costume menu field * does not exist on the block with the given id. */ - _getCostumeField (blockId) { + protected _getCostumeField (blockId: string) { const block = this.getBlock(blockId); if (block && Object.prototype.hasOwnProperty.call(block.fields, 'COSTUME')) { return block.fields.COSTUME; @@ -1198,12 +1280,12 @@ class Blocks { /** * Helper function to retrieve a sound menu field from a block given its id. - * @param {string} blockId A unique identifier for a block - * @returns {?object} The sound menu field of the block with the given block id. + * @param blockId A unique identifier for a block + * @returns The sound menu field of the block with the given block id. * Null, if either a block with the given id doesn't exist or if a sound menu field * does not exist on the block with the given id. */ - _getSoundField (blockId) { + protected _getSoundField (blockId: string) { const block = this.getBlock(blockId); if (block && Object.prototype.hasOwnProperty.call(block.fields, 'SOUND_MENU')) { return block.fields.SOUND_MENU; @@ -1213,12 +1295,12 @@ class Blocks { /** * Helper function to retrieve a backdrop menu field from a block given its id. - * @param {string} blockId A unique identifier for a block - * @returns {?object} The backdrop menu field of the block with the given block id. + * @param blockId A unique identifier for a block + * @returns The backdrop menu field of the block with the given block id. * Null, if either a block with the given id doesn't exist or if a backdrop menu field * does not exist on the block with the given id. */ - _getBackdropField (blockId) { + protected _getBackdropField (blockId: string) { const block = this.getBlock(blockId); if (block && Object.prototype.hasOwnProperty.call(block.fields, 'BACKDROP')) { return block.fields.BACKDROP; @@ -1228,12 +1310,12 @@ class Blocks { /** * Helper function to retrieve a sprite menu field from a block given its id. - * @param {string} blockId A unique identifier for a block - * @returns {?object} The sprite menu field of the block with the given block id. + * @param blockId A unique identifier for a block + * @returns The sprite menu field of the block with the given block id. * Null, if either a block with the given id doesn't exist or if a sprite menu field * does not exist on the block with the given id. */ - _getSpriteField (blockId) { + protected _getSpriteField (blockId: string) { const block = this.getBlock(blockId); if (!block) { return null; @@ -1254,21 +1336,22 @@ class Blocks { /** * Encode all of `this._blocks` as an XML string usable * by a Blockly/scratch-blocks workspace. - * @param {Record} comments Map of comments referenced by id - * @returns {string} String of XML representing this object's blocks. + * @param comments Map of comments referenced by id + * @deprecated Use `toState` instead. + * @returns String of XML representing this object's blocks. */ - toXML (comments) { + toXML (comments?: Record) { return this._scripts.map(script => this.blockToXML(script, comments)).join(); } /** * Recursively encode an individual block and its children * into a Blockly/scratch-blocks XML string. - * @param {!string} blockId ID of block to encode. - * @param {Record} comments Map of comments referenced by id - * @returns {string} String of XML representing this block and any children. + * @param blockId ID of block to encode. + * @param comments Map of comments referenced by id + * @returns String of XML representing this block and any children. */ - blockToXML (blockId, comments) { + blockToXML (blockId: string, comments?: Record) { const block = this._blocks[blockId]; // block should exist, but currently some blocks' next property point // to a blockId for non-existent blocks. Until we track down that behavior, @@ -1337,7 +1420,7 @@ class Blocks { } let value = blockField.value; if (typeof value === 'string') { - value = xmlEscape(blockField.value); + value = xmlEscape(blockField.value!); } xmlString += `>${value}`; } @@ -1352,30 +1435,30 @@ class Blocks { /** * Encode all of `this._blocks` as a JSON array usable * by a Blockly/scratch-blocks workspace. - * @param {Record} comments Map of comments referenced by id - * @returns {Array} JSON array representing this object's blocks. + * @param comments Map of comments referenced by id + * @returns JSON array representing this object's blocks. */ - toState (comments) { + toState (comments?: Record){ return this._scripts .map(script => this.blockToState(script, comments)) - .filter(script => script); // Filter out nulls + .filter((script): script is ClipCCBlock.serialization.blocks.State => !!script); // Filter out nulls } /** * Recursively encode an individual block and its children * into a Blockly/scratch-blocks JSON object. - * @param {!string} blockId ID of block to encode. - * @param {Record} comments Map of comments referenced by id - * @returns {object} JSON object representing this block and any children. + * @param blockId ID of block to encode. + * @param comments Map of comments referenced by id + * @returns JSON object representing this block and any children. */ - blockToState (blockId, comments) { + blockToState (blockId: string, comments?: Record) { const block = this._blocks[blockId]; // block should exist, but currently some blocks' next property point // to a blockId for non-existent blocks. Until we track down that behavior, // this early exit allows the project to load. if (!block) return; - const state = { + const state: ClipCCBlock.serialization.blocks.State = { id: block.id, type: block.opcode }; @@ -1427,7 +1510,7 @@ class Blocks { const blockInput = block.inputs[input]; if (blockInput.block || blockInput.shadow) { if (!state.inputs) state.inputs = {}; - const inputState = {}; + const inputState: ClipCCBlock.serialization.blocks.ConnectionState = {}; if (blockInput.block) { if (blockInput.block === blockInput.shadow) { inputState.shadow = this.blockToState(blockInput.block, comments); @@ -1437,7 +1520,7 @@ class Blocks { inputState.shadow = this.blockToState(blockInput.shadow, comments); } } - } else { + } else if (blockInput.shadow) { inputState.shadow = this.blockToState(blockInput.shadow, comments); } state.inputs[blockInput.name] = inputState; @@ -1476,10 +1559,11 @@ class Blocks { /** * Recursively encode a mutation object to XML. - * @param {!object} mutation Object representing a mutation. - * @returns {string} XML string representing a mutation. + * @param mutation Object representing a mutation. + * @deprecated Use `blockToState` instead and include the mutation in the `extraState` property. + * @returns XML string representing a mutation. */ - mutationToXML (mutation) { + mutationToXML (mutation: VMMutation) { let mutationString = `<${mutation.tagName}`; for (const prop in mutation) { if (prop === 'children' || prop === 'tagName') continue; @@ -1494,8 +1578,10 @@ class Blocks { mutationString += ` ${prop}="${mutationValue}"`; } mutationString += '>'; - for (let i = 0; i < mutation.children.length; i++) { - mutationString += this.mutationToXML(mutation.children[i]); + if (mutation.children) { + for (let i = 0; i < mutation.children.length; i++) { + mutationString += this.mutationToXML(mutation.children[i]); + } } mutationString += ``; return mutationString; @@ -1504,16 +1590,17 @@ class Blocks { // --------------------------------------------------------------------- /** * Helper to serialize block fields and input fields for reporting new monitors - * @param {!object} block Block to be paramified. - * @returns {!object} object of param key/values. + * @param block Block to be paramified. + * @returns object of param key/values. */ - _getBlockParams (block) { - const params = {}; + _getBlockParams (block: VMBlock) { + const params: Record = {}; for (const key in block.fields) { params[key] = block.fields[key].value; } for (const inputKey in block.inputs) { - const inputBlock = this._blocks[block.inputs[inputKey].block]; + const inputBlock = this._blocks[block.inputs[inputKey].block!]; + if (!inputBlock) continue; for (const key in inputBlock.fields) { params[key] = inputBlock.fields[key].value; } @@ -1523,20 +1610,20 @@ class Blocks { /** * Helper to get the corresponding internal procedure definition block - * @param {!object} defineBlock Outer define block. - * @returns {!object} internal definition block which has the mutation. + * @param defineBlock Outer define block. + * @returns internal definition block which has the mutation. */ - _getCustomBlockInternal (defineBlock) { - if (defineBlock.inputs && defineBlock.inputs.custom_block) { + protected _getCustomBlockInternal (defineBlock: VMBlock) { + if (defineBlock.inputs?.custom_block?.block) { return this._blocks[defineBlock.inputs.custom_block.block]; } } /** * Helper to add a stack to `this._scripts`. - * @param {?string} topBlockId ID of block that starts the script. + * @param topBlockId ID of block that starts the script. */ - _addScript (topBlockId) { + protected _addScript (topBlockId: string) { const i = this._scripts.indexOf(topBlockId); if (i > -1) return; // Already in scripts. this._scripts.push(topBlockId); @@ -1546,9 +1633,9 @@ class Blocks { /** * Helper to remove a script from `this._scripts`. - * @param {?string} topBlockId ID of block that starts the script. + * @param topBlockId ID of block that starts the script. */ - _deleteScript (topBlockId) { + protected _deleteScript (topBlockId: string) { const i = this._scripts.indexOf(topBlockId); if (i > -1) this._scripts.splice(i, 1); // Update `topLevel` property on the top block. @@ -1557,10 +1644,10 @@ class Blocks { /** * Get dangling inputs in a block. - * @param {object} block The block to check - * @returns {boolean} True if the input is dangling + * @param block The block to check + * @returns True if the input is dangling */ - _getDanglingInputs (block) { + protected _getDanglingInputs (block: VMBlock) { const danglingInputs = new Set(); // It's most possible to have dangling inputs when mutation exists, other sequences need to read the Blockly // definition to validate inputs. just skip now. diff --git a/packages/vm/src/engine/execute.js b/packages/vm/src/engine/execute.ts similarity index 69% rename from packages/vm/src/engine/execute.js rename to packages/vm/src/engine/execute.ts index e4649803b..58510b6ec 100644 --- a/packages/vm/src/engine/execute.js +++ b/packages/vm/src/engine/execute.ts @@ -1,9 +1,15 @@ import BlockUtility from './block-utility'; -import {getCached as getCachedExecuteBlock} from './blocks-execute-cache'; +import {type CachedBlockData, getCached as getCachedExecuteBlock} from './blocks-execute-cache'; import log from '../util/log'; import Thread from './thread'; import {Map} from 'immutable'; import cast from '../util/cast'; +import type Blocks from './blocks'; +import type {VMField, VMInput, VMMutation} from '../serialization/schema'; +import type Profiler from './profiler'; +import type {ProfilerFrame} from './profiler'; +import type Sequencer from './sequencer'; +import type {BlockFunction} from '../blocks/category_prototype'; /** * Single BlockUtility instance reused by execute for every pritimive ran. @@ -13,51 +19,75 @@ const blockUtility = new BlockUtility(); /** * Profiler frame name for block functions. - * @constant {string} + * @constant */ -const blockFunctionProfilerFrame = 'blockFunction'; +const blockFunctionProfilerFrame = 'blockFunction' as const; /** * Profiler frame ID for 'blockFunction'. - * @type {number} */ let blockFunctionProfilerId = -1; +/* eslint-disable @typescript-eslint/no-explicit-any */ /** * Utility function to determine if a value is a Promise. - * @param {*} value Value to check for a Promise. - * @returns {boolean} True if the value appears to be a Promise. + * @param value Value to check for a Promise. + * @returns True if the value appears to be a Promise. */ -const isPromise = function (value) { +const isPromise = function (value: any): value is Promise { return ( value !== null && typeof value === 'object' && typeof value.then === 'function' ); }; +/* eslint-enable @typescript-eslint/no-explicit-any */ /** * Utility function to determine if a block is a procedure caller. - * @param {BlockCached} cached Cached block to check. - * @returns {boolean} True if the block is a procedure. + * @param cached Cached block to check. + * @returns True if the block is a procedure. */ -const isProcedureCaller = function (cached) { +const isProcedureCaller = function (cached: BlockCached) { return cached.opcode === 'procedures_call'; }; +type VariableFieldKeys = 'VARIABLE' | 'LIST' | 'BROADCAST_OPTION'; + +type ArgValues = { + mutation?: VMMutation; + [key: string]: CachedArgValue | VMMutation; +} & { + [key in VariableFieldKeys]?: VariableArgValue; +}; + +interface VariableArgValue { + id: string | null; + name: string | null; +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export type CachedArgValue = any; + /** * Handle any reported value from the primitive, either directly returned * or after a promise resolves. - * @param {*} resolvedValue Value eventually returned from the primitive. - * @param {!Sequencer} sequencer Sequencer stepping the thread for the ran + * @param resolvedValue Value eventually returned from the primitive. + * @param sequencer Sequencer stepping the thread for the ran * primitive. - * @param {!Thread} thread Thread containing the primitive. - * @param {!BlockCached} blockCached Cached block metadata. - * @param {boolean} lastOperation True if this is the last operation in a stack. + * @param thread Thread containing the primitive. + * @param blockCached Cached block metadata. + * @param lastOperation True if this is the last operation in a stack. */ // @todo move this to callback attached to the thread when we have performance // metrics (dd) -const handleReport = function (resolvedValue, sequencer, thread, blockCached, lastOperation) { +const handleReport = function ( + resolvedValue: unknown, + sequencer: Sequencer, + thread: Thread, + blockCached: BlockCached, + lastOperation: boolean +) { const currentBlockId = blockCached.id; const opcode = blockCached.opcode; const isHat = blockCached._isHat; @@ -70,8 +100,8 @@ const handleReport = function (resolvedValue, sequencer, thread, blockCached, la // true and used to be false, or the stack was activated explicitly // via stack click if (!thread.stackClick) { - const hasOldEdgeValue = thread.target.hasEdgeActivatedValue(currentBlockId); - const oldEdgeValue = thread.target.updateEdgeActivatedValue( + const hasOldEdgeValue = thread.target!.hasEdgeActivatedValue(currentBlockId); + const oldEdgeValue = thread.target!.updateEdgeActivatedValue( currentBlockId, resolvedValue ); @@ -94,14 +124,14 @@ const handleReport = function (resolvedValue, sequencer, thread, blockCached, la sequencer.runtime.visualReport(currentBlockId, resolvedValue); } if (thread.updateMonitor) { - const targetId = sequencer.runtime.monitorBlocks.getBlock(currentBlockId).targetId; + const targetId = sequencer.runtime.monitorBlocks.getBlock(currentBlockId)?.targetId; if (targetId && !sequencer.runtime.getTargetById(targetId)) { // Target no longer exists return; } sequencer.runtime.requestUpdateMonitor(Map({ id: currentBlockId, - spriteName: targetId ? sequencer.runtime.getTargetById(targetId).getName() : null, + spriteName: targetId ? sequencer.runtime.getTargetById(targetId)!.getName() : null, value: resolvedValue })); } @@ -111,7 +141,21 @@ const handleReport = function (resolvedValue, sequencer, thread, blockCached, la } }; -const handlePromise = (primitiveReportedValue, sequencer, thread, blockCached, lastOperation) => { +/** + * Handle a value that may be a Promise returned from a primitive, yielding the thread if so. + * @param primitiveReportedValue Value returned from the primitive, which may be a Promise. + * @param sequencer Sequencer stepping the thread for the ran primitive. + * @param thread Thread containing the primitive. + * @param blockCached Cached block metadata. + * @param lastOperation True if this is the last operation in a stack. + */ +const handlePromise = ( + primitiveReportedValue: Promise, + sequencer: Sequencer, + thread: Thread, + blockCached: BlockCached, + lastOperation: boolean +) => { if (thread.status === Thread.STATUS_RUNNING) { // Primitive returned a promise; automatically yield thread. thread.status = Thread.STATUS_PROMISE_WAIT; @@ -131,8 +175,8 @@ const handlePromise = (primitiveReportedValue, sequencer, thread, blockCached, l if (willPop === null) { return; } - nextBlockId = thread.blockContainer.getNextBlock(willPop); - target = thread.peekStackFrame().target; + nextBlockId = thread.blockContainer!.getNextBlock(willPop); + target = thread.peekStackFrame()!.target!; thread.popStack(); if (nextBlockId !== null) { // A next block exists so break out this loop @@ -165,135 +209,128 @@ const handlePromise = (primitiveReportedValue, sequencer, thread, blockCached, l * block when any change happens to it. This way we can quickly execute blocks * and keep perform the right action according to the current block information * in the editor. - * - * @param {Blocks} blockContainer the related Blocks instance - * @param {object} cached default set of cached values */ class BlockCached { - constructor (blockContainer, cached) { - /** - * Block id in its parent set of blocks. - * @type {string} - */ - this.id = cached.id; + /** + * Block id in its parent set of blocks. + */ + id: string; + /** + * Block operation code for this block. + */ + opcode: string; - /** - * Block operation code for this block. - * @type {string} - */ - this.opcode = cached.opcode; + /** + * Original block object containing argument values for static fields. + */ + fields: Record; - /** - * Original block object containing argument values for static fields. - * @type {object} - */ - this.fields = cached.fields; + /** + * Original block object containing argument values for executable inputs. + */ + inputs: Record; - /** - * Original block object containing argument values for executable inputs. - * @type {object} - */ - this.inputs = cached.inputs; + /** + * Procedure mutation. + */ + mutation?: VMMutation; + /** + * The profiler the block is configured with. + */ + _profiler: Profiler | null = null; + + /** + * Profiler information frame. + */ + _profilerFrame: ProfilerFrame | null = null; - /** - * Procedure mutation. - * @type {?object} - */ + /** + * Is the opcode a hat (event responder) block. + */ + _isHat = false; + + /** + * The block opcode's implementation function. + */ + _blockFunction: BlockFunction | null = null; + + /** + * Is the block function defined for this opcode? + */ + protected _definedBlockFunction = false; + + /** + * Is this block a block with no function but a static value to return. + */ + protected _isShadowBlock = false; + + /** + * The static value of this block if it is a shadow block. + */ + /* eslint-disable @typescript-eslint/no-explicit-any */ + protected _shadowValue: any = null; + /* eslint-enable @typescript-eslint/no-explicit-any */ + + /** + * A copy of the block's fields that may be modified. + */ + protected _fields: Record; + /** + * A copy of the block's inputs that may be modified. + */ + protected _inputs: Record; + + /** + * The inputs key the parent refers to this BlockCached by. + */ + _parentKey: string | null = null; + + /** + * The target object where the parent wants the resulting value stored + * with _parentKey as the key. + */ + _parentValues: ArgValues | null = null; + + /** + * An arguments object for block implementations. All executions of this + * specific block will use this objecct. + */ + _argValues: ArgValues; + + /** + * A sequence of non-shadow operations that can must be performed. This + * list recreates the order this block and its children are executed. + * Since the order is always the same we can safely store that order + * and iterate over the operations instead of dynamically walking the + * tree every time. + */ + _ops: BlockCached[] = []; + + /** + * @param blockContainer the related Blocks instance + * @param cached default set of cached values + */ + constructor (blockContainer: Blocks, cached: CachedBlockData) { + this.id = cached.id; + this.opcode = cached.opcode; + this.fields = cached.fields; + this.inputs = cached.inputs; this.mutation = cached.mutation; - /** - * The profiler the block is configured with. - * @type {?Profiler} - */ - this._profiler = null; - - /** - * Profiler information frame. - * @type {?ProfilerFrame} - */ - this._profilerFrame = null; - - /** - * Is the opcode a hat (event responder) block. - * @type {boolean} - */ - this._isHat = false; - - /** - * The block opcode's implementation function. - * @type {?Function} - */ - this._blockFunction = null; - - /** - * Is the block function defined for this opcode? - * @type {boolean} - */ - this._definedBlockFunction = false; - - /** - * Is this block a block with no function but a static value to return. - * @type {boolean} - */ - this._isShadowBlock = false; - - /** - * The static value of this block if it is a shadow block. - * @type {?any} - */ - this._shadowValue = null; - - /** - * A copy of the block's fields that may be modified. - * @type {object} - */ this._fields = Object.assign({}, this.fields); - - /** - * A copy of the block's inputs that may be modified. - * @type {object} - */ this._inputs = Object.assign({}, this.inputs); - - /** - * An arguments object for block implementations. All executions of this - * specific block will use this objecct. - * @type {object} - */ this._argValues = { mutation: this.mutation }; - /** - * The inputs key the parent refers to this BlockCached by. - * @type {string} - */ - this._parentKey = null; - - /** - * The target object where the parent wants the resulting value stored - * with _parentKey as the key. - * @type {object} - */ - this._parentValues = null; - - /** - * A sequence of non-shadow operations that can must be performed. This - * list recreates the order this block and its children are executed. - * Since the order is always the same we can safely store that order - * and iterate over the operations instead of dynamically walking the - * tree every time. - * @type {Array} - */ - this._ops = []; - - const {runtime} = blockUtility.sequencer; + const runtime = blockUtility.sequencer?.runtime; + if (!runtime) throw new Error('Runtime is required for BlockCached.'); const {opcode, fields, inputs} = this; // Assign opcode isHat and blockFunction data to avoid dynamic lookups. this._isHat = runtime.getIsHat(opcode); - this._blockFunction = runtime.getOpcodeFunction(opcode); + this._blockFunction = runtime.getOpcodeFunction(opcode)!; this._definedBlockFunction = typeof this._blockFunction !== 'undefined'; // Store the current shadow value if there is a shadow value. @@ -313,8 +350,8 @@ class BlockCached { fieldName === 'BROADCAST_OPTION' ) { this._argValues[fieldName] = { - id: fields[fieldName].id, - name: fields[fieldName].value + id: fields[fieldName].id!, + name: fields[fieldName].value! }; } else { this._argValues[fieldName] = fields[fieldName].value; @@ -339,13 +376,15 @@ class BlockCached { // Shadow dropdown menu is being used. // Get the appropriate information out of it. const shadow = blockContainer.getBlock(broadcastInput.shadow); - const broadcastField = shadow.fields.BROADCAST_OPTION; - this._argValues.BROADCAST_OPTION.id = broadcastField.id; - this._argValues.BROADCAST_OPTION.name = broadcastField.value; - - // Evaluating BROADCAST_INPUT here we do not need to do so - // later. - delete this._inputs.BROADCAST_INPUT; + if (shadow) { + const broadcastField = shadow.fields.BROADCAST_OPTION; + this._argValues.BROADCAST_OPTION.id = broadcastField.id!; + this._argValues.BROADCAST_OPTION.name = broadcastField.value!; + + // Evaluating BROADCAST_INPUT here we do not need to do so + // later. + delete this._inputs.BROADCAST_INPUT; + } } } @@ -359,16 +398,16 @@ class BlockCached { if (item.execute === this.opcode) { this._ops.push(this); } else { - // eslint-disable-next-line no-shadow - const cached = new BlockCached(blockContainer, { + + const newCached = new BlockCached(blockContainer, { id: '', opcode: item.execute, fields: {}, inputs: {} }); - cached._argValues = this._argValues; - cached._parentValues = {}; - this._ops.push(cached); + newCached._argValues = this._argValues; + newCached._parentValues = {}; + this._ops.push(newCached); } } } else if (Object.prototype.hasOwnProperty.call(this._inputs, item)) { @@ -394,15 +433,14 @@ class BlockCached { /** * Push an input with given name to ops. - * @param {!string} inputName The input name. - * @param {!Blocks} blockContainer The related Blocks instance. - * @private + * @param inputName The input name. + * @param blockContainer The related Blocks instance. */ - _pushInput (inputName, blockContainer) { + protected _pushInput (inputName: string, blockContainer: Blocks) { const input = this._inputs[inputName]; if (input.block) { const inputCached = getCachedExecuteBlock(blockContainer, input.block, BlockCached); - if (inputCached._isHat) { + if (!inputCached || inputCached._isHat) { return; } @@ -422,10 +460,10 @@ class BlockCached { /** * Initialize a BlockCached instance so its command/hat * block and reporters can be profiled during execution. - * @param {Profiler} profiler - The profiler that is currently enabled. - * @param {BlockCached} blockCached - The blockCached instance to profile. + * @param profiler - The profiler that is currently enabled. + * @param blockCached - The blockCached instance to profile. */ -const _prepareBlockProfiling = function (profiler, blockCached) { +const _prepareBlockProfiling = function (profiler: Profiler, blockCached: BlockCached) { blockCached._profiler = profiler; if (blockFunctionProfilerId === -1) { @@ -440,10 +478,10 @@ const _prepareBlockProfiling = function (profiler, blockCached) { /** * Execute a block. - * @param {!Sequencer} sequencer Which sequencer is executing. - * @param {!Thread} thread Thread which to read and execute. + * @param sequencer Which sequencer is executing. + * @param thread Thread which to read and execute. */ -const execute = function (sequencer, thread) { +const execute = function (sequencer: Sequencer, thread: Thread) { const runtime = sequencer.runtime; // store sequencer and thread so block functions can access them through @@ -452,10 +490,10 @@ const execute = function (sequencer, thread) { blockUtility.thread = thread; // Current block to execute is the one on the top of the stack. - const currentBlockId = thread.peekStack(); - const currentStackFrame = thread.peekStackFrame(); + const currentBlockId = thread.peekStack()!; + const currentStackFrame = thread.peekStackFrame()!; - let blockContainer = thread.blockContainer; + let blockContainer = thread.blockContainer!; let blockCached = getCachedExecuteBlock(blockContainer, currentBlockId, BlockCached); if (blockCached === null) { blockContainer = runtime.flyoutBlocks; @@ -481,14 +519,14 @@ const execute = function (sequencer, thread) { const opCached = ops.find(op => op.id === oldOpCached); if (opCached) { - const inputName = opCached._parentKey; - const argValues = opCached._parentValues; + const inputName = opCached._parentKey!; + const argValues = opCached._parentValues!; if (inputName === 'BROADCAST_INPUT') { // Something is plugged into the broadcast input. // Cast it to a string. We don't need an id here. - argValues.BROADCAST_OPTION.id = null; - argValues.BROADCAST_OPTION.name = cast.toString(inputValue); + argValues.BROADCAST_OPTION!.id = null; + argValues.BROADCAST_OPTION!.name = cast.toString(inputValue); } else { argValues[inputName] = inputValue; } @@ -515,20 +553,20 @@ const execute = function (sequencer, thread) { thread.justReported = null; - const inputName = opCached._parentKey; - const argValues = opCached._parentValues; + const inputName = opCached._parentKey!; + const argValues = opCached._parentValues!; // cc - if current call is the last operation, which means that it is called by clicking directly, // then call handleReport. if (currentStackFrame.waitingReporter && i === length - 1) { // cc - if returned value is null, then set the argument to undefined to avoid visual report. - // eslint-disable-next-line no-undefined + handleReport(inputValue ?? undefined, sequencer, thread, opCached, true); } else if (inputName === 'BROADCAST_INPUT') { // Something is plugged into the broadcast input. // Cast it to a string. We don't need an id here. - argValues.BROADCAST_OPTION.id = null; - argValues.BROADCAST_OPTION.name = cast.toString(inputValue); + argValues.BROADCAST_OPTION!.id = null; + argValues.BROADCAST_OPTION!.name = cast.toString(inputValue); } else { argValues[inputName] = inputValue; } @@ -564,7 +602,7 @@ const execute = function (sequencer, thread) { // Inputs are set during previous steps in the loop. - const primitiveReportedValue = blockFunction(argValues, blockUtility); + const primitiveReportedValue = blockFunction?.(argValues, blockUtility); // cc - preserve returned value if (opCached.opcode === 'procedures_return') { @@ -605,7 +643,7 @@ const execute = function (sequencer, thread) { currentStackFrame.reporting = ops[i].id; currentStackFrame.reported = ops.slice(0, i).map(reportedCached => { const inputName = reportedCached._parentKey; - const reportedValues = reportedCached._parentValues; + const reportedValues = reportedCached._parentValues!; if (inputName === 'BROADCAST_INPUT') { return { @@ -615,7 +653,7 @@ const execute = function (sequencer, thread) { } return { opCached: reportedCached.id, - inputValue: reportedValues ? reportedValues[inputName] : null + inputValue: reportedValues ? reportedValues[inputName!] : null }; }); @@ -628,14 +666,14 @@ const execute = function (sequencer, thread) { } else { // By definition a block that is not last in the list has a // parent. - const inputName = opCached._parentKey; - const parentValues = opCached._parentValues; + const inputName = opCached._parentKey!; + const parentValues = opCached._parentValues!; if (inputName === 'BROADCAST_INPUT') { // Something is plugged into the broadcast input. // Cast it to a string. We don't need an id here. - parentValues.BROADCAST_OPTION.id = null; - parentValues.BROADCAST_OPTION.name = cast.toString(primitiveReportedValue); + parentValues.BROADCAST_OPTION!.id = null; + parentValues.BROADCAST_OPTION!.name = cast.toString(primitiveReportedValue); } else { parentValues[inputName] = primitiveReportedValue; } @@ -653,7 +691,7 @@ const execute = function (sequencer, thread) { // reference an operation outside of the set of operations. const end = Math.min(i + 1, length); for (let p = start; p < end; p++) { - ops[p]._profilerFrame.count += 1; + ops[p]._profilerFrame!.count += 1; } } }; diff --git a/packages/vm/src/engine/index.ts b/packages/vm/src/engine/index.ts new file mode 100644 index 000000000..ddbf7310b --- /dev/null +++ b/packages/vm/src/engine/index.ts @@ -0,0 +1,6 @@ +export type * from './block-utility'; +export type { + CloudDataManager, + RuntimeEvents +} from './runtime'; +export type * from './thread'; diff --git a/packages/vm/src/engine/monitor-record.ts b/packages/vm/src/engine/monitor-record.ts index 859fc7adc..a431979e7 100644 --- a/packages/vm/src/engine/monitor-record.ts +++ b/packages/vm/src/engine/monitor-record.ts @@ -1,23 +1,24 @@ import {Record} from 'immutable'; -interface MonitorRecordProps { +export interface MonitorRecordProps { id: string | null; /** Present only if the monitor is sprite-specific, such as x position */ - spriteName: string | null; + spriteName?: string | null; /** Present only if the monitor is sprite-specific, such as x position */ - targetId: string | null; + targetId?: string | null; opcode: string | null; value: unknown; params: unknown; mode: string; - sliderMin: number; - sliderMax: number; - isDiscrete: boolean; + sliderMin?: number; + sliderMax?: number; + isDiscrete?: boolean; x: number | null; y: number | null; width: number; height: number; visible: boolean; + [key: string | symbol]: unknown; } const defaultMonitorRecord: MonitorRecordProps = { diff --git a/packages/vm/src/engine/mutation-adapter.ts b/packages/vm/src/engine/mutation-adapter.ts index 4447efeff..6eb06b003 100644 --- a/packages/vm/src/engine/mutation-adapter.ts +++ b/packages/vm/src/engine/mutation-adapter.ts @@ -1,11 +1,12 @@ import * as html from 'htmlparser2'; import decodeHtml from 'decode-html'; import type {Element} from 'domhandler'; +import type {VMMutation} from '../serialization/schema'; /** * Convert a part of a mutation DOM to a mutation VM object, recursively. - * @param {object} dom DOM object for mutation tag. - * @returns {object} Object representing useful parts of this mutation. + * @param dom DOM object for mutation tag. + * @returns Object representing useful parts of this mutation. */ const mutatorTagToObject = function (dom: Element) { const obj = Object.create(null); @@ -33,9 +34,9 @@ const mutatorTagToObject = function (dom: Element) { * Adapter between mutator XML or DOM and block representation which can be * used by the Scratch runtime. * @param mutation Mutation XML string or DOM. - * @returns {object} Object representing the mutation. + * @returns Object representing the mutation. */ -const mutationAdapter = function (mutation: Element | string) { +const mutationAdapter = function (mutation: Element | string): VMMutation { let mutationParsed; // Check if the mutation is already parsed; if not, parse it. if (typeof mutation === 'object') { diff --git a/packages/vm/src/engine/profiler.ts b/packages/vm/src/engine/profiler.ts index 0f3d3ef8f..769ef5c5a 100644 --- a/packages/vm/src/engine/profiler.ts +++ b/packages/vm/src/engine/profiler.ts @@ -43,7 +43,7 @@ const START_SIZE = 4; */ const STOP_SIZE = 2; -type FrameCallback = (frame: ProfilerFrame) => void; +export type FrameCallback = (frame: ProfilerFrame) => void; /** * A set of information about a frame of execution that was recorded. @@ -135,14 +135,14 @@ class Profiler { * Runtime._step. * @param arg An arbitrary argument value to store with the frame. */ - start (id: number, arg?: unknown): void { + start (id: number, arg?: unknown) { this.records.push(START, id, arg, performance.now()); } /** * Stop the current frame. */ - stop (): void { + stop () { this.records.push(STOP, performance.now()); } @@ -150,7 +150,7 @@ class Profiler { * Increment the number of times this symbol is called. * @param id The id returned by idByName for a name symbol. */ - increment (id: number): void { + increment (id: number) { if (!this.increments[id]) { this.increments[id] = new ProfilerFrame(-1); this.increments[id].id = id; @@ -184,7 +184,7 @@ class Profiler { /** * Decode records and report all frames to `this.onFrame`. */ - reportFrames (): void { + reportFrames () { const stack = this._stack; let depth = 1; @@ -322,3 +322,4 @@ class Profiler { } export default Profiler; +export type {ProfilerFrame}; diff --git a/packages/vm/src/engine/runtime.js b/packages/vm/src/engine/runtime.ts similarity index 65% rename from packages/vm/src/engine/runtime.js rename to packages/vm/src/engine/runtime.ts index d2710d2d4..3e36ab3e6 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.ts @@ -1,30 +1,31 @@ import EventEmitter from 'events'; -import {OrderedMap} from 'immutable'; +import {OrderedMap, Map} from 'immutable'; +import type {RecordOf, Map as ImmutableMap} from 'immutable'; import ArgumentType from '../extension-support/argument-type'; -import Blocks from './blocks.js'; -import {getScripts as getCachedScriptsByOpcode} from './blocks-runtime-cache'; +import Blocks from './blocks'; +import {getScripts as getCachedScriptsByOpcode, RuntimeScriptCache} from './blocks-runtime-cache'; import BlockType from '../extension-support/block-type'; -import Profiler from './profiler'; +import Profiler, {type FrameCallback} from './profiler'; import Sequencer from './sequencer'; -import execute from './execute.js'; +import execute from './execute'; import ScratchBlocksConstants from './scratch-blocks-constants'; import TargetType from '../extension-support/target-type'; import Thread from './thread'; import log from '../util/log'; import maybeFormatMessage from '../util/maybe-format-message'; import StageLayering from './stage-layering'; -import Variable from './variable'; +import Variable, {type VariableType} from './variable'; import xmlEscape from '../util/xml-escape'; import ScratchLinkWebSocket from '../util/scratch-link-websocket'; import Clock from '../io/clock'; -import Cloud from '../io/cloud.js'; +import Cloud from '../io/cloud'; import Keyboard from '../io/keyboard'; -import Mouse from '../io/mouse.js'; +import Mouse from '../io/mouse'; import MouseWheel from '../io/mouseWheel'; import UserData from '../io/userData'; -import Video from '../io/video.js'; +import Video from '../io/video'; import Joystick from '../io/joystick'; import StringUtil from '../util/string-util'; import uid from '../util/uid'; @@ -38,6 +39,70 @@ import sensing from '../blocks/scratch3_sensing'; import data from '../blocks/scratch3_data'; import procedures from '../blocks/scratch3_procedures'; +import type RenderedTarget from '../sprites/rendered-target'; +import type AudioEngine from 'clipcc-audio'; +import type RenderWebGL from 'clipcc-render'; +import type {ScratchStorage} from 'clipcc-storage'; +import type {BitmapAdapter} from 'clipcc-svg-renderer'; +import type * as ClipCCBlocks from 'clipcc-block'; +import type {BlockFunction} from '../blocks/category_prototype'; +import type {VMBlock, VMField} from '../serialization/schema'; +import type { + ExtensionArgumentMetadata, + ExtensionButtonMetadata, + ExtensionCustomFieldTypeMetadata, + ExtensionImageMetadata, + ShortExtensionMenuItem, + NormalizedExtensionMetadata, + NormalizedExtensionMenuItem, + ExtensionMenuItemObject, + NormalizedExtensionItemMetadata, + NormalizedExtensionBlockMetadata, + ExtensionCustomFieldTypeInfo +} from '../extension-support/extension-metadata'; +import type {FieldDropdownArg, JsonBlockArg, JsonBlockDefinition} from '../types/json-block-definitions'; +import type {MonitorRecordProps} from './monitor-record'; + +type MenuGenerator = ClipCCBlocks.MenuOption[]; + +export interface MenuInfo { + json: JsonBlockDefinition; +} + +export interface BlockInfo { + info: NormalizedExtensionBlockMetadata; + json: JsonBlockDefinition; + xml: string; +}; + +export interface ButtonInfo { + info: ExtensionButtonMetadata; + xml: string; +}; + +export interface SepInfo { + info: '---'; + xml: string; +}; + +export type CategoryInfo = + Pick< + NormalizedExtensionMetadata, + 'id' | 'name' | 'showStatusButton' | 'blockIconURI' | 'menuIconURI' | 'color1' | 'color2' | 'color3' + > & + { + menuInfo: Record; + customFieldTypes: Record; + menus: MenuInfo[]; + blocks: (BlockInfo | ButtonInfo | SepInfo)[]; + }; + +export interface PeripheralExtensionClass { + scan(): void; + connect(peripheralId: number): void; + disconnect(): void; + isConnected(): boolean; +} const defaultBlockPackages = { scratch3_control: control, @@ -54,219 +119,25 @@ const defaultBlockPackages = { const defaultExtensionColors = ['#0FBD8C', '#0DA57A', '#0B8E69']; -/** - * @typedef {import('../sprites/rendered-target').default} RenderedTarget - * @typedef {import('clipcc-audio')} AudioEngine - * @typedef {import('clipcc-render')} RenderWebGL - * @typedef {import('clipcc-storage').ScratchStorage} ScratchStorage - */ - -/** - * @callback PrimitiveHandler - * @param {...unknown} args - * @returns {unknown} - */ - -/** - * @callback BooleanFunction - * @returns {boolean} - */ - -/** - * @callback VoidFunction - * @returns {void} - */ - -/** - * @callback ScriptCallback - * @param {string} script - * @param {RenderedTarget} target - * @returns {void} - */ - -/** - * @callback ScratchLinkSocketFactory - * @param {string} type - * @returns {ScratchLinkSocket} - */ - -/** - * @callback ProfilerFrameHandler - * @param {Record} frame - * @returns {void} - */ - -/** - * @typedef {{send?: (...args: unknown[]) => void, close?: () => void}} ScratchLinkSocket - */ - -/** - * @typedef {Record} ScratchBlocksJson - */ - -/** - * @typedef {{json: ScratchBlocksJson}} ScratchBlocksDefinition - */ - -/** - * @typedef {string|{text: string|Record, value: string}} MenuItem - */ - -/** - * @typedef {{ - * items: Array|(() => Array), - * acceptReporters?: boolean - * }} ExtensionMenuInfo - */ - -/** - * @typedef {{ - * type?: string, - * defaultValue?: unknown, - * menu?: string, - * dataURI?: string, - * flipRTL?: boolean - * }} ExtensionArgumentInfo - */ - -/** - * @typedef {{ - * opcode: string, - * text: string|Array, - * func: PrimitiveHandler|string, - * blockIconURI?: string, - * arguments?: Record, - * branchCount?: number, - * disableMonitor?: boolean, - * isDynamic?: boolean, - * isEdgeActivated?: boolean, - * isTerminal?: boolean, - * hideFromPalette?: boolean, - * shouldRestartExistingThreads?: boolean, - * filter?: Array, - * blockType: string|number - * }} ExtensionBlockMetadata - */ - -/** - * @typedef {{info: ExtensionBlockMetadata|string, json?: ScratchBlocksJson, xml: string}} ConvertedBlockInfo - */ - -/** - * @typedef {ExtensionBlockMetadata & {func: string}} ExtensionButtonMetadata - */ - -/** - * @typedef {{ - * output: string, - * outputShape: number, - * implementation: unknown - * }} ExtensionCustomFieldTypeMetadata - */ - -/** - * @typedef {{ - * fieldName: string, - * extendedName: string, - * argumentTypeInfo: {shadow: {type: string, fieldName: string}}, - * scratchBlocksDefinition: ScratchBlocksDefinition, - * fieldImplementation: unknown - * }} ExtensionCustomFieldTypeInfo - */ - -/** - * @typedef {{ - * edgeActivated?: boolean, - * restartExistingThreads?: boolean - * }} HatMetadata - */ - -/** - * @typedef {{ - * isSpriteSpecific?: boolean, - * getId: (targetId?: string, fields?: Record) => string - * }} MonitorBlockInfo - */ - -/** - * @typedef {{ - * infiniteCloning: boolean, - * edgelessStage: boolean, - * unlimitedListLength: boolean, - * unlimitedPenSize: boolean, - * accurateCoordinates: boolean, - * unlimitedSoundStuffs: boolean - * }} RuntimeLimitOptions - */ - -/** - * @typedef {{ - * id: string, - * name: string, - * showStatusButton?: boolean, - * blockIconURI?: string, - * menuIconURI?: string, - * color1: string, - * color2: string, - * color3: string, - * blocks: Array, - * customFieldTypes: Record, - * menus: Array, - * menuInfo: Record - * }} CategoryInfo - */ - -/** - * @typedef {{ - * id: string, - * name: string|Record, - * showStatusButton?: boolean, - * blockIconURI?: string, - * menuIconURI?: string, - * color1?: string, - * color2?: string, - * color3?: string, - * menus: Record, - * customFieldTypes: Record, - * blocks: Array - * }} ExtensionMetadata - */ - -/** - * @typedef {{ - * scan: () => void, - * connect: (peripheralId: number) => void, - * disconnect: () => void, - * isConnected: () => boolean - * }} PeripheralExtension - */ +type ScriptCallback = (script: string, target: RenderedTarget) => void; +type ScriptByOpcodeCallback = (script: RuntimeScriptCache, target: RenderedTarget) => void; +export type ScratchLinkSocketFactory = (type: string) => ScratchLinkWebSocket; -/** - * @typedef {{id: string, xml: string}} BlockCategoryXml - */ - -/** - * @typedef {{category: string, label?: string, labelFn?: PrimitiveHandler}} OpcodeLabelInfo - */ +export interface HatMetadata { + edgeActivated?: boolean; + restartExistingThreads?: boolean; +} -/** - * @typedef {{ - * outLineNum: number, - * blockInfo: ExtensionBlockMetadata, - * categoryInfo: CategoryInfo, - * blockJSON: ScratchBlocksJson, - * inputList: Array, - * argsMap: Record - * }} PlaceholderContext - */ +export interface MonitorBlockInfo { + isSpriteSpecific?: boolean; + getId: (targetId?: string, fields?: Record) => string; +} /** * Information used for converting Scratch argument types into scratch-blocks data. - * @type {Record} */ -const ArgumentTypeMap = (() => { - const map = {}; - map[ArgumentType.ANGLE] = { +const ArgumentTypeMap = { + [ArgumentType.ANGLE]: { shadow: { type: 'math_angle', // We specify fieldNames here so that we can pick @@ -278,58 +149,55 @@ const ArgumentTypeMap = (() => { // used instead (e.g. default of 0 for number fields) fieldName: 'NUM' } - }; - map[ArgumentType.COLOR] = { + }, + [ArgumentType.COLOR]: { shadow: { type: 'colour_picker', fieldName: 'COLOUR' } - }; - map[ArgumentType.NUMBER] = { + }, + [ArgumentType.NUMBER]: { shadow: { type: 'math_number', fieldName: 'NUM' } - }; - map[ArgumentType.STRING] = { + }, + [ArgumentType.STRING]: { shadow: { type: 'text', fieldName: 'TEXT' } - }; - map[ArgumentType.BOOLEAN] = { + }, + [ArgumentType.BOOLEAN]: { check: 'Boolean' - }; - map[ArgumentType.MATRIX] = { + }, + [ArgumentType.MATRIX]: { shadow: { type: 'matrix', fieldName: 'MATRIX' } - }; - map[ArgumentType.NOTE] = { + }, + [ArgumentType.NOTE]: { shadow: { type: 'note', fieldName: 'NOTE' } - }; - map[ArgumentType.IMAGE] = { + }, + [ArgumentType.IMAGE]: { // Inline images are weird because they're not actually "arguments". // They are more analagous to the label on a block. fieldType: 'field_image' - }; - return map; -})(); + } +}; -/** - * A pair of functions used to manage the cloud variable limit, - * to be used when adding (or attempting to add) or removing a cloud variable. - * @typedef {{ - * canAddCloudVariable: BooleanFunction, - * addCloudVariable: VoidFunction, - * removeCloudVariable: VoidFunction, - * hasCloudVariables: BooleanFunction - * }} CloudDataManager - */ +export interface PlaceholderContext { + argsMap: Record; + blockJSON: JsonBlockDefinition + categoryInfo: CategoryInfo; + blockInfo: NormalizedExtensionBlockMetadata; + inputList: string[]; + outLineNum?: number; +} /** * Creates and manages cloud variable limit in a project, @@ -338,7 +206,7 @@ const ArgumentTypeMap = (() => { * and remove an existing cloud variable. * These are to be called whenever attempting to create or delete * a cloud variable. - * @returns {CloudDataManager} The functions to be used when adding or removing a + * @returns The functions to be used when adding or removing a * cloud variable. */ const cloudDataManager = () => { @@ -365,299 +233,321 @@ const cloudDataManager = () => { }; }; +export type CloudDataManager = ReturnType; + /** * Numeric ID for Runtime._step in Profiler instances. - * @type {number} */ let stepProfilerId = -1; /** * Numeric ID for Sequencer.stepThreads in Profiler instances. - * @type {number} */ let stepThreadsProfilerId = -1; /** * Numeric ID for RenderWebGL.draw in Profiler instances. - * @type {number} */ let rendererDrawProfilerId = -1; +/** + * Events that can be emitted by Runtime. + */ +export interface RuntimeEvents { + 'STAGE_SIZE_UPDATE': [width: number, height: number]; + 'SCRIPT_GLOW_ON': [{id: string}]; + 'SCRIPT_GLOW_OFF': [{id: string}]; + 'BLOCK_GLOW_ON': [{id: string}]; + 'BLOCK_GLOW_OFF': [{id: string}]; + 'HAS_CLOUD_DATA_UPDATE': [hasCloudData: boolean]; + 'TURBO_MODE_ON': []; + 'TURBO_MODE_OFF': []; + 'PROJECT_START': []; + 'PROJECT_RUN_START': []; + 'PROJECT_RUN_STOP': []; + 'PROJECT_STOP_ALL': []; + 'STOP_FOR_TARGET': [target: RenderedTarget, optThreadException?: Thread]; + 'VISUAL_REPORT': [{id: string, value: string}]; + 'PROJECT_LOADED': []; + 'PROJECT_CHANGED': []; + 'TOOLBOX_EXTENSIONS_NEED_UPDATE': []; + 'TARGETS_UPDATE': [isForceRefresh: boolean]; + 'MONITORS_UPDATE': [monitorState: OrderedMap>]; + 'BLOCK_DRAG_UPDATE': [areBlocksOverGui: boolean]; + 'BLOCK_DRAG_END': [blocks: VMBlock[], topBlockId: string]; + 'EXTENSION_ADDED': [categoryInfo: CategoryInfo]; + 'EXTENSION_FIELD_ADDED': [{name: string, implementation: unknown}]; + 'PERIPHERAL_LIST_UPDATE': [availablePeripherals: Record]; + 'USER_PICKED_PERIPHERAL': [availablePeripherals: Record]; + 'PERIPHERAL_CONNECTED': []; + 'PERIPHERAL_DISCONNECTED': []; + 'PERIPHERAL_REQUEST_ERROR': [{message: string, extensionId: string}]; + 'PERIPHERAL_CONNECTION_LOST_ERROR': [{message: string, extensionId: string}]; + 'PERIPHERAL_SCAN_TIMEOUT': []; + 'MIC_LISTENING': [listening: boolean]; + 'BLOCKSINFO_UPDATE': [categoryInfo: CategoryInfo]; + 'RUNTIME_STARTED': []; + 'RUNTIME_DISPOSED': []; + 'BLOCKS_NEED_UPDATE': []; + 'ANSWER': [answer: string]; + 'SAY': [target: RenderedTarget, variant: 'say' | 'think', text: string]; + 'KEY_PRESSED': [key: string]; + 'QUESTION': [text: string | null]; + 'PLAY_NOTE': [noteNum: number, extensionId: string]; + /** + * Event fired after a new target has been created, possibly by cloning an existing target. + * @param newTarget - the newly created target. + * @param sourceTarget - the target used as a source for the new clone, if any. + */ + 'targetWasCreated': [newTarget: RenderedTarget, sourceTarget?: RenderedTarget]; + 'targetWasRemoved': [target: RenderedTarget]; +} + /** * Manages targets, scripts, and the sequencer. * @class */ -class Runtime extends EventEmitter { - constructor () { - super(); +class Runtime extends EventEmitter { + /** + * Current time in milliseconds, used for determining elapsed time and for scheduling future tasks. + */ + currentMSecs = 0; - /** - * Current time in milliseconds, used for determining elapsed time and for scheduling future tasks. - * @type {number} - */ - this.currentMSecs = 0; - - /** - * Target management and storage. - * @type {Array.} - */ - this.targets = []; - - /** - * Targets in reverse order of execution. Shares its order with drawables. - * @type {Array.} - */ - this.executableTargets = []; - - /** - * A list of threads that are currently running in the VM. - * Threads are added when execution starts and pruned when execution ends. - * @type {Array.} - */ - this.threads = []; + /** + * Target management and storage. + */ + targets: RenderedTarget[] = []; - /** @type {!Sequencer} */ - this.sequencer = new Sequencer(this); - - /** - * Storage container for flyout blocks. - * These will execute on `_editingTarget.` - * @type {!Blocks} - */ - this.flyoutBlocks = new Blocks(this, true /* force no glow */); - - /** - * Storage container for monitor blocks. - * These will execute on a target maybe - * @type {!Blocks} - */ - this.monitorBlocks = new Blocks(this, true /* force no glow */); - - /** - * Currently known editing target for the VM. - * @type {?RenderedTarget} - */ - this._editingTarget = null; - - /** - * Map to look up a block primitive's implementation function by its opcode. - * This is a two-step lookup: package name first, then primitive name. - * @type {Record} - */ - this._primitives = {}; - - /** - * Map to look up all block information by extended opcode. - * @type {Array.} - * @private - */ - this._blockInfo = []; - - /** - * Map to look up hat blocks' metadata. - * Keys are opcode for hat, values are metadata objects. - * @type {Record} - */ - this._hats = {}; - - /** - * Map to look up a block's execution order. - * Keys are opcode for block, values are order array of its arguments. - * @type {Record>} - */ - this._orders = {}; - - /** - * A list of script block IDs that were glowing during the previous frame. - * @type {!Array.} - */ - this._scriptGlowsPreviousFrame = []; + /** + * Targets in reverse order of execution. Shares its order with drawables. + */ + executableTargets: RenderedTarget[] = []; - /** - * Number of non-monitor threads running during the previous frame. - * @type {number} - */ - this._nonMonitorThreadCount = 0; - - /** - * All threads that finished running and were removed from this.threads - * by behaviour in Sequencer.stepThreads. - * @type {Array} - */ - this._lastStepDoneThreads = null; - - /** - * Currently known number of clones, used to enforce clone limit. - * @type {number} - */ - this._cloneCounter = 0; - - /** - * Flag to emit a targets update at the end of a step. When target data - * changes, this flag is set to true. - * @type {boolean} - */ - this._refreshTargets = false; - - /** - * Map to look up all monitor block information by opcode. - * @type {Record} - * @private - */ - this.monitorBlockInfo = {}; - - /** - * Ordered map of all monitors, which are MonitorReporter objects. - */ - this._monitorState = OrderedMap({}); + /** + * A list of threads that are currently running in the VM. + * Threads are added when execution starts and pruned when execution ends. + */ + threads: Thread[] = []; - /** - * Monitor state from last tick - */ - this._prevMonitorState = OrderedMap({}); - - /** - * Whether the project is in "turbo mode." - * @type {boolean} - */ - this.turboMode = false; - - /** - * Whether the project is in "compatibility mode" (30 TPS). - * @type {boolean} - * @deprecated Use framerate instead. - */ - this.compatibilityMode = false; - - /** - * The limit options. - * @type {RuntimeLimitOptions} - */ - this.limitOptions = { - infiniteCloning: false, - edgelessStage: false, - unlimitedListLength: false, - unlimitedPenSize: false, - accurateCoordinates: false, - unlimitedSoundStuffs: false - }; + sequencer = new Sequencer(this); - /** - * A reference to the current runtime stepping interval, set - * by a `setInterval`. - * @type {!number} - */ - this._steppingInterval = null; + /** + * Storage container for flyout blocks. + * These will execute on `_editingTarget.` + */ + flyoutBlocks = new Blocks(this, true /* force no glow */); - /** - * Configured framerate. - * @type {!number} - */ - this.framerate = 60; + /** + * Storage container for monitor blocks. + * These will execute on a target maybe + */ + monitorBlocks = new Blocks(this, true /* force no glow */); - /** - * Current length of a step. Equals to 1000 / this.framerate. - * Changes as mode switches, and used by the sequencer to calculate - * WORK_TIME. - * @type {!number} - */ - this.currentStepTime = null; + /** + * Currently known editing target for the VM. + */ + _editingTarget: RenderedTarget | null = null; - // Set an intial value for this.currentMSecs - this.updateCurrentMSecs(); + /** + * Map to look up a block primitive's implementation function by its opcode. + * This is a two-step lookup: package name first, then primitive name. + */ + _primitives: Record = {}; - /** - * Whether any primitive has requested a redraw. - * Affects whether `Sequencer.stepThreads` will yield - * after stepping each thread. - * Reset on every frame. - * @type {boolean} - */ - this.redrawRequested = false; + /** + * Array that stores all extension block's information. + * @private + */ + _blockInfo: CategoryInfo[] = []; - /** - * Get stage width. - * @type {number} - */ - this.stageWidth = 480; + /** + * Map to look up hat blocks' metadata. + * Keys are opcode for hat, values are metadata objects. + */ + _hats: Record = {}; - /** - * Get stage height. - * @type {number} - */ - this.stageHeight = 360; + /** + * Map to look up a block's execution order. + * Keys are opcode for block, values are order array of its arguments. + */ + _orders: Record = {}; - // Register all given block packages. - this._registerBlockPackages(); + /** + * A list of script block IDs that were glowing during the previous frame. + */ + _scriptGlowsPreviousFrame: string[] = []; - // Register and initialize "IO devices", containers for processing - // I/O related data. - this.ioDevices = { - clock: new Clock(this), - cloud: new Cloud(this), - keyboard: new Keyboard(this), - mouse: new Mouse(this), - joystick: new Joystick(this), - mouseWheel: new MouseWheel(this), - userData: new UserData(), - video: new Video(this) - }; + /** + * Number of non-monitor threads running during the previous frame. + */ + _nonMonitorThreadCount = 0; - /** - * A list of extensions, used to manage hardware connection. - * @type {Record} - */ - this.peripheralExtensions = {}; - - /** - * A runtime profiler that records timed events for later playback to - * diagnose Scratch performance. - * @type {Profiler} - */ - this.profiler = null; + /** + * All threads that finished running and were removed from this.threads + * by behaviour in Sequencer.stepThreads. + */ + _lastStepDoneThreads: Thread[] | null = null; - const newCloudDataManager = cloudDataManager(); + /** + * Currently known number of clones, used to enforce clone limit. + */ + _cloneCounter = 0; - /** - * Check wether the runtime has any cloud data. - * @type {BooleanFunction} - * @returns {boolean} Whether or not the runtime currently has any - * cloud variables. - */ - this.hasCloudData = newCloudDataManager.hasCloudVariables; + /** + * Flag to emit a targets update at the end of a step. When target data + * changes, this flag is set to true. + */ + _refreshTargets = false; - /** - * A function which checks whether a new cloud variable can be added - * to the runtime. - * @type {BooleanFunction} - * @returns {boolean} Whether or not a new cloud variable can be added - * to the runtime. - */ - this.canAddCloudVariable = newCloudDataManager.canAddCloudVariable; + /** + * Map to look up all monitor block information by opcode. + */ + monitorBlockInfo: Record = {}; - /** - * A function that tracks a new cloud variable in the runtime, - * updating the cloud variable limit. Calling this function will - * emit a cloud data update event if this is the first cloud variable - * being added. - * @type {VoidFunction} - */ - this.addCloudVariable = this._initializeAddCloudVariable(newCloudDataManager); + /** + * Ordered map of all monitors, which are MonitorReporter objects. + */ + _monitorState = OrderedMap>(); - /** - * A function which updates the runtime's cloud variable limit - * when removing a cloud variable and emits a cloud update event - * if the last of the cloud variables is being removed. - * @type {VoidFunction} - */ - this.removeCloudVariable = this._initializeRemoveCloudVariable(newCloudDataManager); + /** + * Monitor state from last tick + */ + _prevMonitorState = OrderedMap>(); + + /** + * Whether the project is in "turbo mode." + */ + turboMode = false; - /** - * A string representing the origin of the current project from outside of the - * Scratch community, such as CSFirst. - * @type {?string} - */ - this.origin = null; + /** + * Whether the project is in "compatibility mode" (30 TPS). + * @deprecated Use framerate instead. + */ + compatibilityMode = false; + + /** + * The limit options. + */ + limitOptions = { + infiniteCloning: false, + edgelessStage: false, + unlimitedListLength: false, + unlimitedPenSize: false, + accurateCoordinates: false, + unlimitedSoundStuffs: false + }; + + /** + * A reference to the current runtime stepping interval, set + * by a `setInterval`. + */ + _steppingInterval: ReturnType | null = null; + + /** + * Configured framerate. + */ + framerate = 60; + + /** + * Current length of a step. Equals to 1000 / this.framerate. + * Changes as mode switches, and used by the sequencer to calculate + * WORK_TIME. + */ + currentStepTime: number | null = null; + + /** + * Whether any primitive has requested a redraw. + * Affects whether `Sequencer.stepThreads` will yield + * after stepping each thread. + * Reset on every frame. + */ + redrawRequested = false; + + /** + * Get stage width. + */ + stageWidth = 480; + + /** + * Get stage height. + */ + stageHeight = 360; + + // Register and initialize "IO devices", containers for processing + // I/O related data. + ioDevices = { + clock: new Clock(this), + cloud: new Cloud(this), + keyboard: new Keyboard(this), + mouse: new Mouse(this), + joystick: new Joystick(this), + mouseWheel: new MouseWheel(this), + userData: new UserData(), + video: new Video(this) + }; + + /** + * A list of extensions, used to manage hardware connection. + */ + peripheralExtensions: Record = {}; + + /** + * A runtime profiler that records timed events for later playback to + * diagnose Scratch performance. + */ + profiler: Profiler | null = null; + + /** + * A string representing the origin of the current project from outside of the + * Scratch community, such as CSFirst. + */ + origin: string | null = null; + + /** + * Check wether the runtime has any cloud data. + * @returns Whether or not the runtime currently has any + * cloud variables. + */ + hasCloudData: () => boolean; + /** + * A function which checks whether a new cloud variable can be added + * to the runtime. + * @returns Whether or not a new cloud variable can be added + * to the runtime. + */ + canAddCloudVariable: () => boolean; + /** + * A function that tracks a new cloud variable in the runtime, + * updating the cloud variable limit. Calling this function will + * emit a cloud data update event if this is the first cloud variable + * being added. + */ + addCloudVariable: () => void; + /** + * A function which updates the runtime's cloud variable limit + * when removing a cloud variable and emits a cloud update event + * if the last of the cloud variables is being removed. + */ + removeCloudVariable: () => void; + audioEngine?: AudioEngine; + renderer?: RenderWebGL; + v2BitmapAdapter?: BitmapAdapter; + storage?: ScratchStorage; + + _linkSocketFactory: ScratchLinkSocketFactory | null = null; + constructor () { + super(); + // Set an intial value for this.currentMSecs + this.updateCurrentMSecs(); + + // Register all given block packages. + this._registerBlockPackages(); + + const newCloudDataManager = cloudDataManager(); + this.hasCloudData = newCloudDataManager.hasCloudVariables; + this.canAddCloudVariable = newCloudDataManager.canAddCloudVariable; + this.addCloudVariable = this._initializeAddCloudVariable(newCloudDataManager); + this.removeCloudVariable = this._initializeRemoveCloudVariable(newCloudDataManager); this._initScratchLink(); } @@ -665,318 +555,317 @@ class Runtime extends EventEmitter { /** * Stage width in pixels. * @deprecated Use `runtime.stageWidth` instead. - * @returns {number} The stage width in pixels. + * @returns The stage width in pixels. */ static get STAGE_WIDTH () { - return 480; + return 480 as const; } /** * Stage height in pixels. * @deprecated Use `runtime.stageHeight` instead. - * @returns {number} The stage height in pixels. + * @returns The stage height in pixels. */ static get STAGE_HEIGHT () { - return 360; + return 360 as const; } /** * Event name for stage size update. - * @returns {'STAGE_SIZE_UPDATE'} The event name. + * @returns The event name. */ static get STAGE_SIZE_UPDATE () { - return 'STAGE_SIZE_UPDATE'; + return 'STAGE_SIZE_UPDATE' as const; } /** * Event name for glowing a script. - * @returns {'SCRIPT_GLOW_ON'} The event name. + * @returns The event name. */ static get SCRIPT_GLOW_ON () { - return 'SCRIPT_GLOW_ON'; + return 'SCRIPT_GLOW_ON' as const; } /** * Event name for unglowing a script. - * @returns {'SCRIPT_GLOW_OFF'} The event name. + * @returns The event name. */ static get SCRIPT_GLOW_OFF () { - return 'SCRIPT_GLOW_OFF'; + return 'SCRIPT_GLOW_OFF' as const; } /** * Event name for glowing a block. - * @returns {'BLOCK_GLOW_ON'} The event name. + * @returns The event name. */ static get BLOCK_GLOW_ON () { - return 'BLOCK_GLOW_ON'; + return 'BLOCK_GLOW_ON' as const; } /** * Event name for unglowing a block. - * @returns {'BLOCK_GLOW_OFF'} The event name. + * @returns The event name. */ static get BLOCK_GLOW_OFF () { - return 'BLOCK_GLOW_OFF'; + return 'BLOCK_GLOW_OFF' as const; } /** * Event name for a cloud data update * to this project. - * @returns {'HAS_CLOUD_DATA_UPDATE'} The event name. + * @returns The event name. */ static get HAS_CLOUD_DATA_UPDATE () { - return 'HAS_CLOUD_DATA_UPDATE'; + return 'HAS_CLOUD_DATA_UPDATE' as const; } /** * Event name for turning on turbo mode. - * @returns {'TURBO_MODE_ON'} The event name. + * @returns The event name. */ static get TURBO_MODE_ON () { - return 'TURBO_MODE_ON'; + return 'TURBO_MODE_ON' as const; } /** * Event name for turning off turbo mode. - * @returns {'TURBO_MODE_OFF'} The event name. + * @returns The event name. */ static get TURBO_MODE_OFF () { - return 'TURBO_MODE_OFF'; + return 'TURBO_MODE_OFF' as const; } /** * Event name when the project is started (threads may not necessarily be * running). - * @returns {'PROJECT_START'} The event name. + * @returns The event name. */ static get PROJECT_START () { - return 'PROJECT_START'; + return 'PROJECT_START' as const; } /** * Event name when threads start running. * Used by the UI to indicate running status. - * @returns {'PROJECT_RUN_START'} The event name. + * @returns The event name. */ static get PROJECT_RUN_START () { - return 'PROJECT_RUN_START'; + return 'PROJECT_RUN_START' as const; } /** * Event name when threads stop running * Used by the UI to indicate not-running status. - * @returns {'PROJECT_RUN_STOP'} The event name. + * @returns The event name. */ static get PROJECT_RUN_STOP () { - return 'PROJECT_RUN_STOP'; + return 'PROJECT_RUN_STOP' as const; } /** * Event name for project being stopped or restarted by the user. * Used by blocks that need to reset state. - * @returns {'PROJECT_STOP_ALL'} The event name. + * @returns The event name. */ static get PROJECT_STOP_ALL () { - return 'PROJECT_STOP_ALL'; + return 'PROJECT_STOP_ALL' as const; } /** * Event name for target being stopped by a stop for target call. * Used by blocks that need to stop individual targets. - * @returns {'STOP_FOR_TARGET'} The event name. + * @returns The event name. */ static get STOP_FOR_TARGET () { - return 'STOP_FOR_TARGET'; + return 'STOP_FOR_TARGET' as const; } /** * Event name for visual value report. - * @returns {'VISUAL_REPORT'} The event name. + * @returns The event name. */ static get VISUAL_REPORT () { - return 'VISUAL_REPORT'; + return 'VISUAL_REPORT' as const; } /** * Event name for project loaded report. - * @returns {'PROJECT_LOADED'} The event name. + * @returns The event name. */ static get PROJECT_LOADED () { - return 'PROJECT_LOADED'; + return 'PROJECT_LOADED' as const; } /** * Event name for report that a change was made that can be saved - * @returns {'PROJECT_CHANGED'} The event name. + * @returns The event name. */ static get PROJECT_CHANGED () { - return 'PROJECT_CHANGED'; + return 'PROJECT_CHANGED' as const; } /** * Event name for report that a change was made to an extension in the toolbox. - * @returns {'TOOLBOX_EXTENSIONS_NEED_UPDATE'} The event name. + * @returns The event name. */ static get TOOLBOX_EXTENSIONS_NEED_UPDATE () { - return 'TOOLBOX_EXTENSIONS_NEED_UPDATE'; + return 'TOOLBOX_EXTENSIONS_NEED_UPDATE' as const; } /** * Event name for targets update report. - * @returns {'TARGETS_UPDATE'} The event name. + * @returns The event name. */ static get TARGETS_UPDATE () { - return 'TARGETS_UPDATE'; + return 'TARGETS_UPDATE' as const; } /** * Event name for monitors update. - * @returns {'MONITORS_UPDATE'} The event name. + * @returns The event name. */ static get MONITORS_UPDATE () { - return 'MONITORS_UPDATE'; + return 'MONITORS_UPDATE' as const; } /** * Event name for block drag update. - * @returns {'BLOCK_DRAG_UPDATE'} The event name. + * @returns The event name. */ static get BLOCK_DRAG_UPDATE () { - return 'BLOCK_DRAG_UPDATE'; + return 'BLOCK_DRAG_UPDATE' as const; } /** * Event name for block drag end. - * @returns {'BLOCK_DRAG_END'} The event name. + * @returns The event name. */ static get BLOCK_DRAG_END () { - return 'BLOCK_DRAG_END'; + return 'BLOCK_DRAG_END' as const; } /** * Event name for reporting that an extension was added. - * @returns {'EXTENSION_ADDED'} The event name. + * @returns The event name. */ static get EXTENSION_ADDED () { - return 'EXTENSION_ADDED'; + return 'EXTENSION_ADDED' as const; } /** * Event name for reporting that an extension as asked for a custom field to be added - * @returns {'EXTENSION_FIELD_ADDED'} The event name. + * @returns The event name. */ static get EXTENSION_FIELD_ADDED () { - return 'EXTENSION_FIELD_ADDED'; + return 'EXTENSION_FIELD_ADDED' as const; } /** * Event name for updating the available set of peripheral devices. * This causes the peripheral connection modal to update a list of * available peripherals. - * @returns {'PERIPHERAL_LIST_UPDATE'} The event name. + * @returns The event name. */ static get PERIPHERAL_LIST_UPDATE () { - return 'PERIPHERAL_LIST_UPDATE'; + return 'PERIPHERAL_LIST_UPDATE' as const; } /** * Event name for when the user picks a bluetooth device to connect to * via Companion Device Manager (CDM) - * @returns {'USER_PICKED_PERIPHERAL'} The event name. + * @returns The event name. */ static get USER_PICKED_PERIPHERAL () { - return 'USER_PICKED_PERIPHERAL'; + return 'USER_PICKED_PERIPHERAL' as const; } /** * Event name for reporting that a peripheral has connected. * This causes the status button in the blocks menu to indicate 'connected'. - * @returns {'PERIPHERAL_CONNECTED'} The event name. + * @returns The event name. */ static get PERIPHERAL_CONNECTED () { - return 'PERIPHERAL_CONNECTED'; + return 'PERIPHERAL_CONNECTED' as const; } /** * Event name for reporting that a peripheral has been intentionally disconnected. * This causes the status button in the blocks menu to indicate 'disconnected'. - * @returns {'PERIPHERAL_DISCONNECTED'} The event name. + * @returns The event name. */ static get PERIPHERAL_DISCONNECTED () { - return 'PERIPHERAL_DISCONNECTED'; + return 'PERIPHERAL_DISCONNECTED' as const; } /** * Event name for reporting that a peripheral has encountered a request error. * This causes the peripheral connection modal to switch to an error state. - * @returns {'PERIPHERAL_REQUEST_ERROR'} The event name. + * @returns The event name. */ static get PERIPHERAL_REQUEST_ERROR () { - return 'PERIPHERAL_REQUEST_ERROR'; + return 'PERIPHERAL_REQUEST_ERROR' as const; } /** * Event name for reporting that a peripheral connection has been lost. * This causes a 'peripheral connection lost' error alert to display. - * @returns {'PERIPHERAL_CONNECTION_LOST_ERROR'} The event name. + * @returns The event name. */ static get PERIPHERAL_CONNECTION_LOST_ERROR () { - return 'PERIPHERAL_CONNECTION_LOST_ERROR'; + return 'PERIPHERAL_CONNECTION_LOST_ERROR' as const; } /** * Event name for reporting that a peripheral has not been discovered. * This causes the peripheral connection modal to show a timeout state. - * @returns {'PERIPHERAL_SCAN_TIMEOUT'} The event name. + * @returns The event name. */ static get PERIPHERAL_SCAN_TIMEOUT () { - return 'PERIPHERAL_SCAN_TIMEOUT'; + return 'PERIPHERAL_SCAN_TIMEOUT' as const; } /** * Event name to indicate that the microphone is being used to stream audio. - * @returns {'MIC_LISTENING'} The event name. + * @returns The event name. */ static get MIC_LISTENING () { - return 'MIC_LISTENING'; + return 'MIC_LISTENING' as const; } /** * Event name for reporting that blocksInfo was updated. - * @returns {'BLOCKSINFO_UPDATE'} The event name. + * @returns The event name. */ static get BLOCKSINFO_UPDATE () { - return 'BLOCKSINFO_UPDATE'; + return 'BLOCKSINFO_UPDATE' as const; } /** * Event name when the runtime tick loop has been started. - * @returns {'RUNTIME_STARTED'} The event name. + * @returns The event name. */ static get RUNTIME_STARTED () { - return 'RUNTIME_STARTED'; + return 'RUNTIME_STARTED' as const; } /** * Event name when the runtime dispose has been called. - * @returns {'RUNTIME_DISPOSED'} The event name. + * @returns The event name. */ static get RUNTIME_DISPOSED () { - return 'RUNTIME_DISPOSED'; + return 'RUNTIME_DISPOSED' as const; } /** * Event name for reporting that a block was updated and needs to be rerendered. - * @returns {'BLOCKS_NEED_UPDATE'} The event name. */ static get BLOCKS_NEED_UPDATE () { - return 'BLOCKS_NEED_UPDATE'; + return 'BLOCKS_NEED_UPDATE' as const; } /** * How rapidly we try to step threads by default, in ms. - * @returns {number} The default thread step interval in milliseconds. + * @returns The default thread step interval in milliseconds. */ static get THREAD_STEP_INTERVAL () { return 1000 / 60; @@ -984,7 +873,7 @@ class Runtime extends EventEmitter { /** * In compatibility mode, how rapidly we try to step threads, in ms. - * @returns {number} The compatibility thread step interval in milliseconds. + * @returns The compatibility thread step interval in milliseconds. */ static get THREAD_STEP_INTERVAL_COMPATIBILITY () { return 1000 / 30; @@ -992,7 +881,7 @@ class Runtime extends EventEmitter { /** * How many clones can be created at a time. - * @returns {number} The maximum number of clones allowed. + * @returns The maximum number of clones allowed. */ get MAX_CLONES () { return this.limitOptions.infiniteCloning ? Infinity : 300; @@ -1002,7 +891,7 @@ class Runtime extends EventEmitter { // ----------------------------------------------------------------------------- // Helper function for initializing the addCloudVariable function - _initializeAddCloudVariable (newCloudDataManager) { + _initializeAddCloudVariable (newCloudDataManager: CloudDataManager) { // The addCloudVariable function return (() => { const hadCloudVarsBefore = this.hasCloudData(); @@ -1014,7 +903,7 @@ class Runtime extends EventEmitter { } // Helper function for initializing the removeCloudVariable function - _initializeRemoveCloudVariable (newCloudDataManager) { + _initializeRemoveCloudVariable (newCloudDataManager: CloudDataManager) { return (() => { const hadCloudVarsBefore = this.hasCloudData(); newCloudDataManager.removeCloudVariable(); @@ -1033,36 +922,38 @@ class Runtime extends EventEmitter { for (const packageName in defaultBlockPackages) { if (Object.prototype.hasOwnProperty.call(defaultBlockPackages, packageName)) { // @todo pass a different runtime depending on package privilege? - const packageObject = new (defaultBlockPackages[packageName])(this); + const packageObject = + new (defaultBlockPackages[packageName as keyof typeof defaultBlockPackages])(this); // Collect primitives from package. - if (packageObject.getPrimitives) { + if ('getPrimitives' in packageObject) { const packagePrimitives = packageObject.getPrimitives(); for (const op in packagePrimitives) { if (Object.prototype.hasOwnProperty.call(packagePrimitives, op)) { this._primitives[op] = - packagePrimitives[op].bind(packageObject); + // @ts-expect-error use bind to ensure correct `this` context for primitives + packagePrimitives[op as keyof typeof packagePrimitives].bind(packageObject); } } } // Collect hat metadata from package. - if (packageObject.getHats) { + if ('getHats' in packageObject) { const packageHats = packageObject.getHats(); for (const hatName in packageHats) { if (Object.prototype.hasOwnProperty.call(packageHats, hatName)) { - this._hats[hatName] = packageHats[hatName]; + this._hats[hatName] = packageHats[hatName as keyof typeof packageHats]; } } } // Collect monitored from package. - if (packageObject.getMonitored) { + if ('getMonitored' in packageObject) { this.monitorBlockInfo = Object.assign({}, this.monitorBlockInfo, packageObject.getMonitored()); } // Collect execution orders from package. - if (packageObject.getOrders) { + if ('getOrders' in packageObject) { const packageOrders = packageObject.getOrders(); for (const op in packageOrders) { if (Object.prototype.hasOwnProperty.call(packageOrders, op)) { - this._orders[op] = packageOrders[op]; + this._orders[op] = packageOrders[op as keyof typeof packageOrders]; } } } @@ -1076,42 +967,46 @@ class Runtime extends EventEmitter { /** * Generate an extension-specific menu ID. - * @param {string} menuName - the name of the menu. - * @param {string} extensionId - the ID of the extension hosting the menu. - * @returns {string} - the constructed ID. + * @param menuName - the name of the menu. + * @param extensionId - the ID of the extension hosting the menu. + * @returns the constructed ID. * @private */ - _makeExtensionMenuId (menuName, extensionId) { + _makeExtensionMenuId (menuName: string, extensionId: string) { return `${extensionId}_menu_${xmlEscape(menuName)}`; } /** * Create a context ("args") object for use with `formatMessage` on messages which might be target-specific. - * @param {RenderedTarget} [target] - the target to use as context. + * @param target the target to use as context. * If a target is not provided, default to the current * editing target or the stage. */ - makeMessageContextForTarget (target) { - const context = {}; + makeMessageContextForTarget (target?: RenderedTarget) { // eslint-disable-line @typescript-eslint/no-unused-vars + // Not implemented + /* target = target || this.getEditingTarget() || this.getTargetForStage(); if (target) { - context.targetType = (target.isStage ? TargetType.STAGE : TargetType.SPRITE); + const context = { + targetType: (target.isStage ? TargetType.STAGE : TargetType.SPRITE) + }; } + */ } /** * Register the primitives provided by an extension. - * @param {ExtensionMetadata} extensionInfo - information about the extension (id, blocks, etc.) + * @param extensionInfo - information about the extension (id, blocks, etc.) * @private */ - _registerExtensionPrimitives (extensionInfo) { + _registerExtensionPrimitives (extensionInfo: NormalizedExtensionMetadata) { const categoryInfo = { id: extensionInfo.id, name: maybeFormatMessage(extensionInfo.name), showStatusButton: extensionInfo.showStatusButton, blockIconURI: extensionInfo.blockIconURI, menuIconURI: extensionInfo.menuIconURI - }; + } as CategoryInfo; if (extensionInfo.color1) { categoryInfo.color1 = extensionInfo.color1; @@ -1144,10 +1039,10 @@ class Runtime extends EventEmitter { /** * Reregister the primitives for an extension - * @param {ExtensionMetadata} extensionInfo - new info (results of running getInfo) for an extension + * @param extensionInfo - new info (results of running getInfo) for an extension * @private */ - _refreshExtensionPrimitives (extensionInfo) { + _refreshExtensionPrimitives (extensionInfo: NormalizedExtensionMetadata) { const categoryInfo = this._blockInfo.find(info => info.id === extensionInfo.id); if (categoryInfo) { categoryInfo.name = maybeFormatMessage(extensionInfo.name); @@ -1160,11 +1055,11 @@ class Runtime extends EventEmitter { /** * Read extension information, convert menus, blocks and custom field types * and store the results in the provided category object. - * @param {CategoryInfo} categoryInfo - the category to be filled - * @param {ExtensionMetadata} extensionInfo - the extension metadata to read + * @param categoryInfo - the category to be filled + * @param extensionInfo - the extension metadata to read * @private */ - _fillExtensionCategory (categoryInfo, extensionInfo) { + _fillExtensionCategory (categoryInfo: CategoryInfo, extensionInfo: NormalizedExtensionMetadata) { categoryInfo.blocks = []; categoryInfo.customFieldTypes = {}; categoryInfo.menus = []; @@ -1192,14 +1087,15 @@ class Runtime extends EventEmitter { } } - for (const blockInfo of extensionInfo.blocks) { + for (const metadata of extensionInfo.blocks) { try { - const convertedBlock = this._convertForScratchBlocks(blockInfo, categoryInfo); + const convertedBlock = this._convertForScratchBlocks(metadata, categoryInfo); categoryInfo.blocks.push(convertedBlock); - if (convertedBlock.json) { + if ('json' in convertedBlock) { + const blockInfo = metadata as NormalizedExtensionBlockMetadata; const opcode = convertedBlock.json.type; if (blockInfo.blockType !== BlockType.EVENT) { - this._primitives[opcode] = convertedBlock.info.func; + this._primitives[opcode] = convertedBlock.info.func!; } if (blockInfo.blockType === BlockType.EVENT || blockInfo.blockType === BlockType.HAT) { this._hats[opcode] = { @@ -1209,7 +1105,7 @@ class Runtime extends EventEmitter { } } } catch (e) { - log.error('Error parsing block: ', {block: blockInfo, error: e}); + log.error('Error parsing block: ', {block: metadata, error: e}); } } } @@ -1217,38 +1113,42 @@ class Runtime extends EventEmitter { /** * Convert the given extension menu items into the scratch-blocks style of list of pairs. * If the menu is dynamic (e.g. the passed in argument is a function), return the input unmodified. - * @param {Array|(() => Array)} menuItems - An array of menu items or a function + * @param menuItems - An array of menu items or a function * to retrieve them. - * @returns {Array<[string, unknown]>|(() => Array)} An array of pairs or the original input function. + * @returns An array of pairs or the original input function. * @private */ - _convertMenuItems (menuItems) { + _convertMenuItems (menuItems: ShortExtensionMenuItem | string[]): MenuGenerator { if (typeof menuItems !== 'function') { - const extensionMessageContext = this.makeMessageContextForTarget(); return menuItems.map(item => { - const formattedItem = maybeFormatMessage(item, extensionMessageContext); + const formattedItem = maybeFormatMessage(item); switch (typeof formattedItem) { case 'string': return [formattedItem, formattedItem]; case 'object': - return [maybeFormatMessage(item.text, extensionMessageContext), item.value]; + return [ + maybeFormatMessage((item as unknown as ExtensionMenuItemObject).text), + (item as unknown as ExtensionMenuItemObject).value + ]; default: throw new Error(`Can't interpret menu item: ${JSON.stringify(item)}`); } }); } - return menuItems; + // Not sure whether modern blockly still can construct dynamic menu from field json config, + // just keep original behavior + return menuItems as unknown as MenuGenerator; } /** * Build the scratch-blocks JSON for a menu. Note that scratch-blocks treats menus as a special kind of block. - * @param {string} menuName - the name of the menu - * @param {ExtensionMenuInfo} menuInfo - a description of this menu and its items - * @param {CategoryInfo} categoryInfo - the category for this block - * @returns {ScratchBlocksDefinition} The menu block definition for scratch-blocks. + * @param menuName - the name of the menu + * @param menuInfo - a description of this menu and its items + * @param categoryInfo - the category for this block + * @returns The menu block definition for scratch-blocks. * @private */ - _buildMenuForScratchBlocks (menuName, menuInfo, categoryInfo) { + _buildMenuForScratchBlocks (menuName: string, menuInfo: NormalizedExtensionMenuItem, categoryInfo: CategoryInfo) { const menuId = this._makeExtensionMenuId(menuName, categoryInfo.id); const menuItems = this._convertMenuItems(menuInfo.items); return { @@ -1275,13 +1175,18 @@ class Runtime extends EventEmitter { /** * Build the runtime metadata for an extension-defined custom field type. - * @param {string} fieldName - The field name from the extension metadata. - * @param {ExtensionCustomFieldTypeMetadata} fieldInfo - The extension-defined custom field metadata. - * @param {string} extensionId - The ID of the extension providing the field. - * @param {CategoryInfo} categoryInfo - The category the field belongs to. - * @returns {ExtensionCustomFieldTypeInfo} The runtime metadata for the custom field type. - */ - _buildCustomFieldInfo (fieldName, fieldInfo, extensionId, categoryInfo) { + * @param fieldName - The field name from the extension metadata. + * @param fieldInfo - The extension-defined custom field metadata. + * @param extensionId - The ID of the extension providing the field. + * @param categoryInfo - The category the field belongs to. + * @returns The runtime metadata for the custom field type. + */ + _buildCustomFieldInfo ( + fieldName: string, + fieldInfo: ExtensionCustomFieldTypeMetadata, + extensionId: string, + categoryInfo: CategoryInfo + ) { const extendedName = `${extensionId}_${fieldName}`; return { fieldName: fieldName, @@ -1305,13 +1210,18 @@ class Runtime extends EventEmitter { /** * Build the scratch-blocks JSON needed for a fieldType. * Custom field types need to be namespaced to the extension so that extensions can't interfere with each other - * @param {string} fieldName - The name of the field - * @param {string} output - The output of the field - * @param {number} outputShape - Shape of the field (from ScratchBlocksConstants) - * @param {CategoryInfo} categoryInfo - The category the field belongs to. - * @returns {ScratchBlocksDefinition} The scratch-blocks definition for the custom field. - */ - _buildCustomFieldTypeForScratchBlocks (fieldName, output, outputShape, categoryInfo) { + * @param fieldName - The name of the field + * @param output - The output of the field + * @param outputShape - Shape of the field (from ScratchBlocksConstants) + * @param categoryInfo - The category the field belongs to. + * @returns The scratch-blocks definition for the custom field. + */ + _buildCustomFieldTypeForScratchBlocks ( + fieldName: string, + output: JsonBlockDefinition['output'], + outputShape: JsonBlockDefinition['outputShape'], + categoryInfo: CategoryInfo + ) { return { json: { type: fieldName, @@ -1334,12 +1244,15 @@ class Runtime extends EventEmitter { /** * Convert ExtensionBlockMetadata into data ready for scratch-blocks. - * @param {ExtensionBlockMetadata|string} blockInfo - the block info to convert - * @param {CategoryInfo} categoryInfo - the category for this block - * @returns {ConvertedBlockInfo} - the converted & original block information + * @param blockInfo - the block info to convert + * @param categoryInfo - the category for this block + * @returns the converted & original block information * @private */ - _convertForScratchBlocks (blockInfo, categoryInfo) { + _convertForScratchBlocks ( + blockInfo: NormalizedExtensionItemMetadata, + categoryInfo: CategoryInfo + ) { if (blockInfo === '---') { return this._convertSeparatorForScratchBlocks(blockInfo); } @@ -1353,23 +1266,23 @@ class Runtime extends EventEmitter { /** * Convert ExtensionBlockMetadata into scratch-blocks JSON & XML, and generate a proxy function. - * @param {ExtensionBlockMetadata} blockInfo - the block to convert - * @param {CategoryInfo} categoryInfo - the category for this block - * @returns {ConvertedBlockInfo} - the converted & original block information + * @param blockInfo - the block to convert + * @param categoryInfo - the category for this block + * @returns the converted & original block information * @private */ - _convertBlockForScratchBlocks (blockInfo, categoryInfo) { + _convertBlockForScratchBlocks (blockInfo: NormalizedExtensionBlockMetadata, categoryInfo: CategoryInfo) { const extendedOpcode = `${categoryInfo.id}_${blockInfo.opcode}`; - const blockJSON = { + const blockJSON: JsonBlockDefinition = { type: extendedOpcode, inputsInline: true, - category: categoryInfo.name, - colour: categoryInfo.color1, - colourSecondary: categoryInfo.color2, - colourTertiary: categoryInfo.color3 + // category: categoryInfo.name, + colour: categoryInfo.color1 + // colourSecondary: categoryInfo.color2, + // colourTertiary: categoryInfo.color3 }; - const context = { + const context: PlaceholderContext = { // TODO: store this somewhere so that we can map args appropriately after translation. // This maps an arg name to its relative position in the original (usually English) block text. // When displaying a block in another language we'll need to run a `replace` action similar to the one @@ -1445,13 +1358,13 @@ class Runtime extends EventEmitter { let inBranchNum = 0; // how many branches have we placed into the JSON so far? let outLineNum = 0; // used for scratch-blocks `message${outLineNum}` and `args${outLineNum}` const convertPlaceholders = this._convertPlaceholders.bind(this, context); - const extensionMessageContext = this.makeMessageContextForTarget(); + // const extensionMessageContext = this.makeMessageContextForTarget(); // alternate between a block "arm" with text on it and an open slot for a substack - while (inTextNum < blockText.length || inBranchNum < blockInfo.branchCount) { + while (inTextNum < blockText.length || inBranchNum < (blockInfo.branchCount ?? 0)) { if (inTextNum < blockText.length) { context.outLineNum = outLineNum; - const lineText = maybeFormatMessage(blockText[inTextNum], extensionMessageContext); + const lineText: string = maybeFormatMessage(blockText[inTextNum]); const convertedText = lineText.replace(/\[(.+?)]/g, convertPlaceholders); if (blockJSON[`message${outLineNum}`]) { blockJSON[`message${outLineNum}`] += convertedText; @@ -1461,7 +1374,7 @@ class Runtime extends EventEmitter { ++inTextNum; ++outLineNum; } - if (inBranchNum < blockInfo.branchCount) { + if (inBranchNum < (blockInfo.branchCount ?? 0)) { blockJSON[`message${outLineNum}`] = '%1'; blockJSON[`args${outLineNum}`] = [{ type: 'input_statement', @@ -1478,7 +1391,7 @@ class Runtime extends EventEmitter { } } else if (blockInfo.blockType === BlockType.LOOP) { // Add icon to the bottom right of a loop block - blockJSON[`lastDummyAlign${outLineNum}`] = 'RIGHT'; + blockJSON[`implicitAlign${outLineNum}`] = 'RIGHT'; blockJSON[`message${outLineNum}`] = '%1'; blockJSON[`args${outLineNum}`] = [{ type: 'field_image', @@ -1504,11 +1417,11 @@ class Runtime extends EventEmitter { /** * Generate a separator between blocks categories or sub-categories. - * @param {string} blockInfo - the separator marker to convert - * @returns {ConvertedBlockInfo} - the converted & original block information + * @param blockInfo - the separator marker to convert + * @returns the converted & original block information * @private */ - _convertSeparatorForScratchBlocks (blockInfo) { + _convertSeparatorForScratchBlocks (blockInfo: '---') { return { info: blockInfo, xml: '' @@ -1517,19 +1430,18 @@ class Runtime extends EventEmitter { /** * Convert a button for scratch-blocks. A button has no opcode but specifies a callback name in the `func` field. - * @param {ExtensionButtonMetadata} buttonInfo - the button to convert - * @returns {ConvertedBlockInfo} - the converted & original button information + * @param buttonInfo - the button to convert + * @returns the converted & original button information * @private */ - _convertButtonForScratchBlocks (buttonInfo) { + _convertButtonForScratchBlocks (buttonInfo: ExtensionButtonMetadata) { // for now we only support these pre-defined callbacks handled in scratch-blocks const supportedCallbackKeys = ['MAKE_A_LIST', 'MAKE_A_PROCEDURE', 'MAKE_A_VARIABLE']; - if (supportedCallbackKeys.indexOf(buttonInfo.func) < 0) { + if (supportedCallbackKeys.indexOf(buttonInfo.func!) < 0) { log.error(`Custom button callbacks not supported yet: ${buttonInfo.func}`); } - const extensionMessageContext = this.makeMessageContextForTarget(); - const buttonText = maybeFormatMessage(buttonInfo.text, extensionMessageContext); + const buttonText = maybeFormatMessage(buttonInfo.text); return { info: buttonInfo, xml: `` @@ -1538,11 +1450,11 @@ class Runtime extends EventEmitter { /** * Helper for _convertPlaceholdes which handles inline images which are a specialized case of block "arguments". - * @param {ExtensionArgumentInfo} argInfo Metadata about the inline image as specified by the extension. - * @returns {ScratchBlocksJson} The scratch-blocks JSON for the inline image field. + * @param argInfo Metadata about the inline image as specified by the extension. + * @returns The scratch-blocks JSON for the inline image field. * @private */ - _constructInlineImageJson (argInfo) { + _constructInlineImageJson (argInfo: ExtensionImageMetadata) { if (!argInfo.dataURI) { log.warn('Missing data URI in extension block with argument type IMAGE'); } @@ -1562,18 +1474,19 @@ class Runtime extends EventEmitter { /** * Helper for _convertForScratchBlocks which handles linearization of argument placeholders. Called as a callback * from string#replace. In addition to the return value the JSON and XML items in the context will be filled. - * @param {PlaceholderContext} context - Information shared with _convertForScratchBlocks about the block. - * @param {string} match - the overall string matched by the placeholder regex, including brackets: '[FOO]'. - * @param {string} placeholder - the name of the placeholder being matched: 'FOO'. - * @returns {string} The scratch-blocks placeholder for the argument, such as '%1'. + * @param context - Information shared with _convertForScratchBlocks about the block. + * @param match - the overall string matched by the placeholder regex, including brackets: '[FOO]'. + * @param placeholder - the name of the placeholder being matched: 'FOO'. + * @returns The scratch-blocks placeholder for the argument, such as '%1'. * @private */ - _convertPlaceholders (context, match, placeholder) { + _convertPlaceholders (context: PlaceholderContext, match: string, placeholder: string) { // Sanitize the placeholder to ensure valid XML placeholder = placeholder.replace(/[<"&]/, '_'); // Determine whether the argument type is one of the known standard field types - const argInfo = context.blockInfo.arguments[placeholder] || {}; + const argInfo: ExtensionArgumentMetadata = + context.blockInfo.arguments?.[placeholder] || {} as ExtensionArgumentMetadata; let argTypeInfo = ArgumentTypeMap[argInfo.type] || {}; // Field type not a standard field type, see if extension has registered custom field type @@ -1583,12 +1496,12 @@ class Runtime extends EventEmitter { // Start to construct the scratch-blocks style JSON defining how the block should be // laid out - let argJSON; + let argJSON: JsonBlockArg; // Most field types are inputs (slots on the block that can have other blocks plugged into them) // check if this is not one of those cases. E.g. an inline image on a block. - if (argTypeInfo.fieldType === 'field_image') { - argJSON = this._constructInlineImageJson(argInfo); + if ((argTypeInfo as (typeof ArgumentTypeMap)['image']).fieldType === 'field_image') { + argJSON = this._constructInlineImageJson(argInfo as ExtensionImageMetadata); } else { // Construct input value @@ -1600,13 +1513,13 @@ class Runtime extends EventEmitter { const defaultValue = typeof argInfo.defaultValue === 'undefined' ? '' : - xmlEscape(maybeFormatMessage(argInfo.defaultValue, this.makeMessageContextForTarget()).toString()); + xmlEscape(maybeFormatMessage(argInfo.defaultValue).toString()); - if (argTypeInfo.check) { + if ((argTypeInfo as (typeof ArgumentTypeMap)['Boolean']).check) { // Right now the only type of 'check' we have specifies that the // input slot on the block accepts Boolean reporters, so it should be // shaped like a hexagon - argJSON.check = argTypeInfo.check; + argJSON.check = (argTypeInfo as (typeof ArgumentTypeMap)['Boolean']).check; } let valueName; @@ -1620,15 +1533,15 @@ class Runtime extends EventEmitter { fieldName = argInfo.menu; } else { argJSON.type = 'field_dropdown'; - argJSON.options = this._convertMenuItems(menuInfo.items); + (argJSON as FieldDropdownArg).options = this._convertMenuItems(menuInfo.items); valueName = null; shadowType = null; fieldName = placeholder; } } else { valueName = placeholder; - shadowType = (argTypeInfo.shadow && argTypeInfo.shadow.type) || null; - fieldName = (argTypeInfo.shadow && argTypeInfo.shadow.fieldName) || null; + shadowType = ((argTypeInfo as (typeof ArgumentTypeMap)[ArgumentType.STRING]).shadow?.type) || null; + fieldName = ((argTypeInfo as (typeof ArgumentTypeMap)[ArgumentType.STRING]).shadow?.fieldName) || null; } // is the ScratchBlocks name for a block input. @@ -1657,7 +1570,7 @@ class Runtime extends EventEmitter { } } - const argsName = `args${context.outLineNum}`; + const argsName = `args${context.outLineNum!}` as const; const blockArgs = (context.blockJSON[argsName] = context.blockJSON[argsName] || []); if (argJSON) blockArgs.push(argJSON); const argNum = blockArgs.length; @@ -1668,10 +1581,10 @@ class Runtime extends EventEmitter { /** * Get scratch-blocks XML for each extension category. - * @param {RenderedTarget|undefined} target - the active editing target, if any. - * @returns {Array} Scratch-blocks XML for each category of extension blocks. + * @param target - the active editing target, if any. + * @returns Scratch-blocks XML for each category of extension blocks. */ - getBlocksXML (target) { + getBlocksXML (target?: RenderedTarget) { return this._blockInfo.map(categoryInfo => { const {name, color1, color2} = categoryInfo; // Filter out blocks that aren't supposed to be shown on this target, as determined by the block info's @@ -1680,13 +1593,13 @@ class Runtime extends EventEmitter { let blockFilterIncludesTarget = true; // If an editing target is not passed, include all blocks // If the block info doesn't include a `filter` property, always include it - if (target && block.info.filter) { - blockFilterIncludesTarget = block.info.filter.includes( + if (target && (block.info as NormalizedExtensionBlockMetadata).filter) { + blockFilterIncludesTarget = (block.info as NormalizedExtensionBlockMetadata).filter!.includes( target.isStage ? TargetType.STAGE : TargetType.SPRITE ); } // If the block info's `hideFromPalette` is true, then filter out this block - return blockFilterIncludesTarget && !block.info.hideFromPalette; + return blockFilterIncludesTarget && !(block.info as NormalizedExtensionBlockMetadata).hideFromPalette; }); const colorXML = `colour="${color1}" secondaryColour="${color2}"`; @@ -1718,11 +1631,13 @@ class Runtime extends EventEmitter { /** * Get scratch-blocks JSON for each dynamic block. - * @returns {Array} The scratch-blocks JSON information for each dynamic block. + * @returns The scratch-blocks JSON information for each dynamic block. */ getBlocksJSON () { return this._blockInfo.reduce( - (result, categoryInfo) => result.concat(categoryInfo.blocks.map(blockInfo => blockInfo.json)), []); + (result: JsonBlockDefinition[], categoryInfo) => result.concat( + categoryInfo.blocks.filter(info => 'json' in info).map(blockInfo => blockInfo.json) + ), []); } /** @@ -1731,7 +1646,6 @@ class Runtime extends EventEmitter { _initScratchLink () { // Check that we're actually in a real browser, not Node.js or JSDOM, and we have a valid-looking origin. if (globalThis.document && - globalThis.document.getElementById && globalThis.origin && globalThis.origin !== 'null' && globalThis.navigator && @@ -1756,10 +1670,10 @@ class Runtime extends EventEmitter { /** * Get a scratch link socket. - * @param {string} type Either BLE or BT - * @returns {ScratchLinkSocket} The scratch link socket. + * @param type Either BLE or BT + * @returns The scratch link socket. */ - getScratchLinkSocket (type) { + getScratchLinkSocket (type: 'BLE' | 'BT') { const factory = this._linkSocketFactory || this._defaultScratchLinkSocketFactory; return factory(type); } @@ -1767,20 +1681,20 @@ class Runtime extends EventEmitter { /** * Configure how ScratchLink sockets are created. Factory must consume a "type" parameter * either BT or BLE. - * @param {ScratchLinkSocketFactory} factory The new factory for creating ScratchLink sockets. + * @param factory The new factory for creating ScratchLink sockets. */ - configureScratchLinkSocketFactory (factory) { + configureScratchLinkSocketFactory (factory: ScratchLinkSocketFactory) { this._linkSocketFactory = factory; } /** * The default scratch link socket creator, using websockets to the installed device manager. - * @param {string} type Either BLE or BT - * @returns {ScratchLinkSocket} The new scratch link socket. + * @param type Either BLE or BT + * @returns The new scratch link socket. */ - _defaultScratchLinkSocketFactory (type) { + _defaultScratchLinkSocketFactory (type: 'BLE' | 'BT') { const Scratch = self.Scratch; - const ScratchLinkSafariSocket = Scratch && Scratch.ScratchLinkSafariSocket; + const ScratchLinkSafariSocket = Scratch?.ScratchLinkSafariSocket; // detect this every time in case the user turns on the extension after loading the page const useSafariSocket = ScratchLinkSafariSocket && ScratchLinkSafariSocket.isSafariHelperCompatible(); return useSafariSocket ? new ScratchLinkSafariSocket(type) : new ScratchLinkWebSocket(type); @@ -1789,116 +1703,106 @@ class Runtime extends EventEmitter { /** * Register an extension that communications with a hardware peripheral by id, * to have access to it and its peripheral functions in the future. - * @param {string} extensionId - the id of the extension. - * @param {PeripheralExtension} extension - the extension to register. + * @param extensionId - the id of the extension. + * @param extension - the extension to register. */ - registerPeripheralExtension (extensionId, extension) { + registerPeripheralExtension (extensionId: string, extension: PeripheralExtensionClass) { this.peripheralExtensions[extensionId] = extension; } /** * Tell the specified extension to scan for a peripheral. - * @param {string} extensionId - the id of the extension. + * @param extensionId - the id of the extension. */ - scanForPeripheral (extensionId) { - if (this.peripheralExtensions[extensionId]) { - this.peripheralExtensions[extensionId].scan(); - } + scanForPeripheral (extensionId: string) { + this.peripheralExtensions[extensionId]?.scan(); } /** * Connect to the extension's specified peripheral. - * @param {string} extensionId - the id of the extension. - * @param {number} peripheralId - the id of the peripheral. + * @param extensionId - the id of the extension. + * @param peripheralId - the id of the peripheral. */ - connectPeripheral (extensionId, peripheralId) { - if (this.peripheralExtensions[extensionId]) { - this.peripheralExtensions[extensionId].connect(peripheralId); - } + connectPeripheral (extensionId: string, peripheralId: number) { + this.peripheralExtensions[extensionId]?.connect(peripheralId); } /** * Disconnect from the extension's connected peripheral. - * @param {string} extensionId - the id of the extension. + * @param extensionId - the id of the extension. */ - disconnectPeripheral (extensionId) { - if (this.peripheralExtensions[extensionId]) { - this.peripheralExtensions[extensionId].disconnect(); - } + disconnectPeripheral (extensionId: string) { + this.peripheralExtensions[extensionId]?.disconnect(); } /** * Returns whether the extension has a currently connected peripheral. - * @param {string} extensionId - the id of the extension. - * @returns {boolean} - whether the extension has a connected peripheral. + * @param extensionId - the id of the extension. + * @returns whether the extension has a connected peripheral. */ - getPeripheralIsConnected (extensionId) { - let isConnected = false; - if (this.peripheralExtensions[extensionId]) { - isConnected = this.peripheralExtensions[extensionId].isConnected(); - } - return isConnected; + getPeripheralIsConnected (extensionId: string) { + return this.peripheralExtensions[extensionId]?.isConnected() ?? false; } /** * Emit an event to indicate that the microphone is being used to stream audio. - * @param {boolean} listening - true if the microphone is currently listening. + * @param listening - true if the microphone is currently listening. */ - emitMicListening (listening) { + emitMicListening (listening: boolean) { this.emit(Runtime.MIC_LISTENING, listening); } /** * Retrieve the function associated with the given opcode. - * @param {!string} opcode The opcode to look up. - * @returns {PrimitiveHandler|undefined} The function which implements the opcode. + * @param opcode The opcode to look up. + * @returns The function which implements the opcode. */ - getOpcodeFunction (opcode) { + getOpcodeFunction (opcode: string): BlockFunction | undefined { return this._primitives[opcode]; } /** * Return whether an opcode represents a hat block. - * @param {!string} opcode The opcode to look up. - * @returns {boolean} True if the op is known to be a hat. + * @param opcode The opcode to look up. + * @returns True if the op is known to be a hat. */ - getIsHat (opcode) { + getIsHat (opcode: string) { return Object.prototype.hasOwnProperty.call(this._hats, opcode); } /** * Return whether an opcode represents an edge-activated hat block. - * @param {!string} opcode The opcode to look up. - * @returns {boolean} True if the op is known to be a edge-activated hat. + * @param opcode The opcode to look up. + * @returns True if the op is known to be a edge-activated hat. */ - getIsEdgeActivatedHat (opcode) { + getIsEdgeActivatedHat (opcode: string) { return Object.prototype.hasOwnProperty.call(this._hats, opcode) && this._hats[opcode].edgeActivated; } /** * Retrieve the execution order of the given opcode. - * @param {!string} opcode The opcode to look up. - * @returns {Array.} The execution order array of given opcode. + * @param opcode The opcode to look up. + * @returns The execution order array of given opcode. */ - getExecutionOrders (opcode) { + getExecutionOrders (opcode: string) { return Object.prototype.hasOwnProperty.call(this._orders, opcode) && this._orders[opcode]; } /** * Attach the audio engine - * @param {!AudioEngine} audioEngine The audio engine to attach + * @param audioEngine The audio engine to attach */ - attachAudioEngine (audioEngine) { + attachAudioEngine (audioEngine: AudioEngine) { this.audioEngine = audioEngine; } /** * Attach the renderer - * @param {!RenderWebGL} renderer The renderer to attach + * @param renderer The renderer to attach */ - attachRenderer (renderer) { + attachRenderer (renderer: RenderWebGL) { this.renderer = renderer; this.renderer.setEdgelessStage(this.limitOptions.edgelessStage); this.renderer.setAccurateCoordinates(this.limitOptions.accurateCoordinates); @@ -1908,17 +1812,17 @@ class Runtime extends EventEmitter { /** * Set the bitmap adapter for the VM/runtime, which converts scratch 2 * bitmaps to scratch 3 bitmaps. (Scratch 3 bitmaps are all bitmap resolution 2) - * @param {PrimitiveHandler} bitmapAdapter The adapter to attach. + * @param bitmapAdapter The adapter to attach. */ - attachV2BitmapAdapter (bitmapAdapter) { + attachV2BitmapAdapter (bitmapAdapter: BitmapAdapter) { this.v2BitmapAdapter = bitmapAdapter; } /** * Attach the storage module - * @param {!ScratchStorage} storage The storage module to attach + * @param storage The storage module to attach */ - attachStorage (storage) { + attachStorage (storage: ScratchStorage) { this.storage = storage; } @@ -1927,22 +1831,24 @@ class Runtime extends EventEmitter { /** * Create a thread and push it to the list of threads. - * @param {!string} id ID of block that starts the stack. - * @param {!RenderedTarget} target Target to run thread on. - * @param {{stackClick?: boolean, updateMonitor?: boolean}|undefined} opts Optional arguments. - * @param {?boolean} opts.stackClick true if the script was activated by clicking on the stack - * @param {?boolean} opts.updateMonitor true if the script should update a monitor value - * @returns {!Thread} The newly created thread. - */ - _pushThread (id, target, opts) { + * @param id ID of block that starts the stack. + * @param target Target to run thread on. + * @param opts Optional arguments. + * @param opts.stackClick true if the script was activated by clicking on the stack + * @param opts.updateMonitor true if the script should update a monitor value + * @returns The newly created thread. + */ + _pushThread (id: string, target: RenderedTarget | null, opts?: { + stackClick?: boolean, + updateMonitor?: boolean + }) { const thread = new Thread(id); thread.target = target; - thread.stackClick = Boolean(opts && opts.stackClick); - thread.updateMonitor = Boolean(opts && opts.updateMonitor); + thread.stackClick = Boolean(opts?.stackClick); + thread.updateMonitor = Boolean(opts?.updateMonitor); thread.blockContainer = thread.updateMonitor ? this.monitorBlocks : - target.blocks; - + target!.blocks; thread.pushStack(id); this.threads.push(thread); return thread; @@ -1950,9 +1856,9 @@ class Runtime extends EventEmitter { /** * Stop a thread: stop running it immediately, and remove it from the thread list later. - * @param {!Thread} thread Thread object to remove from actives + * @param thread Thread object to remove from actives */ - _stopThread (thread) { + _stopThread (thread: Thread) { // Mark the thread for later removal thread.isKilled = true; // Inform sequencer to stop executing that thread. @@ -1963,10 +1869,10 @@ class Runtime extends EventEmitter { * Restart a thread in place, maintaining its position in the list of threads. * This is used by `startHats` to and is necessary to ensure 2.0-like execution order. * Test project: https://scratch.mit.edu/projects/130183108/ - * @param {!Thread} thread Thread object to restart. - * @returns {Thread} The restarted thread. + * @param thread Thread object to restart. + * @returns The restarted thread. */ - _restartThread (thread) { + _restartThread (thread: Thread) { const newThread = new Thread(thread.topBlock); newThread.target = thread.target; newThread.stackClick = thread.stackClick; @@ -1984,10 +1890,10 @@ class Runtime extends EventEmitter { /** * Return whether a thread is currently active/running. - * @param {?Thread} thread Thread object to check. - * @returns {boolean} True if the thread is active/running. + * @param thread Thread object to check. + * @returns True if the thread is active/running. */ - isActiveThread (thread) { + isActiveThread (thread: Thread) { return ( ( thread.stack.length > 0 && @@ -1997,10 +1903,10 @@ class Runtime extends EventEmitter { /** * Return whether a thread is waiting for more information or done. - * @param {?Thread} thread Thread object to check. - * @returns {boolean} True if the thread is waiting + * @param thread Thread object to check. + * @returns True if the thread is waiting */ - isWaitingThread (thread) { + isWaitingThread (thread: Thread) { return ( thread.status === Thread.STATUS_PROMISE_WAIT || thread.status === Thread.STATUS_YIELD_TICK || @@ -2010,13 +1916,16 @@ class Runtime extends EventEmitter { /** * Toggle a script. - * @param {!string} topBlockId ID of block that starts the script. - * @param {{target?: RenderedTarget, stackClick?: boolean}|undefined} opts Optional arguments to toggle the script. - * @param {?RenderedTarget} opts.target Target to run the script on. If not supplied, uses the editing target. - * @param {?boolean} opts.stackClick true if the user activated the stack by clicking, false if not. This + * @param topBlockId ID of block that starts the script. + * @param opts Optional arguments to toggle the script. + * @param opts.target Target to run the script on. If not supplied, uses the editing target. + * @param opts.stackClick true if the user activated the stack by clicking, false if not. This * determines whether we show a visual report when turning on the script. */ - toggleScript (topBlockId, opts) { + toggleScript (topBlockId: string, opts: { + target?: RenderedTarget, + stackClick?: boolean + }) { opts = Object.assign({ target: this._editingTarget, stackClick: false @@ -2025,10 +1934,10 @@ class Runtime extends EventEmitter { for (let i = 0; i < this.threads.length; i++) { // Toggling a script that's already running turns it off if (this.threads[i].topBlock === topBlockId && this.threads[i].status !== Thread.STATUS_DONE) { - const blockContainer = opts.target.blocks; + const blockContainer = opts.target!.blocks; const opcode = blockContainer.getOpcode(blockContainer.getBlock(topBlockId)); - if (this.getIsEdgeActivatedHat(opcode) && this.threads[i].stackClick !== opts.stackClick) { + if (this.getIsEdgeActivatedHat(opcode!) && this.threads[i].stackClick !== opts.stackClick) { // Allow edge activated hat thread stack click to coexist with // edge activated hat thread that runs every frame continue; @@ -2038,15 +1947,15 @@ class Runtime extends EventEmitter { } } // Otherwise add it. - this._pushThread(topBlockId, opts.target, opts); + this._pushThread(topBlockId, opts.target!, opts); } /** * Enqueue a script that when finished will update the monitor for the block. - * @param {!string} topBlockId ID of block that starts the script. - * @param {?RenderedTarget} optTarget target Target to run script on. If not supplied, uses editing target. + * @param topBlockId ID of block that starts the script. + * @param optTarget target Target to run script on. If not supplied, uses editing target. */ - addMonitorScript (topBlockId, optTarget) { + addMonitorScript (topBlockId: string, optTarget?: RenderedTarget | null) { if (!optTarget) optTarget = this._editingTarget; for (let i = 0; i < this.threads.length; i++) { // Don't re-add the script if it's already running @@ -2064,10 +1973,10 @@ class Runtime extends EventEmitter { * `f` will be called with two parameters: * - the top block ID of the script. * - the target that owns the script. - * @param {ScriptCallback} f Function to call for each script. - * @param {RenderedTarget=} optTarget Optionally, a target to restrict to. + * @param f Function to call for each script. + * @param optTarget Optionally, a target to restrict to. */ - allScriptsDo (f, optTarget) { + allScriptsDo (f: ScriptCallback, optTarget?: RenderedTarget) { let targets = this.executableTargets; if (optTarget) { targets = [optTarget]; @@ -2082,7 +1991,7 @@ class Runtime extends EventEmitter { } } - allScriptsByOpcodeDo (opcode, f, optTarget) { + allScriptsByOpcodeDo (opcode: string, f: ScriptByOpcodeCallback, optTarget?: RenderedTarget) { let targets = this.executableTargets; if (optTarget) { targets = [optTarget]; @@ -2098,19 +2007,20 @@ class Runtime extends EventEmitter { /** * Start all relevant hats. - * @param {!string} requestedHatOpcode Opcode of hats to start. - * @param {object= | null} optMatchFields Optionally, fields to match on the hat. - * @param {RenderedTarget=} optTarget Optionally, a target to restrict to. - * @returns {Array.|undefined} List of threads started by this function. + * @param requestedHatOpcode Opcode of hats to start. + * @param optMatchFields Optionally, fields to match on the hat. + * @param optTarget Optionally, a target to restrict to. + * @returns List of threads started by this function. */ - startHats (requestedHatOpcode, - optMatchFields, optTarget) { + startHats (requestedHatOpcode: string, + optMatchFields?: Record | null, optTarget?: RenderedTarget) { if (!Object.prototype.hasOwnProperty.call(this._hats, requestedHatOpcode)) { // No known hat with this opcode. return; } + // eslint-disable-next-line @typescript-eslint/no-this-alias const instance = this; - const newThreads = []; + const newThreads: Thread[] = []; // Look up metadata for the relevant hat. const hatMeta = instance._hats[requestedHatOpcode]; @@ -2194,7 +2104,7 @@ class Runtime extends EventEmitter { if (this.renderer && '_allSkins' in this.renderer) { this.renderer._allSkins.forEach(skin => { - this.renderer.destroySkin(skin._id); + this.renderer!.destroySkin(skin._id); }); } // @todo clear out extensions? turboMode? etc. @@ -2222,9 +2132,9 @@ class Runtime extends EventEmitter { * Add a target to the runtime. This tracks the sprite pane * ordering of the target. The target still needs to be put * into the correct execution order after calling this function. - * @param {RenderedTarget} target target to add + * @param target target to add */ - addTarget (target) { + addTarget (target: RenderedTarget) { this.targets.push(target); this.executableTargets.push(target); } @@ -2235,11 +2145,11 @@ class Runtime extends EventEmitter { * A positve number will make the target execute earlier. A negative number * will make the target execute later in the order. * - * @param {RenderedTarget} executableTarget target to move - * @param {number} delta number of positions to move target by - * @returns {number} new position in execution order + * @param executableTarget target to move + * @param delta number of positions to move target by + * @returns new position in execution order */ - moveExecutable (executableTarget, delta) { + moveExecutable (executableTarget: RenderedTarget, delta: number) { const oldIndex = this.executableTargets.indexOf(executableTarget); this.executableTargets.splice(oldIndex, 1); let newIndex = oldIndex + delta; @@ -2263,20 +2173,20 @@ class Runtime extends EventEmitter { * Infinity will set the target to execute first. 0 will set the target to * execute last (before the stage). * - * @param {RenderedTarget} executableTarget target to move - * @param {number} newIndex position in execution order to place the target - * @returns {number} new position in the execution order + * @param executableTarget target to move + * @param newIndex position in execution order to place the target + * @returns new position in the execution order */ - setExecutablePosition (executableTarget, newIndex) { + setExecutablePosition (executableTarget: RenderedTarget, newIndex: number) { const oldIndex = this.executableTargets.indexOf(executableTarget); return this.moveExecutable(executableTarget, newIndex - oldIndex); } /** * Remove a target from the execution set. - * @param {RenderedTarget} executableTarget target to remove + * @param executableTarget target to remove */ - removeExecutable (executableTarget) { + removeExecutable (executableTarget: RenderedTarget) { const oldIndex = this.executableTargets.indexOf(executableTarget); if (oldIndex > -1) { this.executableTargets.splice(oldIndex, 1); @@ -2285,9 +2195,9 @@ class Runtime extends EventEmitter { /** * Dispose of a target. - * @param {!RenderedTarget} disposingTarget Target to dispose of. + * @param disposingTarget Target to dispose of. */ - disposeTarget (disposingTarget) { + disposeTarget (disposingTarget: RenderedTarget) { this.targets = this.targets.filter(target => { if (disposingTarget !== target) return true; // Allow target to do dispose actions. @@ -2299,10 +2209,10 @@ class Runtime extends EventEmitter { /** * Stop any threads acting on the target. - * @param {!RenderedTarget} target Target to stop threads for. - * @param {Thread=} optThreadException Optional thread to skip. + * @param target Target to stop threads for. + * @param optThreadException Optional thread to skip. */ - stopForTarget (target, optThreadException) { + stopForTarget (target: RenderedTarget, optThreadException?: Thread) { // Emit stop event to allow blocks to clean up any state. this.emit(Runtime.STOP_FOR_TARGET, target, optThreadException); @@ -2436,10 +2346,10 @@ class Runtime extends EventEmitter { /** * Get the number of threads in the given array that are monitor threads (threads * that update monitor values, and don't count as running a script). - * @param {!Array.} threads The set of threads to look through. - * @returns {number} The number of monitor threads in threads. + * @param threads The set of threads to look through. + * @returns The number of monitor threads in threads. */ - _getMonitorThreadCount (threads) { + _getMonitorThreadCount (threads: Thread[]) { let count = 0; threads.forEach(thread => { if (thread.updateMonitor) count++; @@ -2456,9 +2366,9 @@ class Runtime extends EventEmitter { /** * Set the current editing target known by the runtime. - * @param {!RenderedTarget} editingTarget New editing target. + * @param editingTarget New editing target. */ - setEditingTarget (editingTarget) { + setEditingTarget (editingTarget: RenderedTarget) { const oldEditingTarget = this._editingTarget; this._editingTarget = editingTarget; // Script glows must be cleared. @@ -2472,20 +2382,20 @@ class Runtime extends EventEmitter { /** * Set whether we are in 30 TPS compatibility mode. - * @param {boolean} compatibilityModeOn True iff in compatibility mode. + * @param compatibilityModeOn True iff in compatibility mode. * @deprecated Use setFramerate(30) (compatibility mode) or setFramerate(60) instead. * @see {@link setFramerate} */ - setCompatibilityMode (compatibilityModeOn) { + setCompatibilityMode (compatibilityModeOn: boolean) { this.compatibilityMode = compatibilityModeOn; this.setFramerate(compatibilityModeOn ? 30 : 60); } /** * Set the framerate (also called TPS in VM). - * @param {number} framerate Frames per second. + * @param framerate Frames per second. */ - setFramerate (framerate) { + setFramerate (framerate: number) { this.framerate = framerate; if (this._steppingInterval) { clearInterval(this._steppingInterval); @@ -2497,9 +2407,9 @@ class Runtime extends EventEmitter { /** * Emit glows/glow clears for scripts after a single tick. * Looks at `this.threads` and notices which have turned on/off new glows. - * @param {Array.=} optExtraThreads Optional list of inactive threads. + * @param optExtraThreads Optional list of inactive threads. */ - _updateGlows (optExtraThreads) { + _updateGlows (optExtraThreads?: Thread[]) { const searchThreads = []; searchThreads.push(...this.threads); if (optExtraThreads) { @@ -2516,7 +2426,7 @@ class Runtime extends EventEmitter { if (target === this._editingTarget) { const blockForThread = thread.blockGlowInFrame; if (thread.requestScriptGlowInFrame || thread.stackClick) { - let script = target.blocks.getTopLevelScript(blockForThread); + let script = target?.blocks.getTopLevelScript(blockForThread); if (!script) { // Attempt to find in flyout blocks. script = this.flyoutBlocks.getTopLevelScript( @@ -2555,9 +2465,9 @@ class Runtime extends EventEmitter { * Emit run start/stop after each tick. Emits when `this.threads.length` goes * between non-zero and zero * - * @param {number} nonMonitorThreadCount The new nonMonitorThreadCount + * @param nonMonitorThreadCount The new nonMonitorThreadCount */ - _emitProjectRunStatus (nonMonitorThreadCount) { + _emitProjectRunStatus (nonMonitorThreadCount: number) { if (this._nonMonitorThreadCount === 0 && nonMonitorThreadCount > 0) { this.emit(Runtime.PROJECT_RUN_START); } @@ -2571,9 +2481,9 @@ class Runtime extends EventEmitter { * "Quiet" a script's glow: stop the VM from generating glow/unglow events * about that script. Use when a script has just been deleted, but we may * still be tracking glow data about it. - * @param {!string} scriptBlockId Id of top-level block in script to quiet. + * @param scriptBlockId Id of top-level block in script to quiet. */ - quietGlow (scriptBlockId) { + quietGlow (scriptBlockId: string) { const index = this._scriptGlowsPreviousFrame.indexOf(scriptBlockId); if (index > -1) { this._scriptGlowsPreviousFrame.splice(index, 1); @@ -2582,10 +2492,10 @@ class Runtime extends EventEmitter { /** * Emit feedback for block glowing (used in the sequencer). - * @param {?string} blockId ID for the block to update glow - * @param {boolean} isGlowing True to turn on glow; false to turn off. + * @param blockId ID for the block to update glow + * @param isGlowing True to turn on glow; false to turn off. */ - glowBlock (blockId, isGlowing) { + glowBlock (blockId: string, isGlowing: boolean) { if (isGlowing) { this.emit(Runtime.BLOCK_GLOW_ON, {id: blockId}); } else { @@ -2595,10 +2505,10 @@ class Runtime extends EventEmitter { /** * Emit feedback for script glowing. - * @param {?string} topBlockId ID for the top block to update glow - * @param {boolean} isGlowing True to turn on glow; false to turn off. + * @param topBlockId ID for the top block to update glow + * @param isGlowing True to turn on glow; false to turn off. */ - glowScript (topBlockId, isGlowing) { + glowScript (topBlockId: string, isGlowing: boolean) { if (isGlowing) { this.emit(Runtime.SCRIPT_GLOW_ON, {id: topBlockId}); } else { @@ -2608,61 +2518,65 @@ class Runtime extends EventEmitter { /** * Emit whether blocks are being dragged over gui - * @param {boolean} areBlocksOverGui True if blocks are dragged out of blocks workspace, false otherwise + * @param areBlocksOverGui True if blocks are dragged out of blocks workspace, false otherwise */ - emitBlockDragUpdate (areBlocksOverGui) { + emitBlockDragUpdate (areBlocksOverGui: boolean) { this.emit(Runtime.BLOCK_DRAG_UPDATE, areBlocksOverGui); } /** * Emit event to indicate that the block drag has ended with the blocks outside the blocks workspace - * @param {Array.} blocks The set of blocks dragged to the GUI - * @param {string} topBlockId The original id of the top block being dragged + * @param blocks The set of blocks dragged to the GUI + * @param topBlockId The original id of the top block being dragged */ - emitBlockEndDrag (blocks, topBlockId) { + emitBlockEndDrag (blocks: VMBlock[], topBlockId: string) { this.emit(Runtime.BLOCK_DRAG_END, blocks, topBlockId); } /** * Emit value for reporter to show in the blocks. - * @param {string} blockId ID for the block. - * @param {string} value Value to show associated with the block. + * @param blockId ID for the block. + * @param value Value to show associated with the block. */ - visualReport (blockId, value) { + visualReport (blockId: string, value: unknown) { this.emit(Runtime.VISUAL_REPORT, {id: blockId, value: String(value)}); } /** * Add a monitor to the state. If the monitor already exists in the state, * updates those properties that are defined in the given monitor record. - * @param {{get: (key: string) => unknown}} monitor Monitor to add. + * @param monitor Monitor to add. */ - requestAddMonitor (monitor) { - const id = monitor.get('id'); + requestAddMonitor (monitor: RecordOf | ImmutableMap) { + // @ts-expect-error scfoundation mixed use of Immutable and non-Immutable, JS native Map and Immutable Map + const id: string = monitor.get('id')!; if (!this.requestUpdateMonitor(monitor)) { // update monitor if it exists in the state // if the monitor did not exist in the state, add it - this._monitorState = this._monitorState.set(id, monitor); + this._monitorState = this._monitorState.set(id, monitor as RecordOf); } } /** * Update a monitor in the state and report success/failure of update. - * @param {{get: (key: string) => unknown}} monitor Monitor values to update. + * @param monitor Monitor values to update. * values on the old monitor with the same ID. If a value isn't defined on the new monitor, * the old monitor will keep its old value. - * @returns {boolean} true if monitor exists in the state and was updated, false if it did not exist. + * @returns true if monitor exists in the state and was updated, false if it did not exist. */ - requestUpdateMonitor (monitor) { - const id = monitor.get('id'); + requestUpdateMonitor ( + monitor: RecordOf | ImmutableMap + ) { + // @ts-expect-error scfoundation mixed use of Immutable and non-Immutable, JS native Map and Immutable Map + const id: string = monitor.get('id'); if (this._monitorState.has(id)) { this._monitorState = // Use mergeWith here to prevent undefined values from overwriting existing ones - this._monitorState.set(id, this._monitorState.get(id).mergeWith((prev, next) => { + this._monitorState.set(id, this._monitorState.get(id)!.mergeWith((prev, next) => { if (typeof next === 'undefined' || next === null) { return prev; } return next; - }, monitor)); + }, monitor as RecordOf)); return true; } return false; @@ -2671,52 +2585,52 @@ class Runtime extends EventEmitter { /** * Removes a monitor from the state. Does nothing if the monitor already does * not exist in the state. - * @param {!string} monitorId ID of the monitor to remove. + * @param monitorId ID of the monitor to remove. */ - requestRemoveMonitor (monitorId) { + requestRemoveMonitor (monitorId: string) { this._monitorState = this._monitorState.delete(monitorId); } /** * Hides a monitor and returns success/failure of action. - * @param {!string} monitorId ID of the monitor to hide. - * @returns {boolean} true if monitor exists and was updated, false otherwise + * @param monitorId ID of the monitor to hide. + * @returns true if monitor exists and was updated, false otherwise */ - requestHideMonitor (monitorId) { - return this.requestUpdateMonitor(new Map([ - ['id', monitorId], - ['visible', false] - ])); + requestHideMonitor (monitorId: string) { + return this.requestUpdateMonitor(Map({ + id: monitorId, + visible: false + })); } /** * Shows a monitor and returns success/failure of action. * not exist in the state. - * @param {!string} monitorId ID of the monitor to show. - * @returns {boolean} true if monitor exists and was updated, false otherwise + * @param monitorId ID of the monitor to show. + * @returns true if monitor exists and was updated, false otherwise */ - requestShowMonitor (monitorId) { - return this.requestUpdateMonitor(new Map([ - ['id', monitorId], - ['visible', true] - ])); + requestShowMonitor (monitorId: string) { + return this.requestUpdateMonitor(Map({ + id: monitorId, + visible: true + })); } /** * Removes all monitors with the given target ID from the state. Does nothing if * the monitor already does not exist in the state. - * @param {!string} targetId Remove all monitors with given target ID. + * @param targetId Remove all monitors with given target ID. */ - requestRemoveMonitorByTargetId (targetId) { + requestRemoveMonitorByTargetId (targetId: string) { this._monitorState = this._monitorState.filterNot(value => value.targetId === targetId); } /** * Get a target by its id. - * @param {string} targetId Id of target to find. - * @returns {RenderedTarget|undefined} The target, if found. + * @param targetId Id of target to find. + * @returns The target, if found. */ - getTargetById (targetId) { + getTargetById (targetId: string) { for (let i = 0; i < this.targets.length; i++) { const target = this.targets[i]; if (target.id === targetId) { @@ -2727,10 +2641,10 @@ class Runtime extends EventEmitter { /** * Get the first original (non-clone-block-created) sprite given a name. - * @param {string} spriteName Name of sprite to look for. - * @returns {RenderedTarget|undefined} Target representing a sprite of the given name. + * @param spriteName Name of sprite to look for. + * @returns Target representing a sprite of the given name. */ - getSpriteTargetByName (spriteName) { + getSpriteTargetByName (spriteName: string) { for (let i = 0; i < this.targets.length; i++) { const target = this.targets[i]; if (target.isStage) { @@ -2744,10 +2658,10 @@ class Runtime extends EventEmitter { /** * Get a target by its drawable id. - * @param {number} drawableID drawable id of target to find - * @returns {RenderedTarget|undefined} The target, if found. + * @param drawableID drawable id of target to find + * @returns The target, if found. */ - getTargetByDrawableId (drawableID) { + getTargetByDrawableId (drawableID: number) { for (let i = 0; i < this.targets.length; i++) { const target = this.targets[i]; if (target.drawableID === drawableID) return target; @@ -2756,15 +2670,15 @@ class Runtime extends EventEmitter { /** * Update the clone counter to track how many clones are created. - * @param {number} changeAmount How many clones have been created/destroyed. + * @param changeAmount How many clones have been created/destroyed. */ - changeCloneCounter (changeAmount) { + changeCloneCounter (changeAmount: number) { this._cloneCounter += changeAmount; } /** * Return whether there are clones available. - * @returns {boolean} True until the number of clones hits Runtime.MAX_CLONES. + * @returns True until the number of clones hits Runtime.MAX_CLONES. */ clonesAvailable () { return this._cloneCounter < this.MAX_CLONES; @@ -2786,26 +2700,26 @@ class Runtime extends EventEmitter { /** * Report that a new target has been created, possibly by cloning an existing target. - * @param {RenderedTarget} newTarget - the newly created target. - * @param {RenderedTarget} [sourceTarget] - the target used as a source for the new clone, if any. + * @param newTarget - the newly created target. + * @param sourceTarget - the target used as a source for the new clone, if any. * @fires Runtime#targetWasCreated */ - fireTargetWasCreated (newTarget, sourceTarget) { + fireTargetWasCreated (newTarget: RenderedTarget, sourceTarget?: RenderedTarget) { this.emit('targetWasCreated', newTarget, sourceTarget); } /** * Report that a clone target is being removed. - * @param {RenderedTarget} target - the target being removed + * @param target - the target being removed * @fires Runtime#targetWasRemoved */ - fireTargetWasRemoved (target) { + fireTargetWasRemoved (target: RenderedTarget) { this.emit('targetWasRemoved', target); } /** * Get a target representing the Scratch stage, if one exists. - * @returns {RenderedTarget|undefined} The target, if found. + * @returns The target, if found. */ getTargetForStage () { for (let i = 0; i < this.targets.length; i++) { @@ -2818,14 +2732,14 @@ class Runtime extends EventEmitter { /** * Get the editing target. - * @returns {?RenderedTarget} The editing target. + * @returns The editing target. */ getEditingTarget () { return this._editingTarget; } - getAllVarNamesOfType (varType) { - let varNames = []; + getAllVarNamesOfType (varType: VariableType) { + let varNames: string[] = []; for (const target of this.targets) { const targetVarNames = target.getAllVariableNamesInScopeByType(varType, true); varNames = varNames.concat(targetVarNames); @@ -2835,60 +2749,62 @@ class Runtime extends EventEmitter { /** * Get the label or label function for an opcode - * @param {string} extendedOpcode - the opcode you want a label for - * @returns {OpcodeLabelInfo|undefined} The label metadata for this opcode. + * @param extendedOpcode - the opcode you want a label for + * @returns The label metadata for this opcode. */ - getLabelForOpcode (extendedOpcode) { + getLabelForOpcode (extendedOpcode: string) { const [category, opcode] = StringUtil.splitFirst(extendedOpcode, '_'); if (!(category && opcode)) return; const categoryInfo = this._blockInfo.find(ci => ci.id === category); if (!categoryInfo) return; - const block = categoryInfo.blocks.find(b => b.info.opcode === opcode); + const block = categoryInfo.blocks.find(b => (b.info as NormalizedExtensionBlockMetadata).opcode === opcode); if (!block) return; // TODO: we may want to format the label in a locale-specific way. return { category: 'extension', // This assumes that all extensions have the same monitor color. - label: `${categoryInfo.name}: ${block.info.text}` + label: `${categoryInfo.name}: ${(block.info as NormalizedExtensionBlockMetadata).text}` }; } /** * Create a new global variable avoiding conflicts with other variable names. - * @param {string} variableName The desired variable name for the new global variable. + * @param variableName The desired variable name for the new global variable. * This can be turned into a fresh name as necessary. - * @param {string} optVarId An optional ID to use for the variable. A new one will be generated + * @param optVarId An optional ID to use for the variable. A new one will be generated * if a falsey value for this parameter is provided. - * @param {string} optVarType The type of the variable to create. Defaults to Variable.SCALAR_TYPE. - * @returns {Variable} The new variable that was created. + * @param optVarType The type of the variable to create. Defaults to Variable.SCALAR_TYPE. + * @returns The new variable that was created. */ - createNewGlobalVariable (variableName, optVarId, optVarType) { + createNewGlobalVariable (variableName: string, optVarId?: string, optVarType?: VariableType) { const varType = (typeof optVarType === 'string') ? optVarType : Variable.SCALAR_TYPE; const allVariableNames = this.getAllVarNamesOfType(varType); const newName = StringUtil.unusedName(variableName, allVariableNames); const variable = new Variable(optVarId || uid(), newName, varType); const stage = this.getTargetForStage(); + if (!stage) throw new Error('No stage found when creating a global variable'); stage.variables[variable.id] = variable; return variable; } /** * Get names and ids of parameters for the given procedure. - * @param {string} procedureCode Procedure code for procedure to query. - * @returns {Array.} List of param names for a procedure. + * @param procedureCode Procedure code for procedure to query. + * @returns List of param names for a procedure. */ - getProcedureParamNamesAndIds (procedureCode) { - return this.getProcedureParamNamesIdsAndDefaults(procedureCode).slice(0, 2); + getProcedureParamNamesAndIds (procedureCode: string) { + return (this.getProcedureParamNamesIdsAndDefaults(procedureCode) + ?.slice(0, 2) ?? null) as [string[], string[]] | null; } /** * Get names, ids, and defaults of parameters for the given procedure. - * @param {?string} name Name of procedure to query. - * @returns {?Array.} List of param names for a procedure. + * @param name Name of procedure to query. + * @returns List of param names for a procedure. */ - getProcedureParamNamesIdsAndDefaults (name) { + getProcedureParamNamesIdsAndDefaults (name: string) { for (const target of this.targets) { const result = target.blocks.getProcedureParamNamesIdsAndDefaults(name); if (result) { @@ -2900,10 +2816,10 @@ class Runtime extends EventEmitter { /** * Get the global procedure definition for a given name. - * @param {?string} name Name of procedure to query. - * @returns {[?RenderedTarget, ?string]} ID of procedure definition. + * @param name Name of procedure to query. + * @returns ID of procedure definition. */ - getProcedureDefinition (name) { + getProcedureDefinition (name: string): [RenderedTarget, string] | [null, null] { for (const target of this.targets) { const definition = target.blocks.getProcedureDefinition(name, true); if (definition) { @@ -2924,9 +2840,9 @@ class Runtime extends EventEmitter { /** * Emit a targets update at the end of the step if the provided target is * the original sprite - * @param {!RenderedTarget} target Target requesting the targets update + * @param target Target requesting the targets update */ - requestTargetsUpdate (target) { + requestTargetsUpdate (target: RenderedTarget) { if (!target.isOriginal) return; this._refreshTargets = true; } @@ -2964,15 +2880,15 @@ class Runtime extends EventEmitter { * Do not use the runtime after calling this method. This method is meant for test shutdown. */ quit () { - clearInterval(this._steppingInterval); + clearInterval(this._steppingInterval!); this._steppingInterval = null; } /** * Turn on profiling. - * @param {ProfilerFrameHandler} onFrame A callback for profiling frames. + * @param onFrame A callback for profiling frames. */ - enableProfiling (onFrame) { + enableProfiling (onFrame: FrameCallback) { if (Profiler.available()) { this.profiler = new Profiler(onFrame); } @@ -2995,12 +2911,6 @@ class Runtime extends EventEmitter { } } -/** - * Event fired after a new target has been created, possibly by cloning an existing target. - * - * @event Runtime#targetWasCreated - * @param {RenderedTarget} newTarget - the newly created target. - * @param {RenderedTarget} [sourceTarget] - the target used as a source for the new clone, if any. - */ +export type IODevices = Runtime['ioDevices']; export default Runtime; diff --git a/packages/vm/src/engine/scratch-blocks-constants.ts b/packages/vm/src/engine/scratch-blocks-constants.ts index 1d8bc67c5..f2f3e3985 100644 --- a/packages/vm/src/engine/scratch-blocks-constants.ts +++ b/packages/vm/src/engine/scratch-blocks-constants.ts @@ -1,9 +1,7 @@ /** * These constants are copied from scratch-blocks/core/constants.js - * @readonly - * @enum {int} */ -const ScratchBlocksConstants = { +export const ScratchBlocksConstants = { /** * ENUM for output shape: hexagonal (booleans/predicates). * @constant @@ -24,9 +22,3 @@ const ScratchBlocksConstants = { } as const; export default ScratchBlocksConstants; - -export const { - OUTPUT_SHAPE_HEXAGONAL, - OUTPUT_SHAPE_ROUND, - OUTPUT_SHAPE_SQUARE -} = ScratchBlocksConstants; diff --git a/packages/vm/src/engine/sequencer.ts b/packages/vm/src/engine/sequencer.ts index ac1e6bd1b..b3f6ae5ff 100644 --- a/packages/vm/src/engine/sequencer.ts +++ b/packages/vm/src/engine/sequencer.ts @@ -1,6 +1,6 @@ import Timer from '../util/timer'; import Thread from './thread'; -import execute from './execute.js'; +import execute from './execute'; import type Runtime from './runtime'; /** @@ -59,7 +59,7 @@ class Sequencer { */ stepThreads (): Thread[] { // Work time is 75% of the thread stepping interval. - const WORK_TIME = 0.75 * this.runtime.currentStepTime; + const WORK_TIME = 0.75 * this.runtime.currentStepTime!; // For compatibility with Scatch 2, update the millisecond clock // on the Runtime once per step (see Interpreter.as in Scratch 2 // for original use of `currentMSecs`) diff --git a/packages/vm/src/engine/target.js b/packages/vm/src/engine/target.ts similarity index 75% rename from packages/vm/src/engine/target.js rename to packages/vm/src/engine/target.ts index 7dc979209..5aa5fdc51 100644 --- a/packages/vm/src/engine/target.js +++ b/packages/vm/src/engine/target.ts @@ -1,6 +1,6 @@ import EventEmitter from 'events'; -import Blocks from './blocks.js'; -import Variable from '../engine/variable'; +import Blocks from './blocks'; +import Variable, {type VariableType} from '../engine/variable'; import Comment from '../engine/comment'; import uid from '../util/uid'; import {Map} from 'immutable'; @@ -8,9 +8,18 @@ import log from '../util/log'; import StringUtil from '../util/string-util'; import VariableUtil from '../util/variable-util'; +import type Runtime from './runtime'; +import type RenderedTarget from '../sprites/rendered-target'; +import type {VarReference} from '../util/variable-util'; +import type {VMBlock} from '../serialization/schema'; + /** - * @typedef {import('./runtime').default} Runtime + * Events that can be emitted by Target (including subclasses like RenderedTarget). */ +interface TargetEvents { + 'EVENT_TARGET_MOVED': [target: RenderedTarget, oldX: number, oldY: number, force: boolean]; + 'EVENT_TARGET_VISUAL_CHANGE': [target: RenderedTarget]; +} /** * @fileoverview @@ -18,86 +27,84 @@ import VariableUtil from '../util/variable-util'; * Examples include sprites/clones or potentially physical-world devices. */ -class Target extends EventEmitter { +abstract class Target extends EventEmitter { + /** + * Reference to the runtime. + */ + runtime: Runtime; + /** + * A unique ID for this target. + */ + id: string; + /** + * Blocks run as code for this target. + */ + blocks: Blocks; + /** + * Dictionary of variables and their values for this target. + * Key is the variable id. + */ + variables: Record = {}; + /** + * Dictionary of comments for this target. + * Key is the comment id. + */ + comments: Record = {}; + /** + * Dictionary of custom state for this target. + * This can be used to store target-specific custom state for blocks which need it. + * TODO: do we want to persist this in SB3 files? + */ + _customState: Record = {}; /** - * @param {Runtime} runtime Reference to the runtime. - * @param {?Blocks} blocks Blocks instance for the blocks owned by this target. - * @class + * Currently known values for edge-activated hats. + * Keys are block ID for the hat; values are the currently known values. */ - constructor (runtime, blocks) { + _edgeActivatedHatValues: Record = {}; + + /** + * Whether this target is the stage. Set by subclasses. + */ + isStage!: boolean; + + /** + * @param runtime Reference to the runtime. + * @param blocks Blocks instance for the blocks owned by this target. + */ + constructor (runtime: Runtime, blocks: Blocks | null) { super(); if (!blocks) { blocks = new Blocks(runtime); } - - /** - * Reference to the runtime. - * @type {Runtime} - */ this.runtime = runtime; - /** - * A unique ID for this target. - * @type {string} - */ this.id = uid(); - /** - * Blocks run as code for this target. - * @type {!Blocks} - */ this.blocks = blocks; - /** - * Dictionary of variables and their values for this target. - * Key is the variable id. - * @type {Record} - */ - this.variables = {}; - /** - * Dictionary of comments for this target. - * Key is the comment id. - * @type {Record} - */ - this.comments = {}; - /** - * Dictionary of custom state for this target. - * This can be used to store target-specific custom state for blocks which need it. - * TODO: do we want to persist this in SB3 files? - * @type {Record} - */ - this._customState = {}; - - /** - * Currently known values for edge-activated hats. - * Keys are block ID for the hat; values are the currently known values. - * @type {Record} - */ - this._edgeActivatedHatValues = {}; } /** * Called when the project receives a "green flag." - * @abstract */ - onGreenFlag () {} + abstract onGreenFlag (): void; /** * Return a human-readable name for this target. * Target implementations should override this. * @abstract - * @returns {string} Human-readable name for the target. + * @returns Human-readable name for the target. */ - getName () { + getName (): string { return this.id; } /** * Update an edge-activated hat block value. - * @param {!string} blockId ID of hat to store value for. - * @param {*} newValue Value to store for edge-activated hat. - * @returns {*} The old value for the edge-activated hat. + * @param blockId ID of hat to store value for. + * @param newValue Value to store for edge-activated hat. + * @returns The old value for the edge-activated hat. */ - updateEdgeActivatedValue (blockId, newValue) { + updateEdgeActivatedValue (blockId: string, newValue: unknown): unknown { const oldValue = this._edgeActivatedHatValues[blockId]; this._edgeActivatedHatValues[blockId] = newValue; return oldValue; @@ -105,15 +112,15 @@ class Target extends EventEmitter { /** * Whether the block has edge-activated value. - * @param {string} blockId The block id. - * @returns {boolean} Whether the block has edge-activated value. + * @param blockId The block id. + * @returns Whether the block has edge-activated value. */ - hasEdgeActivatedValue (blockId) { + hasEdgeActivatedValue (blockId: string): boolean { return Object.prototype.hasOwnProperty.call(this._edgeActivatedHatValues, blockId); } /** - * Clear all edge-activaed hat values. + * Clear all edge-activated hat values. */ clearEdgeActivatedValues () { this._edgeActivatedHatValues = {}; @@ -122,12 +129,12 @@ class Target extends EventEmitter { /** * Look up a variable object, first by id, and then by name if the id is not found. * Create a new variable if both lookups fail. - * @param {string} id Id of the variable. - * @param {string} name Name of the variable. - * @returns {!Variable} Variable object. + * @param id Id of the variable. + * @param name Name of the variable. + * @returns Variable object. */ - lookupOrCreateVariable (id, name) { - let variable = this.lookupVariableById(id); + lookupOrCreateVariable (id: string, name: string): Variable { + let variable: Variable | null | undefined = this.lookupVariableById(id); if (variable) return variable; variable = this.lookupVariableByNameAndType(name, Variable.SCALAR_TYPE); @@ -142,11 +149,11 @@ class Target extends EventEmitter { /** * Look up a broadcast message object with the given id and return it * if it exists. - * @param {string} id Id of the variable. - * @param {string} name Name of the variable. - * @returns {Variable | undefined} Variable object. + * @param id Id of the variable. + * @param name Name of the variable. + * @returns Variable object. */ - lookupBroadcastMsg (id, name) { + lookupBroadcastMsg (id: string, name: string): Variable | undefined { let broadcastMsg; if (id) { broadcastMsg = this.lookupVariableById(id); @@ -172,10 +179,10 @@ class Target extends EventEmitter { * Look up a broadcast message with the given name and return the variable * if it exists. Does not create a new broadcast message variable if * it doesn't exist. - * @param {string} name Name of the variable. - * @returns {Variable | undefined} Variable object. + * @param name Name of the variable. + * @returns Variable object. */ - lookupBroadcastByInputValue (name) { + lookupBroadcastByInputValue (name: string): Variable | undefined { const vars = this.variables; for (const propName in vars) { if ((vars[propName].type === Variable.BROADCAST_MESSAGE_TYPE) && @@ -188,10 +195,10 @@ class Target extends EventEmitter { /** * Look up a variable object. * Search begins for local variables; then look for globals. - * @param {string} id Id of the variable. - * @returns {Variable | undefined} Variable object. + * @param id Id of the variable. + * @returns Variable object. */ - lookupVariableById (id) { + lookupVariableById (id: string): Variable | undefined { // If we have a local copy, return it. if (Object.prototype.hasOwnProperty.call(this.variables, id)) { return this.variables[id]; @@ -209,12 +216,12 @@ class Target extends EventEmitter { * Look up a variable object by its name and variable type. * Search begins with local variables; then global variables if a local one * was not found. - * @param {string} name Name of the variable. - * @param {string} type Type of the variable. Defaults to Variable.SCALAR_TYPE. - * @param {boolean=} skipStage Optional flag to skip checking the stage - * @returns {?Variable} Variable object if found, or null if not. + * @param name Name of the variable. + * @param type Type of the variable. Defaults to Variable.SCALAR_TYPE. + * @param skipStage Optional flag to skip checking the stage + * @returns Variable object if found, or null if not. */ - lookupVariableByNameAndType (name, type, skipStage) { + lookupVariableByNameAndType (name: string, type: string, skipStage?: boolean) { if (typeof name !== 'string') return; if (typeof type !== 'string') type = Variable.SCALAR_TYPE; skipStage = skipStage || false; @@ -244,12 +251,12 @@ class Target extends EventEmitter { /** * Look up a list object for this target, and create it if one doesn't exist. * Search begins for local lists; then look for globals. - * @param {!string} id Id of the list. - * @param {!string} name Name of the list. - * @returns {Variable} Variable object representing the found/created list. + * @param id Id of the list. + * @param name Name of the list. + * @returns Variable object representing the found/created list. */ - lookupOrCreateList (id, name) { - let list = this.lookupVariableById(id); + lookupOrCreateList (id: string, name: string): Variable { + let list: Variable | null | undefined = this.lookupVariableById(id); if (list) return list; list = this.lookupVariableByNameAndType(name, Variable.LIST_TYPE); @@ -264,15 +271,15 @@ class Target extends EventEmitter { /** * Creates a variable with the given id and name and adds it to the * dictionary of variables. - * @param {string} id Id of variable - * @param {string} name Name of variable. - * @param {string} type Type of variable, '', 'broadcast_msg', or 'list' - * @param {boolean} isCloud Whether the variable to create has the isCloud flag set. + * @param id Id of variable + * @param name Name of variable. + * @param type Type of variable, '', 'broadcast_msg', or 'list' + * @param isCloud Whether the variable to create has the isCloud flag set. * Additional checks are made that the variable can be created as a cloud variable. */ - createVariable (id, name, type, isCloud) { + createVariable (id: string, name: string, type: string, isCloud?: boolean) { if (!Object.prototype.hasOwnProperty.call(this.variables, id)) { - const newVariable = new Variable(id, name, type, false); + const newVariable = new Variable(id, name, type as Variable['type'], false); if (isCloud && this.isStage && this.runtime.canAddCloudVariable()) { newVariable.isCloud = true; this.runtime.addCloudVariable(); @@ -284,17 +291,18 @@ class Target extends EventEmitter { /** * Creates a comment with the given properties. - * @param {string} id Id of the comment. - * @param {string} blockId Optional id of the block the comment is attached + * @param id Id of the comment. + * @param blockId Optional id of the block the comment is attached * to if it is a block comment. - * @param {string} text The text the comment contains. - * @param {number} x The x coordinate of the comment on the workspace. - * @param {number} y The y coordinate of the comment on the workspace. - * @param {number} width The width of the comment when it is full size - * @param {number} height The height of the comment when it is full size - * @param {boolean} minimized Whether the comment is minimized. - */ - createComment (id, blockId, text, x, y, width, height, minimized) { + * @param text The text the comment contains. + * @param x The x coordinate of the comment on the workspace. + * @param y The y coordinate of the comment on the workspace. + * @param width The width of the comment when it is full size + * @param height The height of the comment when it is full size + * @param minimized Whether the comment is minimized. + */ + createComment (id: string, blockId: string | undefined, text: string, + x: number, y: number, width: number, height: number, minimized: boolean) { if (!Object.prototype.hasOwnProperty.call(this.comments, id)) { const newComment = new Comment(id, text, x, y, width, height, minimized); @@ -314,10 +322,10 @@ class Target extends EventEmitter { /** * Renames the variable with the given id to newName. - * @param {string} id Id of variable to rename. - * @param {string} newName New name for the variable. + * @param id Id of variable to rename. + * @param newName New name for the variable. */ - renameVariable (id, newName) { + renameVariable (id: string, newName: string) { if (Object.prototype.hasOwnProperty.call(this.variables, id)) { const variable = this.variables[id]; if (variable.id === id) { @@ -366,9 +374,9 @@ class Target extends EventEmitter { /** * Removes the variable with the given id from the dictionary of variables. - * @param {string} id Id of variable to delete. + * @param id Id of variable to delete. */ - deleteVariable (id) { + deleteVariable (id: string) { if (Object.prototype.hasOwnProperty.call(this.variables, id)) { // Get info about the variable before deleting it const deletedVariableName = this.variables[id].name; @@ -392,7 +400,7 @@ class Target extends EventEmitter { */ deleteMonitors () { this.runtime.requestRemoveMonitorByTargetId(this.id); - let targetSpecificMonitorBlockIds; + let targetSpecificMonitorBlockIds: string[]; if (this.isStage) { // This only deletes global variables and not other stage monitors like backdrop number. targetSpecificMonitorBlockIds = Object.keys(this.variables); @@ -408,13 +416,13 @@ class Target extends EventEmitter { /** * Create a clone of the variable with the given id from the dictionary of * this target's variables. - * @param {string} id Id of variable to duplicate. - * @param {boolean=} optKeepOriginalId Optional flag to keep the original variable ID + * @param id Id of variable to duplicate. + * @param optKeepOriginalId Optional flag to keep the original variable ID * for the duplicate variable. This is necessary when cloning a sprite, for example. - * @returns {?Variable} The duplicated variable, or null if + * @returns The duplicated variable, or null if * the original variable was not found. */ - duplicateVariable (id, optKeepOriginalId) { + duplicateVariable (id: string, optKeepOriginalId?: boolean): Variable | null { if (Object.prototype.hasOwnProperty.call(this.variables, id)) { const originalVariable = this.variables[id]; const newVariable = new Variable( @@ -434,20 +442,20 @@ class Target extends EventEmitter { } /** - * Duplicate the dictionary of this target's variables as part of duplicating. + * Duplicate the dictionary of this target's variables as part of duplicating * this target or making a clone. - * @param {object=} optBlocks Optional block container for the target being duplicated. + * @param optBlocks Optional block container for the target being duplicated. * If provided, new variables will be generated with new UIDs and any variable references * in this blocks container will be updated to refer to the corresponding new IDs. - * @returns {object} The duplicated dictionary of variables + * @returns The duplicated dictionary of variables */ - duplicateVariables (optBlocks) { - let allVarRefs; + duplicateVariables (optBlocks?: Blocks | null): Record { + let allVarRefs: Record | undefined; if (optBlocks) { allVarRefs = optBlocks.getAllVariableAndListReferences(); } - return Object.keys(this.variables).reduce((accum, varId) => { - const newVariable = this.duplicateVariable(varId, !optBlocks); + return Object.keys(this.variables).reduce>((accum, varId) => { + const newVariable = this.duplicateVariable(varId, !optBlocks)!; accum[newVariable.id] = newVariable; if (optBlocks && allVarRefs) { const currVarRefs = allVarRefs[varId]; @@ -461,27 +469,25 @@ class Target extends EventEmitter { /** * Post/edit sprite info. - * @param {object} data An object with sprite info data to set. - * @abstract + * @param data An object with sprite info data to set. */ - // eslint-disable-next-line no-unused-vars - postSpriteInfo (data) {} + abstract postSpriteInfo (data: Record): void; /** * Retrieve custom state associated with this target and the provided state ID. - * @param {string} stateId - specify which piece of state to retrieve. - * @returns {*} the associated state, if any was found. + * @param stateId - specify which piece of state to retrieve. + * @returns the associated state, if any was found. */ - getCustomState (stateId) { - return this._customState[stateId]; + getCustomState (stateId: string): T | undefined { + return this._customState[stateId] as T | undefined; } /** * Store custom state associated with this target and the provided state ID. - * @param {string} stateId - specify which piece of state to store on this target. - * @param {*} newValue - the state value to store. + * @param stateId - specify which piece of state to store on this target. + * @param newValue - the state value to store. */ - setCustomState (stateId, newValue) { + setCustomState (stateId: string, newValue: T) { this._customState[stateId] = newValue; } @@ -493,7 +499,7 @@ class Target extends EventEmitter { this._customState = {}; if (this.runtime) { - this.runtime.removeExecutable(this); + this.runtime.removeExecutable(this as unknown as RenderedTarget); } } @@ -504,11 +510,11 @@ class Target extends EventEmitter { * For targets that are not the stage, this includes any target-specific * variables as well as any stage variables unless the skipStage flag is true. * For the stage, this is all stage variables. - * @param {string} type The variable type to search for; defaults to Variable.SCALAR_TYPE - * @param {?boolean} skipStage Optional flag to skip the stage. - * @returns {Array} A list of variable names + * @param type The variable type to search for; defaults to Variable.SCALAR_TYPE + * @param skipStage Optional flag to skip the stage. + * @returns A list of variable names */ - getAllVariableNamesInScopeByType (type, skipStage) { + getAllVariableNamesInScopeByType (type: VariableType, skipStage?: boolean): string[] { if (typeof type !== 'string') type = Variable.SCALAR_TYPE; skipStage = skipStage || false; const targetVariables = Object.values(this.variables) @@ -518,21 +524,27 @@ class Target extends EventEmitter { return targetVariables; } const stage = this.runtime.getTargetForStage(); + if (!stage) return targetVariables; const stageVariables = stage.getAllVariableNamesInScopeByType(type); return targetVariables.concat(stageVariables); } /** * Merge variable references with another variable. - * @param {string} idToBeMerged ID of the variable whose references need to be updated - * @param {string} idToMergeWith ID of the variable that the old references should be replaced with - * @param {?Array} optReferencesToUpdate Optional context of the change. + * @param idToBeMerged ID of the variable whose references need to be updated + * @param idToMergeWith ID of the variable that the old references should be replaced with + * @param optReferencesToUpdate Optional context of the change. * Defaults to all the blocks in this target. - * @param {?string} optNewName New variable name to merge with. The old + * @param optNewName New variable name to merge with. The old * variable name in the references being updated should be replaced with this new name. * If this parameter is not provided or is '', no name change occurs. */ - mergeVariables (idToBeMerged, idToMergeWith, optReferencesToUpdate, optNewName) { + mergeVariables ( + idToBeMerged: string, + idToMergeWith: string, + optReferencesToUpdate?: VarReference[], + optNewName?: string + ) { const referencesToChange = optReferencesToUpdate || // TODO should there be a separate helper function that traverses the blocks // for all references for a given ID instead of doing the below..? @@ -543,12 +555,12 @@ class Target extends EventEmitter { /** * Share a local variable (and given references for that variable) to the stage. - * @param {string} varId The ID of the variable to share. - * @param {Array} varRefs The list of variable references being shared, + * @param varId The ID of the variable to share. + * @param varRefs The list of variable references being shared, * that reference the given variable ID. The names and IDs of these variable * references will be updated to refer to the new (or pre-existing) global variable. */ - shareLocalVariableToStage (varId, varRefs) { + shareLocalVariableToStage (varId: string, varRefs: VarReference[]) { if (!this.runtime) return; const variable = this.variables[varId]; if (!variable) { @@ -564,7 +576,7 @@ class Target extends EventEmitter { // First check if we've already done the local to global transition for this // variable. If we have, merge it with the global variable we've already created. const varIdForStage = `StageVarFromLocal_${varId}`; - let stageVar = stage.lookupVariableById(varIdForStage); + let stageVar = stage?.lookupVariableById(varIdForStage); // If a global var doesn't already exist, create a new one with a fresh name. // Use the ID we created above so that we can lookup this new variable in the // future if we decide to share this same variable again. @@ -582,11 +594,11 @@ class Target extends EventEmitter { /** * Share a local variable with a sprite, merging with one of the same name and * type if it already exists on the sprite, or create a new one. - * @param {string} varId Id of the variable to share - * @param {Target} sprite The sprite to share the variable with - * @param {Array} varRefs A list of all the variable references currently being shared. + * @param varId Id of the variable to share + * @param sprite The sprite to share the variable with + * @param varRefs A list of all the variable references currently being shared. */ - shareLocalVariableToSprite (varId, sprite, varRefs) { + shareLocalVariableToSprite (varId: string, sprite: Target, varRefs: VarReference[]) { if (!this.runtime) return; if (this.isStage) return; const variable = this.variables[varId]; @@ -599,7 +611,7 @@ class Target extends EventEmitter { // Check if the receiving sprite already has a variable of the same name and type // and use the existing variable, otherwise create a new one. const existingLocalVar = sprite.lookupVariableByNameAndType(varName, varType); - let newVarId; + let newVarId: string; if (existingLocalVar) { newVarId = existingLocalVar.id; } else { @@ -633,11 +645,11 @@ class Target extends EventEmitter { * Create a new global variable with a fresh name and update all the referencing * fields to reference the new variable. * - * @param {Array} blocks The blocks containing + * @param blocks The blocks containing * potential conflicting references to variables. - * @param {Target} receivingTarget The target receiving the variables + * @param receivingTarget The target receiving the variables */ - resolveVariableSharingConflictsWithTarget (blocks, receivingTarget) { + resolveVariableSharingConflictsWithTarget (blocks: Record, receivingTarget: Target) { if (this.isStage) return; // Get all the variable references in the given list of blocks @@ -687,7 +699,7 @@ class Target extends EventEmitter { const stage = this.runtime.getTargetForStage(); if (!stage || !stage.variables) return; - const renameConflictingLocalVar = (id, name, type) => { + const renameConflictingLocalVar = (id: string, name: string, type: VariableType): string | null => { const conflict = stage.lookupVariableByNameAndType(name, type); if (conflict) { const newName = StringUtil.unusedName( @@ -700,20 +712,20 @@ class Target extends EventEmitter { }; const allReferences = this.blocks.getAllVariableAndListReferences(); - const unreferencedLocalVarIds = []; + const unreferencedLocalVarIds: string[] = []; if (Object.keys(this.variables).length > 0) { for (const localVarId in this.variables) { if (!Object.prototype.hasOwnProperty.call(this.variables, localVarId)) continue; if (!allReferences[localVarId]) unreferencedLocalVarIds.push(localVarId); } } - const conflictIdsToReplace = Object.create(null); - const conflictNamesToReplace = Object.create(null); + const conflictIdsToReplace: Record = Object.create(null); + const conflictNamesToReplace: Record = Object.create(null); // Cache the list of all variable names by type so that we don't need to // re-calculate this in every iteration of the following loop. - const varNamesByType = {}; - const allVarNames = type => { + const varNamesByType: Record = {}; + const allVarNames = (type: VariableType): string[] => { const namesOfType = varNamesByType[type]; if (namesOfType) return namesOfType; varNamesByType[type] = this.runtime.getAllVarNamesOfType(type); @@ -738,7 +750,7 @@ class Target extends EventEmitter { // We are not calling this.blocks.updateBlocksAfterVarRename // here because it will search through all the blocks. We already // have access to all the references for this var id. - allReferences[varId].map(ref => { + allReferences[varId].map((ref: VarReference) => { ref.referencingField.value = newVarName; return ref; }); @@ -779,7 +791,7 @@ class Target extends EventEmitter { // Handle global var conflicts with existing global vars (e.g. a sprite is uploaded, and has // blocks referencing some variable that the sprite does not own, and this // variable conflicts with a global var) - // In this case, we want to merge the new variable referenes with the + // In this case, we want to merge the new variable references with the // existing global variable for (const conflictId in conflictIdsToReplace) { const existingId = conflictIdsToReplace[conflictId]; @@ -787,15 +799,15 @@ class Target extends EventEmitter { this.mergeVariables(conflictId, existingId, referencesToUpdate); } - // Handle global var conflicts existing local vars (e.g a sprite is uploaded, + // Handle global var conflicts with existing local vars (e.g a sprite is uploaded, // and has blocks referencing some variable that the sprite does not own, and this - // variable conflcits with another sprite's local var). + // variable conflicts with another sprite's local var). // In this case, we want to go through the variable references and update // the name of the variable in that reference. for (const conflictId in conflictNamesToReplace) { const newName = conflictNamesToReplace[conflictId]; const referencesToUpdate = allReferences[conflictId]; - referencesToUpdate.map(ref => { + referencesToUpdate.map((ref: VarReference) => { ref.referencingField.value = newName; return ref; }); diff --git a/packages/vm/src/engine/thread.ts b/packages/vm/src/engine/thread.ts index 5d529460b..785428854 100644 --- a/packages/vm/src/engine/thread.ts +++ b/packages/vm/src/engine/thread.ts @@ -1,6 +1,7 @@ import type Blocks from './blocks'; import type Timer from '../util/timer'; import type RenderedTarget from '../sprites/rendered-target'; +import type {CachedArgValue} from './execute'; /** * Recycle bin for empty stackFrame objects @@ -10,7 +11,6 @@ const _stackFrameFreeList: _StackFrame[] = []; /** * A frame used for each level of the stack. A general purpose * place to store a bunch of execution context and parameters - * @private */ class _StackFrame { /** @@ -24,11 +24,11 @@ class _StackFrame { /** * The active block that is waiting on a promise. */ - reporting = ''; + reporting: string | null = null; /** * Persists reported inputs during async block. */ - reported: Record | null = null; + reported: {opCached: string, inputValue: CachedArgValue}[] | null = null; /** * Whether is waiting a custom reporter. */ @@ -72,7 +72,7 @@ class _StackFrame { * Reuse an active stack frame in the stack. * @param warpMode defaults to current warpMode */ - reuse (warpMode: boolean = this.warpMode): this { + reuse (warpMode = this.warpMode): this { this.reset(); this.warpMode = Boolean(warpMode); return this; @@ -95,7 +95,7 @@ class _StackFrame { * Put a stack frame object into the recycle bin for reuse. * @param stackFrame The frame to reset and recycle. */ - static release (stackFrame: _StackFrame): void { + static release (stackFrame: _StackFrame) { if (typeof stackFrame !== 'undefined') { _stackFrameFreeList.push(stackFrame.reset()); } @@ -130,7 +130,7 @@ class Thread { /** * Status of the thread, one of three states (below) */ - status: ThreadStatus = ThreadStatus.RUNNING; + status = ThreadStatus.RUNNING; /** * Whether the thread is killed in the middle of execution. */ @@ -142,11 +142,11 @@ class Thread { /** * The Blocks this thread will execute. */ - blockContainer: Blocks | null = null; + blockContainer?: Blocks | null = null; /** * Whether the thread requests its script to glow during this frame. */ - requestScriptGlowInFrame: boolean = false; + requestScriptGlowInFrame = false; /** * Which block ID should glow during this frame, if any. */ @@ -219,7 +219,7 @@ class Thread { * @param blockId Block ID to push to stack. * @param target New target context. */ - pushStack (blockId: string | null, target?: RenderedTarget): void { + pushStack (blockId: string | null, target?: RenderedTarget) { this.stack.push(blockId); // Push an empty stack frame, if we need one. // Might not, if we just popped the stack. @@ -243,7 +243,7 @@ class Thread { * (avoids popping and re-pushing a new stack frame - keeps the warpmode the same * @param blockId Block ID to push to stack. */ - reuseStackForNextBlock (blockId: string): void { + reuseStackForNextBlock (blockId: string) { this.stack[this.stack.length - 1] = blockId; this.stackFrames[this.stackFrames.length - 1].reuse(); } @@ -264,7 +264,7 @@ class Thread { /** * Pop back down the stack frame until we hit a procedure call or the stack frame is emptied */ - stopThisScript (): void { + stopThisScript () { let blockID = this.peekStack(); while (blockID !== null) { const block = this.blockContainer!.getBlock(blockID); @@ -317,14 +317,14 @@ class Thread { * Push a reported value to the parent of the current stack frame. * @param value Reported value to push. */ - pushReportedValue (value: unknown): void { + pushReportedValue (value: unknown) { this.justReported = typeof value === 'undefined' ? null : value; } /** * Initialize procedure parameters on this stack frame. */ - initParams (): void { + initParams () { const stackFrame = this.peekStackFrame(); if (stackFrame && stackFrame.params === null) { stackFrame.params = {}; @@ -337,7 +337,7 @@ class Thread { * @param paramName Name of parameter. * @param value Value to set for parameter. */ - pushParam (paramName: string, value: unknown): void { + pushParam (paramName: string, value: unknown) { const stackFrame = this.peekStackFrame()!; stackFrame.params![paramName] = value; } @@ -375,7 +375,7 @@ class Thread { * For example, this is used in a standard sequence of blocks, * where execution proceeds from one block to the next. */ - goToNextBlock (): void { + goToNextBlock () { const nextBlockId = this.blockContainer!.getNextBlock(this.peekStack()!) as string; this.reuseStackForNextBlock(nextBlockId); } @@ -412,4 +412,6 @@ class Thread { } } +export type {Thread, _StackFrame as ThreadStackFrame}; + export default Thread; diff --git a/packages/vm/src/engine/variable.ts b/packages/vm/src/engine/variable.ts index 8c8827c25..40374fb48 100644 --- a/packages/vm/src/engine/variable.ts +++ b/packages/vm/src/engine/variable.ts @@ -35,7 +35,7 @@ class Variable { // eslint-disable-next-line @typescript-eslint/no-explicit-any value: any; - constructor (id: string, name: string, type: VariableType, isCloud: boolean) { + constructor (id: string | null, name: string, type: VariableType, isCloud = false) { this.id = id || uid(); this.name = name; this.type = type; diff --git a/packages/vm/src/extension-support/extension-manager.js b/packages/vm/src/extension-support/extension-manager.ts similarity index 55% rename from packages/vm/src/extension-support/extension-manager.js rename to packages/vm/src/extension-support/extension-manager.ts index f636ba015..19a3ae742 100644 --- a/packages/vm/src/extension-support/extension-manager.js +++ b/packages/vm/src/extension-support/extension-manager.ts @@ -3,98 +3,92 @@ import log from '../util/log'; import maybeFormatMessage from '../util/maybe-format-message'; import BlockType from './block-type'; +import type Runtime from '../engine/runtime'; +import type { + ExtensionClass, + ExtensionMetadata, + ExtensionMenuItem, + ExtensionMenuItemObject, + ExtensionItemMetadata, + NormalizedExtensionMetadata, + NormalizedExtensionBlockMetadata, + ExtensionButtonMetadata, + NormalizedExtensionItemMetadata +} from './extension-metadata'; +import type {BlockArgs} from '../blocks/category_prototype'; +import type BlockUtility from '../engine/block-utility'; + +type CallBlockFunc = (args: BlockArgs, util: BlockUtility, realBlockInfo: Record) => unknown; + // These extensions are currently built into the VM repository but should not be loaded at startup. // TODO: move these out into a separate repository? // TODO: change extension spec so that library info, including extension ID, can be collected through static methods -/* eslint-disable global-require */ +/* eslint-disable global-require, @typescript-eslint/no-require-imports */ const builtinExtensions = { // This is an example that isn't loaded with the other core blocks, // but serves as a reference for loading core blocks as extensions. - coreExample: () => require('../blocks/scratch3_core_example'), + coreExample: (): typeof import('../blocks/scratch3_core_example') => require('../blocks/scratch3_core_example'), // These are the non-core built-in extensions. - pen: () => require('../extensions/scratch3_pen'), - wedo2: () => require('../extensions/scratch3_wedo2'), - music: () => require('../extensions/scratch3_music'), - microbit: () => require('../extensions/scratch3_microbit'), - text2speech: () => require('../extensions/scratch3_text2speech'), - translate: () => require('../extensions/scratch3_translate'), - videoSensing: () => require('../extensions/scratch3_video_sensing'), - ev3: () => require('../extensions/scratch3_ev3'), - makeymakey: () => require('../extensions/scratch3_makeymakey'), - boost: () => require('../extensions/scratch3_boost'), - gdxfor: () => require('../extensions/scratch3_gdx_for') + pen: (): typeof import('../extensions/scratch3_pen') => require('../extensions/scratch3_pen'), + wedo2: (): typeof import('../extensions/scratch3_wedo2') => require('../extensions/scratch3_wedo2'), + music: (): typeof import('../extensions/scratch3_music') => require('../extensions/scratch3_music'), + microbit: (): typeof import('../extensions/scratch3_microbit') => require('../extensions/scratch3_microbit'), + text2speech: (): + typeof import('../extensions/scratch3_text2speech') => require('../extensions/scratch3_text2speech'), + translate: (): typeof import('../extensions/scratch3_translate') => require('../extensions/scratch3_translate'), + videoSensing: (): + typeof import('../extensions/scratch3_video_sensing') => require('../extensions/scratch3_video_sensing'), + ev3: (): typeof import('../extensions/scratch3_ev3') => require('../extensions/scratch3_ev3'), + makeymakey: (): typeof import('../extensions/scratch3_makeymakey') => require('../extensions/scratch3_makeymakey'), + boost: (): typeof import('../extensions/scratch3_boost') => require('../extensions/scratch3_boost'), + gdxfor: (): typeof import('../extensions/scratch3_gdx_for') => require('../extensions/scratch3_gdx_for') }; -/* eslint-enable global-require */ - -/** - * @typedef {object} ArgumentInfo - Information about an extension block argument - * @property {ArgumentType} type - the type of value this argument can take - * @property {*|undefined} default - the default value of this argument (default: blank) - */ +/* eslint-enable global-require, @typescript-eslint/no-require-imports */ -/** - * @typedef {object} ConvertedBlockInfo - Raw extension block data paired with processed data ready for scratch-blocks - * @property {ExtensionBlockMetadata} info - the raw block info - * @property {object} json - the scratch-blocks JSON definition for this block - * @property {string} xml - the scratch-blocks XML definition for this block - */ - -/** - * @typedef {object} CategoryInfo - Information about a block category - * @property {string} id - the unique ID of this category - * @property {string} name - the human-readable name of this category - * @property {string|undefined} blockIconURI - optional URI for the block icon image - * @property {string} color1 - the primary color for this category, in '#rrggbb' format - * @property {string} color2 - the secondary color for this category, in '#rrggbb' format - * @property {string} color3 - the tertiary color for this category, in '#rrggbb' format - * @property {Array.} blocks - the blocks, separators, etc. in this category - * @property {Array.} menus - the menus provided by this category - */ +export type BuiltinExtensionId = keyof typeof builtinExtensions; /** - * @typedef {object} PendingExtensionWorker - Information about an extension worker still initializing - * @property {string} extensionURL - the URL of the extension to be loaded by this worker - * @property {Function} resolve - function to call on successful worker startup - * @property {Function} reject - function to call on failed worker startup + * Information about an extension worker still initializing */ +interface PendingExtensionWorker { + /** The URL of the extension to be loaded by this worker */ + extensionURL: string; + /** Function to call on successful worker startup */ + resolve: (id: number) => void; + /** Function to call on failed worker startup */ + reject: (e: Error) => void; +} class ExtensionManager { - constructor (runtime) { - /** - * The ID number to provide to the next extension worker. - * @type {int} - */ - this.nextExtensionWorker = 0; - - /** - * FIFO queue of extensions which have been requested but not yet loaded in a worker, - * along with promise resolution functions to call once the worker is ready or failed. - * - * @type {Array.} - */ - this.pendingExtensions = []; + /** + * The ID number to provide to the next extension worker. + */ + nextExtensionWorker = 0; - /** - * Map of worker ID to workers which have been allocated but have not yet finished initialization. - * @type {Array.} - */ - this.pendingWorkers = []; + /** + * FIFO queue of extensions which have been requested but not yet loaded in a worker, + * along with promise resolution functions to call once the worker is ready or failed. + */ + pendingExtensions: PendingExtensionWorker[] = []; - /** - * Map of loaded extension URLs/IDs (equivalent for built-in extensions) to service name. - * @type {Map.} - * @private - */ - this._loadedExtensions = new Map(); + /** + * Map of worker ID to workers which have been allocated but have not yet finished initialization. + */ + pendingWorkers: PendingExtensionWorker[] = []; + /** + * Map of loaded extension URLs/IDs (equivalent for built-in extensions) to service name. + * @private + */ + _loadedExtensions: Map = new Map(); + constructor ( /** * Keep a reference to the runtime so we can construct internal extension objects. * TODO: remove this in favor of extensions accessing the runtime as a service. - * @type {Runtime} */ - this.runtime = runtime; - + public runtime: Runtime + ) { dispatch.setService('extensions', this).catch(e => { log.error(`ExtensionManager was unable to register extension service: ${JSON.stringify(e)}`); }); @@ -104,19 +98,19 @@ class ExtensionManager { * Check whether an extension is registered or is in the process of loading. This is intended to control loading or * adding extensions so it may return `true` before the extension is ready to be used. Use the promise returned by * `loadExtensionURL` if you need to wait until the extension is truly ready. - * @param {string} extensionID - the ID of the extension. - * @returns {boolean} - true if loaded, false otherwise. + * @param extensionID - the ID of the extension. + * @returns true if loaded, false otherwise. */ - isExtensionLoaded (extensionID) { + isExtensionLoaded (extensionID: string) { return this._loadedExtensions.has(extensionID); } /** * Synchronously load an internal extension (core or non-core) by ID. This call will * fail if the provided id is not does not match an internal extension. - * @param {string} extensionId - the ID of an internal extension + * @param extensionId - the ID of an internal extension */ - loadExtensionIdSync (extensionId) { + loadExtensionIdSync (extensionId: BuiltinExtensionId) { if (!Object.prototype.hasOwnProperty.call(builtinExtensions, extensionId)) { log.warn(`Could not find extension ${extensionId} in the built in extensions.`); return; @@ -131,16 +125,17 @@ class ExtensionManager { const {default: extension} = builtinExtensions[extensionId](); const extensionInstance = new extension(this.runtime); + // @ts-expect-error Returned manifest should always const. remove this since we migrated all extension to ts. const serviceName = this._registerInternalExtension(extensionInstance); this._loadedExtensions.set(extensionId, serviceName); } /** * Load an extension by URL or internal extension ID - * @param {string} extensionURL - the URL for the extension to load OR the ID of an internal extension - * @returns {Promise} resolved once the extension is loaded and initialized or rejected on failure + * @param extensionURL - the URL for the extension to load OR the ID of an internal extension + * @returns resolved once the extension is loaded and initialized or rejected on failure */ - loadExtensionURL (extensionURL) { + loadExtensionURL (extensionURL: string) { if (Object.prototype.hasOwnProperty.call(builtinExtensions, extensionURL)) { /** @todo dupe handling for non-builtin extensions. See commit 670e51d33580e8a2e852b3b038bb3afc282f81b9 */ if (this.isExtensionLoaded(extensionURL)) { @@ -149,30 +144,27 @@ class ExtensionManager { return Promise.resolve(); } - const {default: extension} = builtinExtensions[extensionURL](); + const {default: extension} = builtinExtensions[extensionURL as BuiltinExtensionId](); const extensionInstance = new extension(this.runtime); + // @ts-expect-error Returned manifest should always const. remove this since we migrated all extension to ts const serviceName = this._registerInternalExtension(extensionInstance); this._loadedExtensions.set(extensionURL, serviceName); return Promise.resolve(); } - return new Promise((resolve, reject) => { - /** - * If we `require` this at the global level it breaks non-webpack targets, including tests - * Also, webpack 5's implementation will break non-webpack targets since VM is not a ESModule. - * Before VM migration to ESM, we still need to use worker-loader to solve this problem. - */ - // eslint-disable-next-line global-require - const ExtensionWorker = require('codingclip-worker-loader?filename=extension-worker.js!./extension-worker'); + return new Promise((resolve, reject) => { + const worker = new Worker( + /* webpackChunkName: "extension-worker" */ new URL('./extension-worker', import.meta.url) + ); this.pendingExtensions.push({extensionURL, resolve, reject}); - dispatch.addWorker(new ExtensionWorker()); - }); + dispatch.addWorker(worker); + }).then(() => {}); } /** * Regenerate blockinfo for any loaded extensions - * @returns {Promise} resolved once all the extensions have been reinitialized + * @returns resolved once all the extensions have been reinitialized */ refreshBlocks () { const allPromises = Array.from(this._loadedExtensions.values()).map(serviceName => @@ -190,25 +182,25 @@ class ExtensionManager { allocateWorker () { const id = this.nextExtensionWorker++; - const workerInfo = this.pendingExtensions.shift(); + const workerInfo = this.pendingExtensions.shift()!; this.pendingWorkers[id] = workerInfo; return [id, workerInfo.extensionURL]; } /** * Synchronously collect extension metadata from the specified service and begin the extension registration process. - * @param {string} serviceName - the name of the service hosting the extension. + * @param serviceName - the name of the service hosting the extension. */ - registerExtensionServiceSync (serviceName) { + registerExtensionServiceSync (serviceName: string) { const info = dispatch.callSync(serviceName, 'getInfo'); this._registerExtensionInfo(serviceName, info); } /** * Collect extension metadata from the specified service and begin the extension registration process. - * @param {string} serviceName - the name of the service hosting the extension. + * @param serviceName - the name of the service hosting the extension. */ - registerExtensionService (serviceName) { + registerExtensionService (serviceName: string) { dispatch.call(serviceName, 'getInfo').then(info => { this._registerExtensionInfo(serviceName, info); }); @@ -216,10 +208,10 @@ class ExtensionManager { /** * Called by an extension worker to indicate that the worker has finished initialization. - * @param {int} id - the worker ID. - * @param {*?} e - the error encountered during initialization, if any. + * @param id - the worker ID. + * @param e - the error encountered during initialization, if any. */ - onWorkerInit (id, e) { + onWorkerInit (id: number, e: Error | null) { const workerInfo = this.pendingWorkers[id]; delete this.pendingWorkers[id]; if (e) { @@ -231,10 +223,10 @@ class ExtensionManager { /** * Register an internal (non-Worker) extension object - * @param {object} extensionObject - the extension object to register - * @returns {string} The name of the registered extension service + * @param extensionObject - the extension object to register + * @returns The name of the registered extension service */ - _registerInternalExtension (extensionObject) { + _registerInternalExtension (extensionObject: ExtensionClass) { const extensionInfo = extensionObject.getInfo(); const fakeWorkerId = this.nextExtensionWorker++; const serviceName = `extension_${fakeWorkerId}_${extensionInfo.id}`; @@ -245,12 +237,12 @@ class ExtensionManager { /** * Sanitize extension info then register its primitives with the VM. - * @param {string} serviceName - the name of the service hosting the extension - * @param {ExtensionInfo} extensionInfo - the extension's metadata + * @param serviceName - the name of the service hosting the extension + * @param extensionInfo - the extension's metadata * @private */ - _registerExtensionInfo (serviceName, extensionInfo) { - extensionInfo = this._prepareExtensionInfo(serviceName, extensionInfo); + _registerExtensionInfo (serviceName: string, extensionInfo: ExtensionMetadata) { + (extensionInfo as NormalizedExtensionMetadata) = this._prepareExtensionInfo(serviceName, extensionInfo); dispatch.call('runtime', '_registerExtensionPrimitives', extensionInfo).catch(e => { log.error(`Failed to register primitives for extension on service ${serviceName}:`, e); }); @@ -258,23 +250,23 @@ class ExtensionManager { /** * Modify the provided text as necessary to ensure that it may be used as an attribute value in valid XML. - * @param {string} text - the text to be sanitized - * @returns {string} - the sanitized text + * @param text - the text to be sanitized + * @returns the sanitized text * @private */ - _sanitizeID (text) { + _sanitizeID (text: string) { return text.toString().replace(/[<"&]/, '_'); } /** * Apply minor cleanup and defaults for optional extension fields. * TODO: make the ID unique in cases where two copies of the same extension are loaded. - * @param {string} serviceName - the name of the service hosting this extension block - * @param {ExtensionInfo} extensionInfo - the extension info to be sanitized - * @returns {ExtensionInfo} - a new extension info object with cleaned-up values + * @param serviceName - the name of the service hosting this extension block + * @param extensionInfo - the extension info to be sanitized + * @returns a new extension info object with cleaned-up values * @private */ - _prepareExtensionInfo (serviceName, extensionInfo) { + _prepareExtensionInfo (serviceName: string, extensionInfo: ExtensionMetadata) { extensionInfo = Object.assign({}, extensionInfo); if (!/^[a-z0-9]+$/i.test(extensionInfo.id)) { throw new Error('Invalid extension id'); @@ -284,7 +276,7 @@ class ExtensionManager { extensionInfo.targetTypes = extensionInfo.targetTypes || []; extensionInfo.blocks = extensionInfo.blocks.reduce((results, blockInfo) => { try { - let result; + let result: NormalizedExtensionItemMetadata; switch (blockInfo) { case '---': // separator result = '---'; @@ -296,23 +288,23 @@ class ExtensionManager { results.push(result); } catch (e) { // TODO: more meaningful error reporting - log.error(`Error processing block: ${e.message}, Block:\n${JSON.stringify(blockInfo)}`); + log.error(`Error processing block: ${(e as Error).message}, Block:\n${JSON.stringify(blockInfo)}`); } return results; - }, []); + }, [] as NormalizedExtensionItemMetadata[]) as ExtensionItemMetadata[]; extensionInfo.menus = extensionInfo.menus || {}; extensionInfo.menus = this._prepareMenuInfo(serviceName, extensionInfo.menus); - return extensionInfo; + return extensionInfo as NormalizedExtensionMetadata; } /** * Prepare extension menus. e.g. setup binding for dynamic menu functions. - * @param {string} serviceName - the name of the service hosting this extension block - * @param {Array.} menus - the menu defined by the extension. - * @returns {Array.} - a menuInfo object with all preprocessing done. + * @param serviceName - the name of the service hosting this extension block + * @param menus - the menu defined by the extension. + * @returns a menuInfo object with all preprocessing done. * @private */ - _prepareMenuInfo (serviceName, menus) { + _prepareMenuInfo (serviceName: string, menus: Record) { const menuNames = Object.getOwnPropertyNames(menus); for (let i = 0; i < menuNames.length; i++) { const menuName = menuNames[i]; @@ -320,7 +312,7 @@ class ExtensionManager { // If the menu description is in short form (items only) then normalize it to general form: an object with // its items listed in an `items` property. - if (!menuInfo.items) { + if (typeof menuInfo === 'string' || !('items' in menuInfo)) { menuInfo = { items: menuInfo }; @@ -330,7 +322,7 @@ class ExtensionManager { // function should return an array of items to populate the menu when it is opened. if (typeof menuInfo.items === 'string') { const menuItemFunctionName = menuInfo.items; - const serviceObject = dispatch.services[serviceName]; + const serviceObject = dispatch.services[serviceName] as ExtensionClass; // Bind the function here so we can pass a simple item generation function to Scratch Blocks later. menuInfo.items = this._getExtensionMenuItems.bind(this, serviceObject, menuItemFunctionName); } @@ -340,33 +332,36 @@ class ExtensionManager { /** * Fetch the items for a particular extension menu, providing the target ID for context. - * @param {object} extensionObject - the extension object providing the menu. - * @param {string} menuItemFunctionName - the name of the menu function to call. - * @returns {Array} menu items ready for scratch-blocks. + * @param extensionObject - the extension object providing the menu. + * @param menuItemFunctionName - the name of the menu function to call. + * @returns menu items ready for scratch-blocks. * @private */ - _getExtensionMenuItems (extensionObject, menuItemFunctionName) { + _getExtensionMenuItems (extensionObject: ExtensionClass, menuItemFunctionName: string) { // Fetch the items appropriate for the target currently being edited. This assumes that menus only // collect items when opened by the user while editing a particular target. const editingTarget = this.runtime.getEditingTarget() || this.runtime.getTargetForStage(); const editingTargetID = editingTarget ? editingTarget.id : null; - const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget); + // const extensionMessageContext = this.runtime.makeMessageContextForTarget(editingTarget); // TODO: Fix this to use dispatch.call when extensions are running in workers. - const menuFunc = extensionObject[menuItemFunctionName]; + const menuFunc = + extensionObject[menuItemFunctionName as keyof ExtensionClass] as unknown as + (editingTargetID?: string | null) => ExtensionMenuItem[]; const menuItems = menuFunc.call(extensionObject, editingTargetID).map( item => { - item = maybeFormatMessage(item, extensionMessageContext); + item = maybeFormatMessage(item); switch (typeof item) { case 'object': return [ - maybeFormatMessage(item.text, extensionMessageContext), - item.value - ]; + maybeFormatMessage((item as unknown as ExtensionMenuItemObject).text), + (item as unknown as ExtensionMenuItemObject).value + ] as [string, string]; case 'string': - return [item, item]; + return [item, item] as [string, string]; default: - return item; + console.warn(`Invalid menu item returned by ${menuItemFunctionName}`, item); + return item as unknown as [string, string]; } }); @@ -378,20 +373,18 @@ class ExtensionManager { /** * Apply defaults for optional block fields. - * @param {string} serviceName - the name of the service hosting this extension block - * @param {ExtensionBlockMetadata} blockInfo - the block info from the extension - * @returns {ExtensionBlockMetadata} - a new block info object which has values for all relevant optional fields. + * @param serviceName - the name of the service hosting this extension block + * @param blockInfo - the block info from the extension + * @returns a new block info object which has values for all relevant optional fields. * @private */ - _prepareBlockInfo (serviceName, blockInfo) { + _prepareBlockInfo (serviceName: string, blockInfo: Exclude) { blockInfo = Object.assign({}, { blockType: BlockType.COMMAND, terminal: false, blockAllThreads: false, arguments: {} }, blockInfo); - blockInfo.opcode = blockInfo.opcode && this._sanitizeID(blockInfo.opcode); - blockInfo.text = blockInfo.text || blockInfo.opcode; switch (blockInfo.blockType) { case BlockType.EVENT: @@ -400,46 +393,49 @@ class ExtensionManager { } break; case BlockType.BUTTON: - if (blockInfo.opcode) { + if ('opcode' in blockInfo) { log.warn(`Ignoring opcode "${blockInfo.opcode}" for button with text: ${blockInfo.text}`); } break; default: { + blockInfo.opcode = blockInfo.opcode && this._sanitizeID(blockInfo.opcode); + blockInfo.text = blockInfo.text || blockInfo.opcode; if (!blockInfo.opcode) { throw new Error('Missing opcode for block'); } - const funcName = blockInfo.func ? this._sanitizeID(blockInfo.func) : blockInfo.opcode; + const funcName = typeof blockInfo.func === 'string' ? this._sanitizeID(blockInfo.func) : blockInfo.opcode; const getBlockInfo = blockInfo.isDynamic ? - args => args && args.mutation && args.mutation.blockInfo : + (args: BlockArgs) => args && args.mutation && args.mutation.blockInfo : () => blockInfo; const callBlockFunc = (() => { if (dispatch.isRemoteService(serviceName)) { - return (args, util, realBlockInfo) => + return (args: BlockArgs, util: BlockUtility, realBlockInfo: Record) => dispatch.call(serviceName, funcName, args, util, realBlockInfo); } // avoid promise latency if we can call direct - const serviceObject = dispatch.services[serviceName]; - if (!serviceObject[funcName]) { + const serviceObject = dispatch.services[serviceName] as ExtensionClass; + if (!(funcName in serviceObject)) { // The function might show up later as a dynamic property of the service object log.warn(`Could not find extension block function called ${funcName}`); } - return (args, util, realBlockInfo) => - serviceObject[funcName](args, util, realBlockInfo); + return (args: BlockArgs, util: BlockUtility, realBlockInfo: Record) => + (serviceObject[funcName as keyof ExtensionClass] as CallBlockFunc)(args, util, realBlockInfo); })(); - blockInfo.func = (args, util) => { - const realBlockInfo = getBlockInfo(args); - // TODO: filter args using the keys of realBlockInfo.arguments? maybe only if sandboxed? - return callBlockFunc(args, util, realBlockInfo); - }; + (blockInfo as unknown as NormalizedExtensionBlockMetadata).func = + (args: BlockArgs, util: BlockUtility) => { + const realBlockInfo = getBlockInfo(args); + // TODO: filter args using the keys of realBlockInfo.arguments? maybe only if sandboxed? + return callBlockFunc(args, util, realBlockInfo); + }; break; } } - return blockInfo; + return blockInfo as (ExtensionButtonMetadata | NormalizedExtensionBlockMetadata); } } diff --git a/packages/vm/src/extension-support/extension-metadata.ts b/packages/vm/src/extension-support/extension-metadata.ts index 6fc4bbae8..9bd980d49 100644 --- a/packages/vm/src/extension-support/extension-metadata.ts +++ b/packages/vm/src/extension-support/extension-metadata.ts @@ -1,7 +1,10 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import type {BlockFunction} from '../blocks/category_prototype'; +import type {JsonBlockDefinition} from '../types/json-block-definitions'; import type ArgumentType from './argument-type'; import type BlockType from './block-type'; import type ReporterScope from './reporter-scope'; +import type TargetType from './target-type'; /** * All the metadata needed to register an extension. @@ -18,21 +21,82 @@ export interface ExtensionMetadata { /** Link to documentation content for this extension. */ docsURI?: string; /** The blocks provided by this extension, plus separators. */ - blocks: Array; + blocks: ExtensionItemMetadata[]; /** Map of menu name to metadata for each of this extension's menus. */ - menus?: Record; + menus?: Record; + /** Whether to show a status button for this extension. */ + showStatusButton?: boolean; + /** The primary color for this extension. */ + color1?: string; + /** The secondary color for this extension. */ + color2?: string; + /** The tertiary color for this extension. */ + color3?: string; + /** + * New target type(s). + * @todo Not implemented by VM. + */ + targetTypes?: string[]; + /** + * Custom field types used by this extension's blocks, if any. + * @todo Not implemented by VM. + */ + customFieldTypes?: Record; +} + +/** + * ExtensionMetadata but normalized by extension manager and passed to runtime. + * @internal + */ +export interface NormalizedExtensionMetadata extends Omit { + menus?: Record; + blocks: NormalizedExtensionItemMetadata[]; +} + +export type ExtensionItemMetadata = ExtensionBlockMetadata | ExtensionButtonMetadata | '---'; +/** @internal */ +export type NormalizedExtensionItemMetadata = NormalizedExtensionBlockMetadata | ExtensionButtonMetadata | '---'; + +export interface ExtensionCustomFieldTypeMetadata { + output: JsonBlockDefinition['output']; + outputShape: JsonBlockDefinition['outputShape']; + implementation: any; +} + +export interface ExtensionCustomFieldTypeInfo { + fieldName: string; + extendedName: string; + argumentTypeInfo: { + shadow: { + type: string; + fieldName: string; + } + } + scratchBlocksDefinition: { + json: JsonBlockDefinition; + } + fieldImplementation: any; } /** * All the metadata needed to register an extension block. */ + +export interface ExtensionButtonMetadata { + blockType: BlockType.BUTTON; + text: string; + func?: string; + filter?: TargetType[]; + hideFromPalette?: boolean; +} + export interface ExtensionBlockMetadata { /** A unique alphanumeric identifier for this block. No special characters allowed. */ opcode: string; /** The name of the function implementing this block. Can be shared by other blocks/opcodes. */ func?: string; /** The type of block (command, reporter, etc.) being described. */ - blockType: BlockType; + blockType: Exclude; /** The text on the block, with [PLACEHOLDERS] for arguments. */ text: string; /** True if this block should not appear in the block palette. */ @@ -51,6 +115,19 @@ export interface ExtensionBlockMetadata { branchCount?: number; /** Map of argument placeholder to metadata about each arg. */ arguments?: Record; + blockIconURI?: string; + isDynamic?: boolean; + filter?: TargetType[]; +} + +/** @internal */ +export type NormalizedExtensionBlockMetadata = Omit & { + func: BlockFunction; + /** not implemented */ + blockAllThreads: boolean; + /** not implemented, seems a typo of `isTerminal`? */ + terminal: boolean; + arguments: Required['arguments']; } /** @@ -65,32 +142,41 @@ export interface ExtensionArgumentMetadata { menu?: string; } -/** - * All the metadata needed to register an extension drop-down menu. - */ -export type ExtensionMenuMetadata = ExtensionDynamicMenu | ExtensionMenuItems; - -/** - * The string name of a function which returns menu items. - */ -export type ExtensionDynamicMenu = string; - -/** - * Items in an extension menu. - */ -export type ExtensionMenuItems = Array; - -/** - * A menu item for which the label and value are identical strings. - */ -export type ExtensionMenuItemSimple = string; +export interface ExtensionImageMetadata { + type: ArgumentType.IMAGE; + dataURI?: string; + flipRTL?: boolean; +} /** * A menu item for which the label and value can differ. */ -export interface ExtensionMenuItemComplex { +export interface NormalizedExtensionMenuItem { + items: ShortExtensionMenuItem | string[]; + acceptReporters?: boolean; +} + +export interface ExtensionMenuItemObject { /** The value of the block argument when this menu item is selected. */ - value: any; + value: string; /** The human-readable label of this menu item in the menu. */ text: string; } + +export type MenuItemFunction = ((editingTargetId?: string | null) => [string, string][]); + +export type ShortExtensionMenuItem = + MenuItemFunction | ExtensionMenuItemObject[]; + +export type ExtensionMenuItem = NormalizedExtensionMenuItem | ShortExtensionMenuItem | string[]; + +export interface ExtensionClass { + getInfo(): ExtensionMetadata; +} + +export interface PeripheralExtensionClass { + scan(): void; + connect(peripheralId: number): void; + disconnect(): void; + isConnected(): boolean; +} diff --git a/packages/vm/src/extension-support/extension-worker.js b/packages/vm/src/extension-support/extension-worker.ts similarity index 77% rename from packages/vm/src/extension-support/extension-worker.js rename to packages/vm/src/extension-support/extension-worker.ts index f1abfcb62..2753c6468 100644 --- a/packages/vm/src/extension-support/extension-worker.js +++ b/packages/vm/src/extension-support/extension-worker.ts @@ -2,13 +2,14 @@ import ArgumentType from '../extension-support/argument-type'; import BlockType from '../extension-support/block-type'; import dispatch from '../dispatch/worker-dispatch'; import TargetType from './target-type'; +import type {ExtensionClass} from './extension-metadata'; class ExtensionWorker { + nextExtensionId = 0; + initialRegistrations: Promise[] | null = []; + workerId?: number; + extensions: ExtensionClass[] = []; constructor () { - this.nextExtensionId = 0; - - this.initialRegistrations = []; - dispatch.waitForConnection.then(() => { dispatch.call('extensions', 'allocateWorker').then(x => { const [id, extension] = x; @@ -17,7 +18,7 @@ class ExtensionWorker { try { importScripts(extension); - const initialRegistrations = this.initialRegistrations; + const initialRegistrations = this.initialRegistrations!; this.initialRegistrations = null; Promise.all(initialRegistrations).then(() => dispatch.call('extensions', 'onWorkerInit', id)); @@ -30,7 +31,7 @@ class ExtensionWorker { this.extensions = []; } - register (extensionObject) { + register (extensionObject: ExtensionClass) { const extensionId = this.nextExtensionId++; this.extensions.push(extensionObject); const serviceName = `extension.${this.workerId}.${extensionId}`; @@ -43,6 +44,17 @@ class ExtensionWorker { } } +declare global { + var Scratch: { + extensions: { + register: (extensionObject: ExtensionClass) => Promise; + }, + ArgumentType: typeof ArgumentType; + BlockType: typeof BlockType; + TargetType: typeof TargetType; + }; +} + global.Scratch = global.Scratch || {}; global.Scratch.ArgumentType = ArgumentType; global.Scratch.BlockType = BlockType; diff --git a/packages/vm/src/extensions/scratch3_boost/index.js b/packages/vm/src/extensions/scratch3_boost/index.js index 18bbd8fc7..fe8e0388c 100644 --- a/packages/vm/src/extensions/scratch3_boost/index.js +++ b/packages/vm/src/extensions/scratch3_boost/index.js @@ -3,7 +3,7 @@ import BlockType from '../../extension-support/block-type'; import Cast from '../../util/cast'; import formatMessage from 'format-message'; import color from '../../util/color'; -import BLE from '../../io/ble.js'; +import BLE from '../../io/ble'; import Base64Util from '../../util/base64-util'; import MathUtil from '../../util/math-util'; import RateLimiter from '../../util/rateLimiter'; @@ -1282,7 +1282,7 @@ class Scratch3BoostBlocks { } /** - * @returns {object} metadata for this extension and its blocks. + * @returns metadata for this extension and its blocks. */ getInfo () { return { diff --git a/packages/vm/src/extensions/scratch3_ev3/index.js b/packages/vm/src/extensions/scratch3_ev3/index.js index 91b0e5c58..3045789dd 100644 --- a/packages/vm/src/extensions/scratch3_ev3/index.js +++ b/packages/vm/src/extensions/scratch3_ev3/index.js @@ -3,7 +3,7 @@ import BlockType from '../../extension-support/block-type'; import Cast from '../../util/cast'; import formatMessage from 'format-message'; import uid from '../../util/uid'; -import BT from '../../io/bt.js'; +import BT from '../../io/bt'; import Base64Util from '../../util/base64-util'; import MathUtil from '../../util/math-util'; import RateLimiter from '../../util/rateLimiter'; @@ -955,7 +955,7 @@ class Scratch3Ev3Blocks { /** * Define the EV3 extension. - * @returns {object} Extension description. + * @returns Extension description. */ getInfo () { return { diff --git a/packages/vm/src/extensions/scratch3_gdx_for/index.js b/packages/vm/src/extensions/scratch3_gdx_for/index.js index 672439678..99dd1929b 100644 --- a/packages/vm/src/extensions/scratch3_gdx_for/index.js +++ b/packages/vm/src/extensions/scratch3_gdx_for/index.js @@ -3,7 +3,7 @@ import BlockType from '../../extension-support/block-type'; import log from '../../util/log'; import formatMessage from 'format-message'; import MathUtil from '../../util/math-util'; -import BLE from '../../io/ble.js'; +import BLE from '../../io/ble'; import godirect from '@vernier/godirect/dist/godirect.min.umd.js'; import ScratchLinkDeviceAdapter from './scratch-link-device-adapter.js'; @@ -625,7 +625,7 @@ class Scratch3GdxForBlocks { } /** - * @returns {object} metadata for this extension and its blocks. + * @returns metadata for this extension and its blocks. */ getInfo () { return { diff --git a/packages/vm/src/extensions/scratch3_makeymakey/index.js b/packages/vm/src/extensions/scratch3_makeymakey/index.ts similarity index 82% rename from packages/vm/src/extensions/scratch3_makeymakey/index.js rename to packages/vm/src/extensions/scratch3_makeymakey/index.ts index 32d8d7c69..f81e3743f 100644 --- a/packages/vm/src/extensions/scratch3_makeymakey/index.js +++ b/packages/vm/src/extensions/scratch3_makeymakey/index.ts @@ -3,22 +3,23 @@ import ArgumentType from '../../extension-support/argument-type'; import BlockType from '../../extension-support/block-type'; import Cast from '../../util/cast'; +import type {ExtensionClass, ExtensionMetadata} from '../../extension-support/extension-metadata'; +import type Runtime from '../../engine/runtime'; +import type BlockUtility from '../../engine/block-utility'; + /** * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. - * @type {string} */ // eslint-disable-next-line max-len const blockIconURI = 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHN0eWxlPi5zdDJ7ZmlsbDpyZWR9LnN0M3tmaWxsOiNlMGUwZTB9LnN0NHtmaWxsOm5vbmU7c3Ryb2tlOiM2NjY7c3Ryb2tlLXdpZHRoOi41O3N0cm9rZS1taXRlcmxpbWl0OjEwfTwvc3R5bGU+PHBhdGggZD0iTTM1IDI4SDVhMSAxIDAgMCAxLTEtMVYxMmMwLS42LjQtMSAxLTFoMzBjLjUgMCAxIC40IDEgMXYxNWMwIC41LS41IDEtMSAxeiIgZmlsbD0iI2ZmZiIgaWQ9IkxheWVyXzYiLz48ZyBpZD0iTGF5ZXJfNCI+PHBhdGggY2xhc3M9InN0MiIgZD0iTTQgMjVoMzJ2Mi43SDR6TTEzIDI0aC0yLjJhMSAxIDAgMCAxLTEtMXYtOS43YzAtLjYuNC0xIDEtMUgxM2MuNiAwIDEgLjQgMSAxVjIzYzAgLjYtLjUgMS0xIDF6Ii8+PHBhdGggY2xhc3M9InN0MiIgZD0iTTYuMSAxOS4zdi0yLjJjMC0uNS40LTEgMS0xaDkuN2MuNSAwIDEgLjUgMSAxdjIuMmMwIC41LS41IDEtMSAxSDcuMWExIDEgMCAwIDEtMS0xeiIvPjxjaXJjbGUgY2xhc3M9InN0MiIgY3g9IjIyLjgiIGN5PSIxOC4yIiByPSIzLjQiLz48Y2lyY2xlIGNsYXNzPSJzdDIiIGN4PSIzMC42IiBjeT0iMTguMiIgcj0iMy40Ii8+PHBhdGggY2xhc3M9InN0MiIgZD0iTTQuMiAyN2gzMS45di43SDQuMnoiLz48L2c+PGcgaWQ9IkxheWVyXzUiPjxjaXJjbGUgY2xhc3M9InN0MyIgY3g9IjIyLjgiIGN5PSIxOC4yIiByPSIyLjMiLz48Y2lyY2xlIGNsYXNzPSJzdDMiIGN4PSIzMC42IiBjeT0iMTguMiIgcj0iMi4zIi8+PHBhdGggY2xhc3M9InN0MyIgZD0iTTEyLjUgMjIuOWgtMS4yYy0uMyAwLS41LS4yLS41LS41VjE0YzAtLjMuMi0uNS41LS41aDEuMmMuMyAwIC41LjIuNS41djguNGMwIC4zLS4yLjUtLjUuNXoiLz48cGF0aCBjbGFzcz0ic3QzIiBkPSJNNy4yIDE4Ljd2LTEuMmMwLS4zLjItLjUuNS0uNWg4LjRjLjMgMCAuNS4yLjUuNXYxLjJjMCAuMy0uMi41LS41LjVINy43Yy0uMyAwLS41LS4yLS41LS41ek00IDI2aDMydjJINHoiLz48L2c+PGcgaWQ9IkxheWVyXzMiPjxwYXRoIGNsYXNzPSJzdDQiIGQ9Ik0zNS4yIDI3LjlINC44YTEgMSAwIDAgMS0xLTFWMTIuMWMwLS42LjUtMSAxLTFoMzAuNWMuNSAwIDEgLjQgMSAxVjI3YTEgMSAwIDAgMS0xLjEuOXoiLz48cGF0aCBjbGFzcz0ic3Q0IiBkPSJNMzUuMiAyNy45SDQuOGExIDEgMCAwIDEtMS0xVjEyLjFjMC0uNi41LTEgMS0xaDMwLjVjLjUgMCAxIC40IDEgMVYyN2ExIDEgMCAwIDEtMS4xLjl6Ii8+PC9nPjwvc3ZnPg=='; /** * Length of the buffer to store key presses for the "when keys pressed in order" hat - * @type {number} */ const KEY_BUFFER_LENGTH = 100; /** * Timeout in milliseconds to reset the completed flag for a sequence. - * @type {number} */ const SEQUENCE_HAT_TIMEOUT = 100; @@ -49,7 +50,6 @@ const KEY_ID_DOWN = 'DOWN'; /** * Names used by keyboard io for keys used in scratch. - * @enum {string} */ const SCRATCH_KEY_NAME = { [KEY_ID_SPACE]: 'space', @@ -57,62 +57,69 @@ const SCRATCH_KEY_NAME = { [KEY_ID_UP]: 'up arrow', [KEY_ID_RIGHT]: 'right arrow', [KEY_ID_DOWN]: 'down arrow' -}; +} as const; + +interface SequenceObject { + array: string[]; + completed: boolean; +} + +interface WhenMakeyKeyPressedArgs { + KEY: unknown; +} + +interface WhenCodePressedArgs { + SEQUENCE: unknown; +} /** * Class for the makey makey blocks in Scratch 3.0 - * @class */ -class Scratch3MakeyMakeyBlocks { - constructor (runtime) { - /** - * The runtime instantiating this block package. - * @type {Runtime} - */ - this.runtime = runtime; +class Scratch3MakeyMakeyBlocks implements ExtensionClass { + /** + * A toggle that alternates true and false each frame, so that an + * edge-triggered hat can trigger on every other frame. + */ + frameToggle = false; + + /** + * An object containing a set of sequence objects. + * These are the key sequences currently being detected by the "when + * keys pressed in order" hat block. Each sequence is keyed by its + * string representation (the sequence's value in the menu, which is a + * string of KEY_IDs separated by spaces). Each sequence object + * has an array property (an array of KEY_IDs) and a boolean + * completed property that is true when the sequence has just been + * pressed. + */ + sequences: Record = {}; + /** + * An array of the key codes of recently pressed keys. + */ + keyPressBuffer: string[] = []; + + constructor ( /** - * A toggle that alternates true and false each frame, so that an - * edge-triggered hat can trigger on every other frame. - * @type {boolean} + * The runtime instantiating this block package. */ - this.frameToggle = false; - + public runtime: Runtime + ) { // Set an interval that toggles the frameToggle every frame. setInterval(() => { this.frameToggle = !this.frameToggle; - }, this.runtime.currentStepTime); + }, this.runtime.currentStepTime!); this.keyPressed = this.keyPressed.bind(this); this.runtime.on('KEY_PRESSED', this.keyPressed); this._clearkeyPressBuffer = this._clearkeyPressBuffer.bind(this); this.runtime.on('PROJECT_STOP_ALL', this._clearkeyPressBuffer); - - /** - * An object containing a set of sequence objects. - * These are the key sequences currently being detected by the "when - * keys pressed in order" hat block. Each sequence is keyed by its - * string representation (the sequence's value in the menu, which is a - * string of KEY_IDs separated by spaces). Each sequence object - * has an array property (an array of KEY_IDs) and a boolean - * completed property that is true when the sequence has just been - * pressed. - * @type {object} - */ - this.sequences = {}; - - /** - * An array of the key codes of recently pressed keys. - * @type {Array} - */ - this.keyPressBuffer = []; } /** * Localized short-form names of the space bar and arrow keys, for use in the * displayed menu items of the "when keys pressed in order" block. - * @type {object} */ get KEY_TEXT_SHORT () { return { @@ -147,7 +154,6 @@ class Scratch3MakeyMakeyBlocks { /** * An array of strings of KEY_IDs representing the default set of * key sequences for use by the "when keys pressed in order" block. - * @type {Array} */ get DEFAULT_SEQUENCES () { return [ @@ -159,13 +165,13 @@ class Scratch3MakeyMakeyBlocks { `${KEY_ID_DOWN} ${KEY_ID_UP}`, `${KEY_ID_UP} ${KEY_ID_RIGHT} ${KEY_ID_DOWN} ${KEY_ID_LEFT}`, `${KEY_ID_UP} ${KEY_ID_LEFT} ${KEY_ID_DOWN} ${KEY_ID_RIGHT}`, - `${KEY_ID_UP} ${KEY_ID_UP} ${KEY_ID_DOWN} ${KEY_ID_DOWN} ` + - `${KEY_ID_LEFT} ${KEY_ID_RIGHT} ${KEY_ID_LEFT} ${KEY_ID_RIGHT}` - ]; + // eslint-disable-next-line max-len + `${KEY_ID_UP} ${KEY_ID_UP} ${KEY_ID_DOWN} ${KEY_ID_DOWN} ${KEY_ID_LEFT} ${KEY_ID_RIGHT} ${KEY_ID_LEFT} ${KEY_ID_RIGHT}` + ] as const; } /** - * @returns {object} metadata for this extension and its blocks. + * @returns metadata for this extension and its blocks. */ getInfo () { return { @@ -263,15 +269,15 @@ class Scratch3MakeyMakeyBlocks { items: this.buildSequenceMenu(this.DEFAULT_SEQUENCES) } } - }; + } as ExtensionMetadata; } /** * Build the menu of key sequences. - * @param {Array} sequencesArray an array of strings of KEY_IDs. - * @returns {Array} an array of objects with text and value properties. + * @param sequencesArray an array of strings of KEY_IDs. + * @returns an array of objects with text and value properties. */ - buildSequenceMenu (sequencesArray) { + buildSequenceMenu (sequencesArray: typeof this.DEFAULT_SEQUENCES) { return sequencesArray.map( str => this.getMenuItemForSequenceString(str) ); @@ -279,12 +285,12 @@ class Scratch3MakeyMakeyBlocks { /** * Create a menu item for a sequence string. - * @param {string} sequenceString a string of KEY_IDs. - * @returns {object} an object with text and value properties. + * @param sequenceString a string of KEY_IDs. + * @returns an object with text and value properties. */ - getMenuItemForSequenceString (sequenceString) { + getMenuItemForSequenceString (sequenceString: string) { let sequenceArray = sequenceString.split(' '); - sequenceArray = sequenceArray.map(str => this.KEY_TEXT_SHORT[str]); + sequenceArray = sequenceArray.map(str => this.KEY_TEXT_SHORT[str as keyof typeof this.KEY_TEXT_SHORT]); return { text: sequenceArray.join(' '), value: sequenceString @@ -295,17 +301,16 @@ class Scratch3MakeyMakeyBlocks { * Check whether a keyboard key is currently pressed. * Also, toggle the results of the test on alternate frames, so that the * hat block fires repeatedly. - * @param {object} args - the block arguments. - * @property {number} KEY - a key code. - * @param {object} util - utility object provided by the runtime. - * @returns {boolean} true if the key is currently pressed, toggled on + * @param args - the block arguments. + * @param util - utility object provided by the runtime. + * @returns true if the key is currently pressed, toggled on */ - whenMakeyKeyPressed (args, util) { - let key = args.KEY; + whenMakeyKeyPressed (args: WhenMakeyKeyPressedArgs, util: BlockUtility): boolean { + let key = Cast.toString(args.KEY); // Convert the key arg, if it is a KEY_ID, to the key name used by // the Keyboard io module. - if (SCRATCH_KEY_NAME[args.KEY]) { - key = SCRATCH_KEY_NAME[args.KEY]; + if (SCRATCH_KEY_NAME[args.KEY as keyof typeof SCRATCH_KEY_NAME]) { + key = SCRATCH_KEY_NAME[args.KEY as keyof typeof SCRATCH_KEY_NAME]; } const isDown = util.ioQuery('keyboard', 'getKeyIsDown', [key]); return (isDown && this.frameToggle); @@ -314,9 +319,9 @@ class Scratch3MakeyMakeyBlocks { /** * A function called on the KEY_PRESSED event, to update the key press * buffer and check if any of the key sequences have been completed. - * @param {string} key A scratch key name. + * @param key A scratch key name. */ - keyPressed (key) { + keyPressed (key: string) { // Store only the first word of the Scratch key name, so that e.g. when // "left arrow" is pressed, we store "LEFT", which matches KEY_ID_LEFT key = key.split(' ')[0]; @@ -364,10 +369,10 @@ class Scratch3MakeyMakeyBlocks { /** * Add a key sequence to the set currently being checked on each key press. - * @param {string} sequenceString a string of space-separated KEY_IDs. - * @param {Array} sequenceArray an array of KEY_IDs. + * @param sequenceString a string of space-separated KEY_IDs. + * @param sequenceArray an array of KEY_IDs. */ - addSequence (sequenceString, sequenceArray) { + addSequence (sequenceString: string, sequenceArray: string[]) { // If we already have this sequence string, return. if (Object.prototype.hasOwnProperty.call(this.sequences, sequenceString)) { return; @@ -380,11 +385,10 @@ class Scratch3MakeyMakeyBlocks { /** * Check whether a key sequence was recently completed. - * @param {object} args The block arguments. - * @property {number} SEQUENCE A string of KEY_IDs. - * @returns {boolean} true if the sequence was recently completed. + * @param args The block arguments. + * @returns true if the sequence was recently completed. */ - whenCodePressed (args) { + whenCodePressed (args: WhenCodePressedArgs): boolean { const sequenceString = Cast.toString(args.SEQUENCE).toUpperCase(); const sequenceArray = sequenceString.split(' '); if (sequenceArray.length < 2) { @@ -395,4 +399,5 @@ class Scratch3MakeyMakeyBlocks { return this.sequences[sequenceString].completed; } } + export default Scratch3MakeyMakeyBlocks; diff --git a/packages/vm/src/extensions/scratch3_microbit/index.js b/packages/vm/src/extensions/scratch3_microbit/index.js index 42be2f6aa..369861a60 100644 --- a/packages/vm/src/extensions/scratch3_microbit/index.js +++ b/packages/vm/src/extensions/scratch3_microbit/index.js @@ -3,7 +3,7 @@ import BlockType from '../../extension-support/block-type'; import log from '../../util/log'; import cast from '../../util/cast'; import formatMessage from 'format-message'; -import BLE from '../../io/ble.js'; +import BLE from '../../io/ble'; import Base64Util from '../../util/base64-util'; /** @@ -581,7 +581,7 @@ class Scratch3MicroBitBlocks { } /** - * @returns {object} metadata for this extension and its blocks. + * @returns metadata for this extension and its blocks. */ getInfo () { return { diff --git a/packages/vm/src/extensions/scratch3_music/index.js b/packages/vm/src/extensions/scratch3_music/index.ts similarity index 81% rename from packages/vm/src/extensions/scratch3_music/index.js rename to packages/vm/src/extensions/scratch3_music/index.ts index a28e71bd0..ef5baef8e 100644 --- a/packages/vm/src/extensions/scratch3_music/index.js +++ b/packages/vm/src/extensions/scratch3_music/index.ts @@ -6,13 +6,19 @@ import formatMessage from 'format-message'; import MathUtil from '../../util/math-util'; import Timer from '../../util/timer'; +import type {ExtensionClass, ExtensionMetadata} from '../../extension-support/extension-metadata'; +import type Runtime from '../../engine/runtime'; +import type BlockUtility from '../../engine/block-utility'; +import type Target from '../../engine/target'; +import type SoundPlayer from '../../../../audio/dist/types/SoundPlayer'; +import RenderedTarget from '../../sprites/rendered-target'; + /** * The instrument and drum sounds, loaded as static assets. - * @type {object} */ -let assetData = {}; +let assetData: Record = {}; try { - // eslint-disable-next-line global-require + // eslint-disable-next-line global-require, @typescript-eslint/no-require-imports assetData = require('./manifest'); } catch { // Non-webpack environment, don't worry about assets. @@ -20,67 +26,111 @@ try { /** * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. - * @type {string} */ // eslint-disable-next-line max-len const blockIconURI = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxuczp4bGluaz0iaHR0cDovL3d3dy53My5vcmcvMTk5OS94bGluayI+PHRpdGxlPm11c2ljLWJsb2NrLWljb248L3RpdGxlPjxkZWZzPjxwYXRoIGQ9Ik0zMi4xOCAyNS44NzRDMzIuNjM2IDI4LjE1NyAzMC41MTIgMzAgMjcuNDMzIDMwYy0zLjA3IDAtNS45MjMtMS44NDMtNi4zNzItNC4xMjYtLjQ1OC0yLjI4NSAxLjY2NS00LjEzNiA0Ljc0My00LjEzNi42NDcgMCAxLjI4My4wODQgMS44OS4yMzQuMzM4LjA4Ni42MzcuMTguOTM4LjMwMi44Ny0uMDItLjEwNC0yLjI5NC0xLjgzNS0xMi4yMy0yLjEzNC0xMi4zMDIgMy4wNi0xLjg3IDguNzY4LTIuNzUyIDUuNzA4LS44ODUuMDc2IDQuODItMy42NSAzLjg0NC0zLjcyNC0uOTg3LTQuNjUtNy4xNTMuMjYzIDE0LjczOHptLTE2Ljk5OCA1Ljk5QzE1LjYzIDM0LjE0OCAxMy41MDcgMzYgMTAuNDQgMzZjLTMuMDcgMC01LjkyMi0xLjg1Mi02LjM4LTQuMTM2LS40NDgtMi4yODQgMS42NzQtNC4xMzUgNC43NS00LjEzNSAxLjAwMyAwIDEuOTc1LjE5NiAyLjg1NS41NDMuODIyLS4wNTUtLjE1LTIuMzc3LTEuODYyLTEyLjIyOC0yLjEzMy0xMi4zMDMgMy4wNi0xLjg3IDguNzY0LTIuNzUzIDUuNzA2LS44OTQuMDc2IDQuODItMy42NDggMy44MzQtMy43MjQtLjk4Ny00LjY1LTcuMTUyLjI2MiAxNC43Mzh6IiBpZD0iYSIvPjwvZGVmcz48ZyBmaWxsPSJub25lIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiPjx1c2UgZmlsbD0iI0ZGRiIgeGxpbms6aHJlZj0iI2EiLz48cGF0aCBzdHJva2Utb3BhY2l0eT0iLjEiIHN0cm9rZT0iIzAwMCIgZD0iTTI4LjQ1NiAyMS42NzVjLS4wMS0uMzEyLS4wODctLjgyNS0uMjU2LTEuNzAyLS4wOTYtLjQ5NS0uNjEyLTMuMDIyLS43NTMtMy43My0uMzk1LTEuOTgtLjc2LTMuOTItMS4xNDItNi4xMTMtLjczMi00LjIyMy0uNjkzLTYuMDUuMzQ0LTYuNTI3LjUtLjIzIDEuMDYtLjA4IDEuODQuMzUuNDE0LjIyNyAyLjE4MiAxLjM2NSAyLjA3IDEuMjk2IDEuOTk0IDEuMjQyIDMuNDY0IDEuNzc0IDQuOTMgMS41NDggMS41MjYtLjIzNyAyLjUwNC0uMDYgMi44NzYuNjE4LjM0OC42MzUuMDE1IDEuNDE2LS43MyAyLjE4LTEuNDcyIDEuNTE2LTMuOTc1IDIuNTE0LTUuODQ4IDIuMDIzLS44MjItLjIyLTEuMjM4LS40NjUtMi4zOC0xLjI2N2wtLjA5NS0uMDY2Yy4wNDcuNTkzLjI2NCAxLjc0LjcxNyAzLjgwMy4yOTQgMS4zMzYgMi4wOCA5LjE4NyAyLjYzNyAxMS42NzRsLjAwMi4wMTJjLjUyOCAyLjYzNy0xLjg3MyA0LjcyNC01LjIzNiA0LjcyNC0zLjI5IDAtNi4zNjMtMS45ODgtNi44NjItNC41MjgtLjUzLTIuNjQgMS44NzMtNC43MzQgNS4yMzMtNC43MzQuNjcyIDAgMS4zNDcuMDg1IDIuMDE0LjI1LjIyNy4wNTcuNDM2LjExOC42MzYuMTg3em0tMTYuOTk2IDUuOTljLS4wMS0uMzE4LS4wOS0uODM4LS4yNjYtMS43MzctLjA5LS40Ni0uNTk1LTIuOTM3LS43NTMtMy43MjctLjM5LTEuOTYtLjc1LTMuODktMS4xMy02LjA3LS43MzItNC4yMjMtLjY5Mi02LjA1LjM0NC02LjUyNi41MDItLjIzIDEuMDYtLjA4MiAxLjg0LjM1LjQxNS4yMjcgMi4xODIgMS4zNjQgMi4wNyAxLjI5NSAxLjk5MyAxLjI0MiAzLjQ2MiAxLjc3NCA0LjkyNiAxLjU0OCAxLjUyNS0uMjQgMi41MDQtLjA2NCAyLjg3Ni42MTQuMzQ4LjYzNS4wMTUgMS40MTUtLjcyOCAyLjE4LTEuNDc0IDEuNTE3LTMuOTc3IDIuNTEzLTUuODQ3IDIuMDE3LS44Mi0uMjItMS4yMzYtLjQ2NC0yLjM3OC0xLjI2N2wtLjA5NS0uMDY1Yy4wNDcuNTkzLjI2NCAxLjc0LjcxNyAzLjgwMi4yOTQgMS4zMzcgMi4wNzggOS4xOSAyLjYzNiAxMS42NzVsLjAwMy4wMTNjLjUxNyAyLjYzOC0xLjg4NCA0LjczMi01LjIzNCA0LjczMi0zLjI4NyAwLTYuMzYtMS45OTMtNi44Ny00LjU0LS41Mi0yLjY0IDEuODg0LTQuNzMgNS4yNC00LjczLjkwNSAwIDEuODAzLjE1IDIuNjUuNDM2eiIvPjwvZz48L3N2Zz4='; /** * Icon svg to be displayed in the category menu, encoded as a data URI. - * @type {string} */ // eslint-disable-next-line max-len const menuIconURI = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjAiIGhlaWdodD0iMjAiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggZD0iTTE2LjA5IDEyLjkzN2MuMjI4IDEuMTQxLS44MzMgMi4wNjMtMi4zNzMgMi4wNjMtMS41MzUgMC0yLjk2Mi0uOTIyLTMuMTg2LTIuMDYzLS4yMy0xLjE0Mi44MzMtMi4wNjggMi4zNzItMi4wNjguMzIzIDAgLjY0MS4wNDIuOTQ1LjExN2EzLjUgMy41IDAgMCAxIC40NjguMTUxYy40MzUtLjAxLS4wNTItMS4xNDctLjkxNy02LjExNC0xLjA2Ny02LjE1MiAxLjUzLS45MzUgNC4zODQtMS4zNzcgMi44NTQtLjQ0Mi4wMzggMi40MS0xLjgyNSAxLjkyMi0xLjg2Mi0uNDkzLTIuMzI1LTMuNTc3LjEzMiA3LjM3ek03LjQ2IDguNTYzYy0xLjg2Mi0uNDkzLTIuMzI1LTMuNTc2LjEzIDcuMzdDNy44MTYgMTcuMDczIDYuNzU0IDE4IDUuMjIgMThjLTEuNTM1IDAtMi45NjEtLjkyNi0zLjE5LTIuMDY4LS4yMjQtMS4xNDIuODM3LTIuMDY3IDIuMzc1LTIuMDY3LjUwMSAwIC45ODcuMDk4IDEuNDI3LjI3Mi40MTItLjAyOC0uMDc0LTEuMTg5LS45My02LjExNEMzLjgzNCAxLjg3IDYuNDMgNy4wODcgOS4yODIgNi42NDZjMi44NTQtLjQ0Ny4wMzggMi40MS0xLjgyMyAxLjkxN3oiIGZpbGw9IiM1NzVFNzUiIGZpbGwtcnVsZT0iZXZlbm9kZCIvPjwvc3ZnPg=='; +/** + * Music state associated with a particular target. + */ +interface MusicState { + /** The 0-indexed number of the currently selected instrument. */ + currentInstrument: number; +} + +/** + * Info about an instrument. + */ +interface InstrumentInfo { + /** The translatable name to display in the instruments menu. */ + name: string; + /** The name of the directory containing audio samples for this instrument. */ + dirName: string; + /** An optional duration for the release portion of each note. */ + releaseTime?: number; + /** An array of numbers representing the MIDI note number for each sampled sound used to play this instrument. */ + samples: number[]; +} + +/** + * Info about a drum. + */ +interface DrumInfo { + /** The translatable name to display in the drums menu. */ + name: string; + /** The name of the audio file for this drum sound. */ + fileName: string; +} + +interface PlayDrumForBeatsArgs { + DRUM: unknown; + BEATS: unknown; +} + +interface RestForBeatsArgs { + BEATS: unknown; +} + +interface PlayNoteForBeatsArgs { + NOTE: unknown; + BEATS: unknown; +} + +interface SetInstrumentArgs { + INSTRUMENT: unknown; +} + +interface SetTempoArgs { + TEMPO: unknown; +} + /** * Class for the music-related blocks in Scratch 3.0 - * @param {Runtime} runtime - the runtime instantiating this block package. - * @class */ -class Scratch3MusicBlocks { - constructor (runtime) { - /** - * The runtime instantiating this block package. - * @type {Runtime} - */ - this.runtime = runtime; +class Scratch3MusicBlocks implements ExtensionClass { + /** + * The runtime instantiating this block package. + */ + runtime: Runtime; - /** - * The number of drum and instrument sounds currently being played simultaneously. - * @type {number} - * @private - */ - this._concurrencyCounter = 0; + /** + * The number of drum and instrument sounds currently being played simultaneously. + */ + private _concurrencyCounter = 0; - /** - * An array of sound players, one for each drum sound. - * @type {Array} - * @private - */ - this._drumPlayers = []; + /** + * An array of sound players, one for each drum sound. + */ + private _drumPlayers: SoundPlayer[] = []; - /** - * An array of arrays of sound players. Each instrument has one or more audio players. - * @type {Array[]} - * @private - */ - this._instrumentPlayerArrays = []; + /** + * An array of arrays of sound players. Each instrument has one or more audio players. + */ + private _instrumentPlayerArrays: SoundPlayer[][] = []; - /** - * An array of arrays of sound players. Each instrument mya have an audio player for each playable note. - * @type {Array[]} - * @private - */ - this._instrumentPlayerNoteArrays = []; + /** + * An array of arrays of sound players. Each instrument may have an audio player for each playable note. + */ + private _instrumentPlayerNoteArrays: SoundPlayer[][] = []; + /** + * An array of audio bufferSourceNodes. Each time you play an instrument or drum sound, + * a bufferSourceNode is created. We keep references to them to make sure their onended + * events can fire. + */ + private _bufferSources: unknown[] = []; + + constructor (runtime: Runtime) { /** - * An array of audio bufferSourceNodes. Each time you play an instrument or drum sound, - * a bufferSourceNode is created. We keep references to them to make sure their onended - * events can fire. - * @type {Array} - * @private + * The runtime instantiating this block package. */ - this._bufferSources = []; + this.runtime = runtime; this._loadAllSounds(); @@ -95,7 +145,7 @@ class Scratch3MusicBlocks { * Decode the full set of drum and instrument sounds, and store the audio buffers in arrays. */ _loadAllSounds () { - const loadingPromises = []; + const loadingPromises: Promise[] = []; this.DRUM_INFO.forEach((drumInfo, index) => { const filePath = `drums/${drumInfo.fileName}`; const promise = this._storeSound(filePath, index, this._drumPlayers); @@ -117,12 +167,12 @@ class Scratch3MusicBlocks { /** * Decode a sound and store the player in an array. - * @param {string} filePath - the audio file name. - * @param {number} index - the index at which to store the audio player. - * @param {Array} playerArray - the array of players in which to store it. - * @returns {Promise} - a promise which will resolve once the sound has been stored. + * @param filePath - the audio file name. + * @param index - the index at which to store the audio player. + * @param playerArray - the array of players in which to store it. + * @returns a promise which will resolve once the sound has been stored. */ - _storeSound (filePath, index, playerArray) { + async _storeSound (filePath: string, index: number, playerArray: SoundPlayer[]) { const fullPath = `${filePath}.mp3`; if (!assetData[fullPath]) return; @@ -130,21 +180,20 @@ class Scratch3MusicBlocks { // The sound player has already been downloaded via the manifest file required above. const soundBuffer = assetData[fullPath]; - return this._decodeSound(soundBuffer).then(player => { - playerArray[index] = player; - }); + const player = await this._decodeSound(soundBuffer); + playerArray[index] = player; } /** * Decode a sound and return a promise with the audio buffer. - * @param {ArrayBuffer} soundBuffer - a buffer containing the encoded audio. - * @returns {Promise} - a promise which will resolve once the sound has decoded. + * @param soundBuffer - a buffer containing the encoded audio. + * @returns a promise which will resolve once the sound has decoded. */ - _decodeSound (soundBuffer) { + async _decodeSound (soundBuffer: ArrayBuffer) { const engine = this.runtime.audioEngine; if (!engine) { - return Promise.reject(new Error('No Audio Context Detected')); + throw new Error('No Audio Context Detected'); } // Check for newer promise-based API @@ -154,13 +203,12 @@ class Scratch3MusicBlocks { /** * Create data for a menu in scratch-blocks format, consisting of an array of objects with text and * value properties. The text is a translated string, and the value is one-indexed. - * @param {object[]} info - An array of info objects each having a name property. - * @returns {Array} - An array of objects with text and value properties. - * @private + * @param info - An array of info objects each having a name property. + * @returns An array of objects with text and value properties. */ - _buildMenu (info) { + private _buildMenu (info: Array<{name: string}>) { return info.map((entry, index) => { - const obj = {}; + const obj = {} as {text: string; value: string}; obj.text = entry.name; obj.value = String(index + 1); return obj; @@ -169,9 +217,8 @@ class Scratch3MusicBlocks { /** * An array of info about each drum. - * @returns The drum info array. */ - get DRUM_INFO () { + get DRUM_INFO (): DrumInfo[] { return [ { name: formatMessage({ @@ -320,20 +367,10 @@ class Scratch3MusicBlocks { ]; } - /** - * @typedef {object} InstrumentInfo - * @property {string} name - the translatable name to display in the instruments menu. - * @property {string} dirName - the name of the directory containing audio samples for this instrument. - * @property {number} [releaseTime] - an optional duration for the release portion of each note. - * @property {number[]} samples - an array of numbers representing the MIDI note number for each - * sampled sound used to play this instrument. - */ - /** * An array of info about each instrument. - * @returns {InstrumentInfo[]} the instrument info array. */ - get INSTRUMENT_INFO () { + get INSTRUMENT_INFO (): InstrumentInfo[] { return [ { name: formatMessage({ @@ -543,7 +580,6 @@ class Scratch3MusicBlocks { /** * An array that is a mapping from MIDI instrument numbers to Scratch instrument numbers. - * @type {number[]} */ get MIDI_INSTRUMENTS () { return [ @@ -611,14 +647,13 @@ class Scratch3MusicBlocks { 21, 21, 21, 21, // Telephone Ring, Helicopter, Applause, Gunshot 21, 21, 21, 21 - ]; + ] as const; } /** * An array that is a mapping from MIDI drum numbers in range (35..81) to Scratch drum numbers. * It's in the format [drumNum, pitch, decay]. * The pitch and decay properties are not currently being used. - * @type {Array[]} */ get MIDI_DRUMS () { return [ @@ -669,22 +704,20 @@ class Scratch3MusicBlocks { [17, 0], [11, -6, 1], [11, -6, 3] - ]; + ] as const; } /** * The key to load & store a target's music-related state. - * @type {string} */ static get STATE_KEY () { - return 'Scratch.music'; + return 'Scratch.music' as const; } /** * The default music-related state, to be used when a target has no existing music state. - * @type {MusicState} */ - static get DEFAULT_MUSIC_STATE () { + static get DEFAULT_MUSIC_STATE (): MusicState { return { currentInstrument: 0 }; @@ -692,7 +725,6 @@ class Scratch3MusicBlocks { /** * The minimum and maximum MIDI note numbers, for clamping the input to play note. - * @type {{min: number, max: number}} */ static get MIDI_NOTE_RANGE () { return {min: 0, max: 130}; @@ -701,7 +733,6 @@ class Scratch3MusicBlocks { /** * The minimum and maximum beat values, for clamping the duration of play note, play drum and rest. * 100 beats at the default tempo of 60bpm is 100 seconds. - * @type {{min: number, max: number}} */ static get BEAT_RANGE () { return {min: 0, max: 100}; @@ -709,7 +740,6 @@ class Scratch3MusicBlocks { /** * The minimum and maximum tempo values, in bpm. - * @type {{min: number, max: number}} */ static get TEMPO_RANGE () { return {min: 20, max: 500}; @@ -717,19 +747,18 @@ class Scratch3MusicBlocks { /** * The maximum number of sounds to allow to play simultaneously. - * @type {number} */ get CONCURRENCY_LIMIT () { return this.runtime.limitOptions.unlimitedSoundStuffs ? Infinity : 30; } /** - * @param {Target} target - collect music state for this target. - * @returns {MusicState} the mutable music state associated with that target. This will be created if necessary. + * @param target - collect music state for this target. + * @returns the mutable music state associated with that target. This will be created if necessary. * @private */ - _getMusicState (target) { - let musicState = target.getCustomState(Scratch3MusicBlocks.STATE_KEY); + _getMusicState (target: Target): MusicState { + let musicState = target.getCustomState(Scratch3MusicBlocks.STATE_KEY); if (!musicState) { musicState = Clone.simple(Scratch3MusicBlocks.DEFAULT_MUSIC_STATE); target.setCustomState(Scratch3MusicBlocks.STATE_KEY, musicState); @@ -739,14 +768,14 @@ class Scratch3MusicBlocks { /** * When a music-playing Target is cloned, clone the music state. - * @param {Target} newTarget - the newly created target. - * @param {Target} [sourceTarget] - the target used as a source for the new clone, if any. + * @param newTarget - the newly created target. + * @param sourceTarget - the target used as a source for the new clone, if any. * @listens Runtime#event:targetWasCreated * @private */ - _onTargetCreated (newTarget, sourceTarget) { + _onTargetCreated (newTarget: Target, sourceTarget?: Target) { if (sourceTarget) { - const musicState = sourceTarget.getCustomState(Scratch3MusicBlocks.STATE_KEY); + const musicState = sourceTarget.getCustomState(Scratch3MusicBlocks.STATE_KEY); if (musicState) { newTarget.setCustomState(Scratch3MusicBlocks.STATE_KEY, Clone.simple(musicState)); } @@ -754,7 +783,7 @@ class Scratch3MusicBlocks { } /** - * @returns {object} metadata for this extension and its blocks. + * @returns metadata for this extension and its blocks. */ getInfo () { return { @@ -924,17 +953,17 @@ class Scratch3MusicBlocks { items: this._buildMenu(this.INSTRUMENT_INFO) } } - }; + } as ExtensionMetadata; } /** * Play a drum sound for some number of beats. - * @param {object} args - the block arguments. - * @param {object} util - utility object provided by the runtime. - * @property {int} DRUM - the number of the drum to play. - * @property {number} BEATS - the duration in beats of the drum sound. + * @param args - the block arguments. + * @param util - utility object provided by the runtime. + * @property DRUM - the number of the drum to play. + * @property BEATS - the duration in beats of the drum sound. */ - playDrumForBeats (args, util) { + playDrumForBeats (args: PlayDrumForBeatsArgs, util: BlockUtility) { this._playDrumForBeats(args.DRUM, args.BEATS, util); } @@ -942,10 +971,10 @@ class Scratch3MusicBlocks { * Play a drum sound for some number of beats according to the range of "MIDI" drum codes supported. * This block is implemented for compatibility with old Scratch projects that use the * 'drum:duration:elapsed:from:' block. - * @param {object} args - the block arguments. - * @param {object} util - utility object provided by the runtime. + * @param args - the block arguments. + * @param util - utility object provided by the runtime. */ - midiPlayDrumForBeats (args, util) { + midiPlayDrumForBeats (args: PlayDrumForBeatsArgs, util: BlockUtility) { let drumNum = Cast.toNumber(args.DRUM); drumNum = Math.round(drumNum); const midiDescription = this.MIDI_DRUMS[drumNum - 35]; @@ -960,20 +989,20 @@ class Scratch3MusicBlocks { /** * Internal code to play a drum sound for some number of beats. - * @param {number} drumNum - the drum number. - * @param {beats} beats - the duration in beats to pause after playing the sound. - * @param {object} util - utility object provided by the runtime. + * @param drumNum - the drum number. + * @param beats - the duration in beats to pause after playing the sound. + * @param util - utility object provided by the runtime. */ - _playDrumForBeats (drumNum, beats, util) { + _playDrumForBeats (drumNum: unknown, beats: unknown, util: BlockUtility) { if (this._stackTimerNeedsInit(util)) { drumNum = Cast.toNumber(drumNum); - drumNum = Math.round(drumNum); - drumNum -= 1; // drums are one-indexed - drumNum = MathUtil.wrapClamp(drumNum, 0, this.DRUM_INFO.length - 1); + drumNum = Math.round(drumNum as number); + drumNum = (drumNum as number) - 1; // drums are one-indexed + drumNum = MathUtil.wrapClamp(drumNum as number, 0, this.DRUM_INFO.length - 1); beats = Cast.toNumber(beats); - beats = this._clampBeats(beats); - this._playDrumNum(util, drumNum); - this._startStackTimer(util, this._beatsToSec(beats)); + beats = this._clampBeats(beats as number); + this._playDrumNum(util, drumNum as number); + this._startStackTimer(util, this._beatsToSec(beats as number)); } else { this._checkStackTimer(util); } @@ -981,12 +1010,12 @@ class Scratch3MusicBlocks { /** * Play a drum sound using its 0-indexed number. - * @param {object} util - utility object provided by the runtime. - * @param {number} drumNum - the number of the drum to play. + * @param util - utility object provided by the runtime. + * @param drumNum - the number of the drum to play. * @private */ - _playDrumNum (util, drumNum) { - if (util.runtime.audioEngine === null) return; + _playDrumNum (util: BlockUtility, drumNum: number) { + if (util.runtime!.audioEngine === null) return; if (util.target.sprite.soundBank === null) return; // If we're playing too many sounds, do not play the drum sound. if (this._concurrencyCounter > this.CONCURRENCY_LIMIT) { @@ -1004,7 +1033,7 @@ class Scratch3MusicBlocks { player.take(); } - const engine = util.runtime.audioEngine; + const engine = util.runtime!.audioEngine!; const context = engine.audioContext; const volumeGain = context.createGain(); volumeGain.gain.setValueAtTime(util.target.volume / 100, engine.currentTime); @@ -1024,11 +1053,11 @@ class Scratch3MusicBlocks { /** * Rest for some number of beats. - * @param {object} args - the block arguments. - * @param {object} util - utility object provided by the runtime. - * @property {number} BEATS - the duration in beats of the rest. + * @param args - the block arguments. + * @param util - utility object provided by the runtime. + * @property BEATS - the duration in beats of the rest. */ - restForBeats (args, util) { + restForBeats (args: RestForBeatsArgs, util: BlockUtility) { if (this._stackTimerNeedsInit(util)) { let beats = Cast.toNumber(args.BEATS); beats = this._clampBeats(beats); @@ -1041,12 +1070,12 @@ class Scratch3MusicBlocks { /** * Play a note using the current musical instrument for some number of beats. * This function processes the arguments, and handles the timing of the block's execution. - * @param {object} args - the block arguments. - * @param {object} util - utility object provided by the runtime. - * @property {number} NOTE - the pitch of the note to play, interpreted as a MIDI note number. - * @property {number} BEATS - the duration in beats of the note. + * @param args - the block arguments. + * @param util - utility object provided by the runtime. + * @property NOTE - the pitch of the note to play, interpreted as a MIDI note number. + * @property BEATS - the duration in beats of the note. */ - playNoteForBeats (args, util) { + playNoteForBeats (args: PlayNoteForBeatsArgs, util: BlockUtility) { if (this._stackTimerNeedsInit(util)) { let note = Cast.toNumber(args.NOTE); note = this.runtime.limitOptions.unlimitedSoundStuffs ? @@ -1068,11 +1097,11 @@ class Scratch3MusicBlocks { } } - _playNoteForPicker (noteNum, category) { + _playNoteForPicker (noteNum: number, category: string) { if (category !== this.getInfo().name) return; const util = { runtime: this.runtime, - target: this.runtime.getEditingTarget() + target: this.runtime.getEditingTarget()! }; this._playNote(util, noteNum, 0.25); } @@ -1081,13 +1110,15 @@ class Scratch3MusicBlocks { * Play a note using the current instrument for a duration in seconds. * This function actually plays the sound, and handles the timing of the sound, including the * "release" portion of the sound, which continues briefly after the block execution has finished. - * @param {object} util - utility object provided by the runtime. - * @param {number} note - the pitch of the note to play, interpreted as a MIDI note number. - * @param {number} durationSec - the duration in seconds to play the note. + * @param util - utility object provided by the runtime. + * @param util.runtime - the Scratch runtime, used to access the audio engine. + * @param util.target - the target on which the block is executing. + * @param note - the pitch of the note to play, interpreted as a MIDI note number. + * @param durationSec - the duration in seconds to play the note. * @private */ - _playNote (util, note, durationSec) { - if (util.runtime.audioEngine === null) return; + _playNote (util: {runtime?: Runtime, target: RenderedTarget}, note: number, durationSec: number) { + if (util.runtime!.audioEngine === null) return; if (util.target.sprite.soundBank === null) return; // If we're playing too many sounds, do not play the note. @@ -1107,7 +1138,7 @@ class Scratch3MusicBlocks { if (typeof this._instrumentPlayerArrays[inst][sampleIndex] === 'undefined') return; // Fetch the sound player to play the note. - const engine = util.runtime.audioEngine; + const engine = util.runtime!.audioEngine!; if (!this._instrumentPlayerNoteArrays[inst][note]) { this._instrumentPlayerNoteArrays[inst][note] = this._instrumentPlayerArrays[inst][sampleIndex].take(); @@ -1167,12 +1198,12 @@ class Scratch3MusicBlocks { * The samples array for each instrument is the set of pitches of the available audio samples. * This function selects the best one to use to play a given input note, and returns its index * in the samples array. - * @param {number} note - the input note to select a sample for. - * @param {number[]} samples - an array of the pitches of the available samples. - * @returns {index} the index of the selected sample in the samples array. + * @param note - the input note to select a sample for. + * @param samples - an array of the pitches of the available samples. + * @returns the index of the selected sample in the samples array. * @private */ - _selectSampleIndexForNote (note, samples) { + _selectSampleIndexForNote (note: number, samples: number[]): number { // Step backwards through the array of samples, i.e. in descending pitch, in order to find // the sample that is the closest one below (or matching) the pitch of the input note. for (let i = samples.length - 1; i >= 0; i--) { @@ -1184,53 +1215,53 @@ class Scratch3MusicBlocks { } /** - * Calcuate the frequency ratio for a given musical interval. - * @param {number} interval - the pitch interval to convert. - * @returns {number} a ratio corresponding to the input interval. + * Calculate the frequency ratio for a given musical interval. + * @param interval - the pitch interval to convert. + * @returns a ratio corresponding to the input interval. * @private */ - _ratioForPitchInterval (interval) { + _ratioForPitchInterval (interval: number): number { return Math.pow(2, (interval / 12)); } /** * Clamp a duration in beats to the allowed min and max duration. - * @param {number} beats - a duration in beats. - * @returns {number} - the clamped duration. + * @param beats - a duration in beats. + * @returns the clamped duration. * @private */ - _clampBeats (beats) { + _clampBeats (beats: number): number { return this.runtime.limitOptions.unlimitedSoundStuffs ? beats : MathUtil.clamp(beats, Scratch3MusicBlocks.BEAT_RANGE.min, Scratch3MusicBlocks.BEAT_RANGE.max); } /** * Convert a number of beats to a number of seconds, using the current tempo. - * @param {number} beats - number of beats to convert to secs. - * @returns {number} seconds - number of seconds `beats` will last. + * @param beats - number of beats to convert to secs. + * @returns seconds - number of seconds `beats` will last. * @private */ - _beatsToSec (beats) { + _beatsToSec (beats: number): number { return (60 / this.getTempo()) * beats; } /** * Check if the stack timer needs initialization. - * @param {object} util - utility object provided by the runtime. - * @returns {boolean} - true if the stack timer needs to be initialized. + * @param util - utility object provided by the runtime. + * @returns true if the stack timer needs to be initialized. * @private */ - _stackTimerNeedsInit (util) { + _stackTimerNeedsInit (util: BlockUtility): boolean { return !util.stackFrame.timer; } /** * Start the stack timer and the yield the thread if necessary. - * @param {object} util - utility object provided by the runtime. - * @param {number} duration - a duration in seconds to set the timer for. + * @param util - utility object provided by the runtime. + * @param duration - a duration in seconds to set the timer for. * @private */ - _startStackTimer (util, duration) { + _startStackTimer (util: BlockUtility, duration: number) { util.stackFrame.timer = new Timer(); util.stackFrame.timer.start(); util.stackFrame.duration = duration; @@ -1239,72 +1270,72 @@ class Scratch3MusicBlocks { /** * Check the stack timer, and if its time is not up yet, yield the thread. - * @param {object} util - utility object provided by the runtime. + * @param util - utility object provided by the runtime. * @private */ - _checkStackTimer (util) { - const timeElapsed = util.stackFrame.timer.timeElapsed(); - if (timeElapsed < util.stackFrame.duration * 1000) { + _checkStackTimer (util: BlockUtility) { + const timeElapsed = util.stackFrame.timer!.timeElapsed(); + if (timeElapsed < util.stackFrame.duration! * 1000) { util.yield(); } } /** * Select an instrument for playing notes. - * @param {object} args - the block arguments. - * @param {object} util - utility object provided by the runtime. - * @property {int} INSTRUMENT - the number of the instrument to select. + * @param args - the block arguments. + * @param util - utility object provided by the runtime. + * @property INSTRUMENT - the number of the instrument to select. */ - setInstrument (args, util) { + setInstrument (args: SetInstrumentArgs, util: BlockUtility) { this._setInstrument(args.INSTRUMENT, util, false); } /** * Select an instrument for playing notes according to a mapping of MIDI codes to Scratch instrument numbers. * This block is implemented for compatibility with old Scratch projects that use the 'midiInstrument:' block. - * @param {object} args - the block arguments. - * @param {object} util - utility object provided by the runtime. - * @property {int} INSTRUMENT - the MIDI number of the instrument to select. + * @param args - the block arguments. + * @param util - utility object provided by the runtime. + * @property INSTRUMENT - the MIDI number of the instrument to select. */ - midiSetInstrument (args, util) { + midiSetInstrument (args: SetInstrumentArgs, util: BlockUtility) { this._setInstrument(args.INSTRUMENT, util, true); } /** * Internal code to select an instrument for playing notes. If mapMidi is true, set the instrument according to * the MIDI to Scratch instrument mapping. - * @param {number} instNum - the instrument number. - * @param {object} util - utility object provided by the runtime. - * @param {boolean} mapMidi - whether or not instNum is a MIDI instrument number. + * @param instNum - the instrument number. + * @param util - utility object provided by the runtime. + * @param mapMidi - whether or not instNum is a MIDI instrument number. */ - _setInstrument (instNum, util, mapMidi) { + _setInstrument (instNum: unknown, util: BlockUtility, mapMidi: boolean) { const musicState = this._getMusicState(util.target); instNum = Cast.toNumber(instNum); - instNum = Math.round(instNum); - instNum -= 1; // instruments are one-indexed + instNum = Math.round(instNum as number); + instNum = (instNum as number) - 1; // instruments are one-indexed if (mapMidi) { - instNum = (this.MIDI_INSTRUMENTS[instNum] || 0) - 1; + instNum = (this.MIDI_INSTRUMENTS[instNum as number] || 0) - 1; } - instNum = MathUtil.wrapClamp(instNum, 0, this.INSTRUMENT_INFO.length - 1); - musicState.currentInstrument = instNum; + instNum = MathUtil.wrapClamp(instNum as number, 0, this.INSTRUMENT_INFO.length - 1); + musicState.currentInstrument = instNum as number; } /** * Set the current tempo to a new value. - * @param {object} args - the block arguments. - * @property {number} TEMPO - the tempo, in beats per minute. + * @param args - the block arguments. + * @property TEMPO - the tempo, in beats per minute. */ - setTempo (args) { + setTempo (args: SetTempoArgs) { const tempo = Cast.toNumber(args.TEMPO); this._updateTempo(tempo); } /** * Change the current tempo by some amount. - * @param {object} args - the block arguments. - * @property {number} TEMPO - the amount to change the tempo, in beats per minute. + * @param args - the block arguments. + * @property TEMPO - the amount to change the tempo, in beats per minute. */ - changeTempo (args) { + changeTempo (args: SetTempoArgs) { const change = Cast.toNumber(args.TEMPO); const tempo = change + this.getTempo(); this._updateTempo(tempo); @@ -1312,10 +1343,10 @@ class Scratch3MusicBlocks { /** * Update the current tempo, clamping it to the min and max allowable range. - * @param {number} tempo - the tempo to set, in beats per minute. + * @param tempo - the tempo to set, in beats per minute. * @private */ - _updateTempo (tempo) { + _updateTempo (tempo: number) { tempo = this.runtime.limitOptions.unlimitedSoundStuffs ? tempo : MathUtil.clamp(tempo, Scratch3MusicBlocks.TEMPO_RANGE.min, Scratch3MusicBlocks.TEMPO_RANGE.max); const stage = this.runtime.getTargetForStage(); @@ -1326,7 +1357,7 @@ class Scratch3MusicBlocks { /** * Get the current tempo. - * @returns {number} - the current tempo, in beats per minute. + * @returns the current tempo, in beats per minute. */ getTempo () { const stage = this.runtime.getTargetForStage(); diff --git a/packages/vm/src/extensions/scratch3_pen/index.js b/packages/vm/src/extensions/scratch3_pen/index.ts similarity index 74% rename from packages/vm/src/extensions/scratch3_pen/index.js rename to packages/vm/src/extensions/scratch3_pen/index.ts index a30bb867b..147f4d7db 100644 --- a/packages/vm/src/extensions/scratch3_pen/index.js +++ b/packages/vm/src/extensions/scratch3_pen/index.ts @@ -6,64 +6,97 @@ import Clone from '../../util/clone'; import Color from '../../util/color'; import formatMessage from 'format-message'; import MathUtil from '../../util/math-util'; -import RenderedTarget from '../../sprites/rendered-target.js'; +import RenderedTarget from '../../sprites/rendered-target'; import log from '../../util/log'; import StageLayering from '../../engine/stage-layering'; +import type {ExtensionClass, ExtensionMetadata} from '../../extension-support/extension-metadata'; +import type Runtime from '../../engine/runtime'; +import type BlockUtility from '../../engine/block-utility'; +import type Target from '../../engine/target'; + /** * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. - * @type {string} */ // eslint-disable-next-line max-len const blockIconURI = 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48dGl0bGU+cGVuLWljb248L3RpdGxlPjxnIHN0cm9rZT0iIzU3NUU3NSIgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGQ9Ik04Ljc1MyAzNC42MDJsLTQuMjUgMS43OCAxLjc4My00LjIzN2MxLjIxOC0yLjg5MiAyLjkwNy01LjQyMyA1LjAzLTcuNTM4TDMxLjA2NiA0LjkzYy44NDYtLjg0MiAyLjY1LS40MSA0LjAzMi45NjcgMS4zOCAxLjM3NSAxLjgxNiAzLjE3My45NyA0LjAxNUwxNi4zMTggMjkuNTljLTIuMTIzIDIuMTE2LTQuNjY0IDMuOC03LjU2NSA1LjAxMiIgZmlsbD0iI0ZGRiIvPjxwYXRoIGQ9Ik0yOS40MSA2LjExcy00LjQ1LTIuMzc4LTguMjAyIDUuNzcyYy0xLjczNCAzLjc2Ni00LjM1IDEuNTQ2LTQuMzUgMS41NDYiLz48cGF0aCBkPSJNMzYuNDIgOC44MjVjMCAuNDYzLS4xNC44NzMtLjQzMiAxLjE2NGwtOS4zMzUgOS4zYy4yODItLjI5LjQxLS42NjguNDEtMS4xMiAwLS44NzQtLjUwNy0xLjk2My0xLjQwNi0yLjg2OC0xLjM2Mi0xLjM1OC0zLjE0Ny0xLjgtNC4wMDItLjk5TDMwLjk5IDUuMDFjLjg0NC0uODQgMi42NS0uNDEgNC4wMzUuOTYuODk4LjkwNCAxLjM5NiAxLjk4MiAxLjM5NiAyLjg1NU0xMC41MTUgMzMuNzc0Yy0uNTczLjMwMi0xLjE1Ny41Ny0xLjc2NC44M0w0LjUgMzYuMzgybDEuNzg2LTQuMjM1Yy4yNTgtLjYwNC41My0xLjE4Ni44MzMtMS43NTcuNjkuMTgzIDEuNDQ4LjYyNSAyLjEwOCAxLjI4Mi42Ni42NTggMS4xMDIgMS40MTIgMS4yODcgMi4xMDIiIGZpbGw9IiM0Qzk3RkYiLz48cGF0aCBkPSJNMzYuNDk4IDguNzQ4YzAgLjQ2NC0uMTQuODc0LS40MzMgMS4xNjVsLTE5Ljc0MiAxOS42OGMtMi4xMyAyLjExLTQuNjczIDMuNzkzLTcuNTcyIDUuMDFMNC41IDM2LjM4bC45NzQtMi4zMTYgMS45MjUtLjgwOGMyLjg5OC0xLjIxOCA1LjQ0LTIuOSA3LjU3LTUuMDFsMTkuNzQzLTE5LjY4Yy4yOTItLjI5Mi40MzItLjcwMi40MzItMS4xNjUgMC0uNjQ2LS4yNy0xLjQtLjc4LTIuMTIyLjI1LjE3Mi41LjM3Ny43MzcuNjE0Ljg5OC45MDUgMS4zOTYgMS45ODMgMS4zOTYgMi44NTYiIGZpbGw9IiM1NzVFNzUiIG9wYWNpdHk9Ii4xNSIvPjxwYXRoIGQ9Ik0xOC40NSAxMi44M2MwIC41LS40MDQuOTA1LS45MDQuOTA1cy0uOTA1LS40MDUtLjkwNS0uOTA0YzAtLjUuNDA3LS45MDMuOTA2LS45MDMuNSAwIC45MDQuNDA0LjkwNC45MDR6IiBmaWxsPSIjNTc1RTc1Ii8+PC9nPjwvc3ZnPg=='; /** * Enum for pen color parameter values. - * @readonly - * @enum {string} */ -const ColorParam = { - COLOR: 'color', - SATURATION: 'saturation', - BRIGHTNESS: 'brightness', - TRANSPARENCY: 'transparency' +const enum ColorParam { + COLOR = 'color', + SATURATION = 'saturation', + BRIGHTNESS = 'brightness', + TRANSPARENCY = 'transparency' }; /** - * @typedef {object} PenState - the pen state associated with a particular target. - * @property {boolean} penDown - tracks whether the pen should draw for this target. - * @property {number} color - the current color (hue) of the pen. - * @property {PenAttributes} penAttributes - cached pen attributes for the renderer. This is the authoritative value for - * diameter but not for pen color. + * The pen state associated with a particular target. */ +interface PenState { + /** Tracks whether the pen should draw for this target. */ + penDown: boolean; + /** The current color (hue) of the pen. */ + color: number; + saturation: number; + brightness: number; + transparency: number; + /** Used only for legacy `change shade by` blocks */ + _shade: number; + /** + * Cached pen attributes for the renderer. This is the authoritative value for + * diameter but not for pen color. + */ + penAttributes: { + color4f: [number, number, number, number]; + diameter: number; + }; +} + +interface SetPenColorToColorArgs { + COLOR: unknown; +} + +interface ChangePenColorParamByArgs { + /** the name of the selected color parameter. */ + COLOR_PARAM: ColorParam; + /** the amount to set the selected parameter to. */ + VALUE: unknown; +} + +interface ChangePenSizeByArgs { + SIZE: unknown; +} + +interface SetPenHueToNumberArgs { + HUE: unknown; +} + +interface SetPenShadeToNumberArgs { + SHADE: unknown; +} /** * Host for the Pen-related blocks in Scratch 3.0 - * @param {Runtime} runtime - the runtime instantiating this block package. - * @class */ -class Scratch3PenBlocks { - constructor (runtime) { - /** - * The runtime instantiating this block package. - * @type {Runtime} - */ - this.runtime = runtime; +class Scratch3PenBlocks implements ExtensionClass { + /** + * The ID of the renderer Drawable corresponding to the pen layer. + */ + private _penDrawableId = -1; - /** - * The ID of the renderer Drawable corresponding to the pen layer. - * @type {int} - * @private - */ - this._penDrawableId = -1; + /** + * The ID of the renderer Skin corresponding to the pen layer. + */ + private _penSkinId = -1; + constructor ( /** - * The ID of the renderer Skin corresponding to the pen layer. - * @type {int} - * @private + * The runtime instantiating this block package. */ - this._penSkinId = -1; - + public runtime: Runtime + ) { this._onTargetCreated = this._onTargetCreated.bind(this); this._onTargetMoved = this._onTargetMoved.bind(this); @@ -73,9 +106,8 @@ class Scratch3PenBlocks { /** * The default pen state, to be used when a target has no existing pen state. - * @type {PenState} */ - static get DEFAULT_PEN_STATE () { + static get DEFAULT_PEN_STATE (): PenState { return { penDown: false, color: 66.66, @@ -95,7 +127,6 @@ class Scratch3PenBlocks { * The minimum and maximum allowed pen size. * The maximum is twice the diagonal of the stage, so that even an * off-stage sprite can fill it. - * @type {{min: number, max: number}} */ static get PEN_SIZE_RANGE () { return {min: 1, max: 1200}; @@ -103,19 +134,18 @@ class Scratch3PenBlocks { /** * The key to load & store a target's pen-related state. - * @type {string} */ static get STATE_KEY () { - return 'Scratch.pen'; + return 'Scratch.pen' as const; } /** * Clamp a pen size value to the range allowed by the pen. - * @param {number} requestedSize - the requested pen size. - * @returns {number} the clamped size. + * @param requestedSize - the requested pen size. + * @returns the clamped size. * @private */ - _clampPenSize (requestedSize) { + private _clampPenSize (requestedSize: number): number { return this.runtime.limitOptions.unlimitedPenSize ? requestedSize : MathUtil.clamp( requestedSize, @@ -127,10 +157,10 @@ class Scratch3PenBlocks { /** * Retrieve the ID of the renderer "Skin" corresponding to the pen layer. If * the pen Skin doesn't yet exist, create it. - * @returns {int} the Skin ID of the pen layer, or -1 on failure. + * @returns the Skin ID of the pen layer, or -1 on failure. * @private */ - _getPenLayerID () { + private _getPenLayerID (): number { if (this._penSkinId < 0 && this.runtime.renderer) { this._penSkinId = this.runtime.renderer.createPenSkin(); this._penDrawableId = this.runtime.renderer.createDrawable(StageLayering.PEN_LAYER); @@ -140,12 +170,13 @@ class Scratch3PenBlocks { } /** - * @param {Target} target - collect pen state for this target. Probably, but not necessarily, a RenderedTarget. - * @returns {PenState} the mutable pen state associated with that target. This will be created if necessary. + * Retrieve the pen state for a particular target, creating it if it doesn't already exist. + * @param target - collect pen state for this target. Probably, but not necessarily, a RenderedTarget. + * @returns the mutable pen state associated with that target. This will be created if necessary. * @private */ - _getPenState (target) { - let penState = target.getCustomState(Scratch3PenBlocks.STATE_KEY); + private _getPenState (target: Target): PenState { + let penState = target.getCustomState(Scratch3PenBlocks.STATE_KEY); if (!penState) { penState = Clone.simple(Scratch3PenBlocks.DEFAULT_PEN_STATE); target.setCustomState(Scratch3PenBlocks.STATE_KEY, penState); @@ -155,14 +186,14 @@ class Scratch3PenBlocks { /** * When a pen-using Target is cloned, clone the pen state. - * @param {Target} newTarget - the newly created target. - * @param {Target} [sourceTarget] - the target used as a source for the new clone, if any. + * @param newTarget - the newly created target. + * @param sourceTarget - the target used as a source for the new clone, if any. * @listens Runtime#event:targetWasCreated * @private */ - _onTargetCreated (newTarget, sourceTarget) { + private _onTargetCreated (newTarget: Target, sourceTarget?: Target) { if (sourceTarget) { - const penState = sourceTarget.getCustomState(Scratch3PenBlocks.STATE_KEY); + const penState = sourceTarget.getCustomState(Scratch3PenBlocks.STATE_KEY); if (penState) { newTarget.setCustomState(Scratch3PenBlocks.STATE_KEY, Clone.simple(penState)); if (penState.penDown) { @@ -174,19 +205,19 @@ class Scratch3PenBlocks { /** * Handle a target which has moved. This only fires when the pen is down. - * @param {RenderedTarget} target - the target which has moved. - * @param {number} oldX - the previous X position. - * @param {number} oldY - the previous Y position. - * @param {boolean} isForce - whether the movement was forced. + * @param target - the target which has moved. + * @param oldX - the previous X position. + * @param oldY - the previous Y position. + * @param isForce - whether the movement was forced. * @private */ - _onTargetMoved (target, oldX, oldY, isForce) { + private _onTargetMoved (target: RenderedTarget, oldX: number, oldY: number, isForce?: boolean) { // Only move the pen if the movement isn't forced (ie. dragged). if (!isForce) { const penSkinId = this._getPenLayerID(); if (penSkinId >= 0) { const penState = this._getPenState(target); - this.runtime.renderer.penLine(penSkinId, penState.penAttributes, oldX, oldY, target.x, target.y); + this.runtime.renderer!.penLine(penSkinId, penState.penAttributes, oldX, oldY, target.x, target.y); this.runtime.requestRedraw(); } } @@ -194,20 +225,20 @@ class Scratch3PenBlocks { /** * Wrap a color input into the range (0,100). - * @param {number} value - the value to be wrapped. - * @returns {number} the wrapped value. + * @param value - the value to be wrapped. + * @returns the wrapped value. * @private */ - _wrapColor (value) { + private _wrapColor (value: number): number { return MathUtil.wrapClamp(value, 0, 100); } /** * Initialize color parameters menu with localized strings - * @returns {Array} of the localized text and values for each menu element + * @returns of the localized text and values for each menu element * @private */ - _initColorParam () { + private _initColorParam () { return [ { text: formatMessage({ @@ -247,11 +278,11 @@ class Scratch3PenBlocks { /** * Clamp a pen color parameter to the range (0,100). - * @param {number} value - the value to be clamped. - * @returns {number} the clamped value. + * @param value - the value to be clamped. + * @returns the clamped value. * @private */ - _clampColorParam (value) { + private _clampColorParam (value: number): number { return MathUtil.clamp(value, 0, 100); } @@ -259,11 +290,11 @@ class Scratch3PenBlocks { * Convert an alpha value to a pen transparency value. * Alpha ranges from 0 to 1, where 0 is transparent and 1 is opaque. * Transparency ranges from 0 to 100, where 0 is opaque and 100 is transparent. - * @param {number} alpha - the input alpha value. - * @returns {number} the transparency value. + * @param alpha - the input alpha value. + * @returns the transparency value. * @private */ - _alphaToTransparency (alpha) { + private _alphaToTransparency (alpha: number): number { return (1.0 - alpha) * 100.0; } @@ -271,16 +302,17 @@ class Scratch3PenBlocks { * Convert a pen transparency value to an alpha value. * Alpha ranges from 0 to 1, where 0 is transparent and 1 is opaque. * Transparency ranges from 0 to 100, where 0 is opaque and 100 is transparent. - * @param {number} transparency - the input transparency value. - * @returns {number} the alpha value. + * @param transparency - the input transparency value. + * @returns the alpha value. * @private */ - _transparencyToAlpha (transparency) { + private _transparencyToAlpha (transparency: number): number { return 1.0 - (transparency / 100.0); } /** - * @returns {object} metadata for this extension and its blocks. + * Get the metadata for this extension and its blocks. + * @returns metadata for this extension and its blocks. */ getInfo () { return { @@ -492,7 +524,7 @@ class Scratch3PenBlocks { items: this._initColorParam() } } - }; + } as ExtensionMetadata; } /** @@ -501,31 +533,31 @@ class Scratch3PenBlocks { clear () { const penSkinId = this._getPenLayerID(); if (penSkinId >= 0) { - this.runtime.renderer.penClear(penSkinId); + this.runtime.renderer!.penClear(penSkinId); this.runtime.requestRedraw(); } } /** * The pen "stamp" block stamps the current drawable's image onto the pen layer. - * @param {object} args - the block arguments. - * @param {object} util - utility object provided by the runtime. + * @param args The unused block arguments. + * @param util A utility object containing information about the block's context, including the target. */ - stamp (args, util) { + stamp (args: unknown, util: BlockUtility) { const penSkinId = this._getPenLayerID(); if (penSkinId >= 0) { const target = util.target; - this.runtime.renderer.penStamp(penSkinId, target.drawableID); + this.runtime.renderer!.penStamp(penSkinId, target.drawableID!); this.runtime.requestRedraw(); } } /** * The pen "pen down" block causes the target to leave pen trails on future motion. - * @param {object} args - the block arguments. - * @param {object} util - utility object provided by the runtime. + * @param args The unused block arguments. + * @param util A utility object containing information about the block's context, including the target. */ - penDown (args, util) { + penDown (args: unknown, util: BlockUtility) { const target = util.target; const penState = this._getPenState(target); @@ -536,17 +568,17 @@ class Scratch3PenBlocks { const penSkinId = this._getPenLayerID(); if (penSkinId >= 0) { - this.runtime.renderer.penPoint(penSkinId, penState.penAttributes, target.x, target.y); + this.runtime.renderer!.penPoint(penSkinId, penState.penAttributes, target.x, target.y); this.runtime.requestRedraw(); } } /** * The pen "pen up" block stops the target from leaving pen trails. - * @param {object} args - the block arguments. - * @param {object} util - utility object provided by the runtime. + * @param args The unused block arguments. + * @param util A utility object containing information about the block's context, including the target. */ - penUp (args, util) { + penUp (args: unknown, util: BlockUtility) { const target = util.target; const penState = this._getPenState(target); @@ -559,11 +591,10 @@ class Scratch3PenBlocks { /** * The pen "set pen color to {color}" block sets the pen to a particular RGB color. * The transparency is reset to 0. - * @param {object} args - the block arguments. - * @property {int} COLOR - the color to set, expressed as a 24-bit RGB value (0xRRGGBB). - * @param {object} util - utility object provided by the runtime. + * @param args The block arguments, containing the color to set the pen to. + * @param util A utility object containing information about the block's context, including the target. */ - setPenColorToColor (args, util) { + setPenColorToColor (args: SetPenColorToColorArgs, util: BlockUtility) { const penState = this._getPenState(util.target); const rgb = Cast.toRgbColorObject(args.COLOR); const hsv = Color.rgbToHsv(rgb); @@ -571,7 +602,7 @@ class Scratch3PenBlocks { penState.saturation = hsv.s * 100; penState.brightness = hsv.v * 100; if (Object.prototype.hasOwnProperty.call(rgb, 'a')) { - penState.transparency = 100 * (1 - (rgb.a / 255.0)); + penState.transparency = 100 * (1 - (rgb.a! / 255.0)); } else { penState.transparency = 0; } @@ -585,10 +616,10 @@ class Scratch3PenBlocks { /** * Update the cached color from the color, saturation, brightness and transparency values * in the provided PenState object. - * @param {PenState} penState - the pen state to update. + * @param penState - the pen state to update. * @private */ - _updatePenColor (penState) { + private _updatePenColor (penState: PenState) { const rgb = Color.hsvToRgb({ h: penState.color * 360 / 100, s: penState.saturation / 100, @@ -602,13 +633,13 @@ class Scratch3PenBlocks { /** * Set or change a single color parameter on the pen state, and update the pen color. - * @param {ColorParam} param - the name of the color parameter to set or change. - * @param {number} value - the value to set or change the param by. - * @param {PenState} penState - the pen state to update. - * @param {boolean} change - if true change param by value, if false set param to value. + * @param param - the name of the color parameter to set or change. + * @param value - the value to set or change the param by. + * @param penState - the pen state to update. + * @param change - if true change param by value, if false set param to value. * @private */ - _setOrChangeColorParam (param, value, penState, change) { + private _setOrChangeColorParam (param: ColorParam, value: number, penState: PenState, change: boolean) { switch (param) { case ColorParam.COLOR: penState.color = this._wrapColor(value + (change ? penState.color : 0)); @@ -631,12 +662,10 @@ class Scratch3PenBlocks { /** * The "change pen {ColorParam} by {number}" block changes one of the pen's color parameters * by a given amound. - * @param {object} args - the block arguments. - * @property {ColorParam} COLOR_PARAM - the name of the selected color parameter. - * @property {number} VALUE - the amount to change the selected parameter by. - * @param {object} util - utility object provided by the runtime. + * @param args - the block arguments, containing the color parameter to change and the amount to change it by. + * @param util - a utility object containing information about the block's context, including the target. */ - changePenColorParamBy (args, util) { + changePenColorParamBy (args: ChangePenColorParamByArgs, util: BlockUtility) { const penState = this._getPenState(util.target); this._setOrChangeColorParam(args.COLOR_PARAM, Cast.toNumber(args.VALUE), penState, true); } @@ -644,34 +673,30 @@ class Scratch3PenBlocks { /** * The "set pen {ColorParam} to {number}" block sets one of the pen's color parameters * to a given amound. - * @param {object} args - the block arguments. - * @property {ColorParam} COLOR_PARAM - the name of the selected color parameter. - * @property {number} VALUE - the amount to set the selected parameter to. - * @param {object} util - utility object provided by the runtime. + * @param args - the block arguments, containing the color parameter to change and the amount to set it to. + * @param util - a utility object containing information about the block's context, including the target. */ - setPenColorParamTo (args, util) { + setPenColorParamTo (args: ChangePenColorParamByArgs, util: BlockUtility) { const penState = this._getPenState(util.target); this._setOrChangeColorParam(args.COLOR_PARAM, Cast.toNumber(args.VALUE), penState, false); } /** * The pen "change pen size by {number}" block changes the pen size by the given amount. - * @param {object} args - the block arguments. - * @property {number} SIZE - the amount of desired size change. - * @param {object} util - utility object provided by the runtime. + * @param args - the block arguments, containing the amount to change the pen size by. + * @param util - a utility object containing information about the block's context, including the target. */ - changePenSizeBy (args, util) { + changePenSizeBy (args: ChangePenSizeByArgs, util: BlockUtility) { const penAttributes = this._getPenState(util.target).penAttributes; penAttributes.diameter = this._clampPenSize(penAttributes.diameter + Cast.toNumber(args.SIZE)); } /** * The pen "set pen size to {number}" block sets the pen size to the given amount. - * @param {object} args - the block arguments. - * @property {number} SIZE - the amount of desired size change. - * @param {object} util - utility object provided by the runtime. + * @param args - the block arguments, containing the amount to set the pen size to. + * @param util - a utility object containing information about the block's context, including the target. */ - setPenSizeTo (args, util) { + setPenSizeTo (args: ChangePenSizeByArgs, util: BlockUtility) { const penAttributes = this._getPenState(util.target).penAttributes; penAttributes.diameter = this._clampPenSize(Cast.toNumber(args.SIZE)); } @@ -679,11 +704,10 @@ class Scratch3PenBlocks { /* LEGACY OPCODES */ /** * Scratch 2 "hue" param is equivelant to twice the new "color" param. - * @param {object} args - the block arguments. - * @property {number} HUE - the amount to set the hue to. - * @param {object} util - utility object provided by the runtime. + * @param args - the block arguments, containing the hue value to set the pen color to. + * @param util - a utility object containing information about the block's context, including the target. */ - setPenHueToNumber (args, util) { + setPenHueToNumber (args: SetPenHueToNumberArgs, util: BlockUtility) { const penState = this._getPenState(util.target); const hueValue = Cast.toNumber(args.HUE); const colorValue = hueValue / 2; @@ -694,11 +718,10 @@ class Scratch3PenBlocks { /** * Scratch 2 "hue" param is equivelant to twice the new "color" param. - * @param {object} args - the block arguments. - * @property {number} HUE - the amount of desired hue change. - * @param {object} util - utility object provided by the runtime. + * @param args - the block arguments, containing the hue value to change the pen color by. + * @param util - a utility object containing information about the block's context, including the target. */ - changePenHueBy (args, util) { + changePenHueBy (args: SetPenHueToNumberArgs, util: BlockUtility) { const penState = this._getPenState(util.target); const hueChange = Cast.toNumber(args.HUE); const colorChange = hueChange / 2; @@ -712,11 +735,10 @@ class Scratch3PenBlocks { * then convert back to HSV and store those components. * It is important to also track the given shade in penState._shade * because it cannot be accurately backed out of the new HSV later. - * @param {object} args - the block arguments. - * @property {number} SHADE - the amount to set the shade to. - * @param {object} util - utility object provided by the runtime. + * @param args - the block arguments, containing the shade value to set the pen color to. + * @param util - a utility object containing information about the block's context, including the target. */ - setPenShadeToNumber (args, util) { + setPenShadeToNumber (args: SetPenShadeToNumberArgs, util: BlockUtility) { const penState = this._getPenState(util.target); let newShade = Cast.toNumber(args.SHADE); @@ -733,11 +755,10 @@ class Scratch3PenBlocks { /** * Because "shade" cannot be backed out of hsv consistently, use the previously * stored penState._shade to make the shade change. - * @param {object} args - the block arguments. - * @property {number} SHADE - the amount of desired shade change. - * @param {object} util - utility object provided by the runtime. + * @param args - the block arguments, containing the amount to change the pen shade by. + * @param util - a utility object containing information about the block's context, including the target. */ - changePenShadeBy (args, util) { + changePenShadeBy (args: SetPenShadeToNumberArgs, util: BlockUtility) { const penState = this._getPenState(util.target); const shadeChange = Cast.toNumber(args.SHADE); this.setPenShadeToNumber({SHADE: penState._shade + shadeChange}, util); @@ -745,10 +766,9 @@ class Scratch3PenBlocks { /** * Update the pen state's color from its hue & shade values, Scratch 2.0 style. - * @param {object} penState - update the HSV & RGB values in this pen state from its hue & shade values. - * @private + * @param penState - update the HSV & RGB values in this pen state from its hue & shade values. */ - _legacyUpdatePenColor (penState) { + private _legacyUpdatePenColor (penState: PenState) { // Create the new color in RGB using the scratch 2 "shade" model let rgb = Color.hsvToRgb({h: penState.color * 360 / 100, s: 1, v: 1}); const shade = (penState._shade > 100) ? 200 - penState._shade : penState._shade; @@ -769,9 +789,8 @@ class Scratch3PenBlocks { /** * When runtime is disposed, dispose pen extension. - * @private */ - _dispose () { + private _dispose () { this.clear(); this._penSkinId = -1; this._penDrawableId = -1; diff --git a/packages/vm/src/extensions/scratch3_speech2text/index.js b/packages/vm/src/extensions/scratch3_speech2text/index.js index 72a0d6ed8..52ca54e25 100644 --- a/packages/vm/src/extensions/scratch3_speech2text/index.js +++ b/packages/vm/src/extensions/scratch3_speech2text/index.js @@ -606,7 +606,7 @@ class Scratch3Speech2TextBlocks { } /** - * @returns {object} Metadata for this extension and its blocks. + * @returns metadata for this extension and its blocks. */ getInfo () { return { diff --git a/packages/vm/src/extensions/scratch3_text2speech/index.js b/packages/vm/src/extensions/scratch3_text2speech/index.js index a54873b31..002b4a67f 100644 --- a/packages/vm/src/extensions/scratch3_text2speech/index.js +++ b/packages/vm/src/extensions/scratch3_text2speech/index.js @@ -406,7 +406,7 @@ class Scratch3Text2SpeechBlocks { } /** - * @returns {object} metadata for this extension and its blocks. + * @returns metadata for this extension and its blocks. */ getInfo () { // Only localize the default input to the "speak" block if we are in a diff --git a/packages/vm/src/extensions/scratch3_translate/index.js b/packages/vm/src/extensions/scratch3_translate/index.ts similarity index 84% rename from packages/vm/src/extensions/scratch3_translate/index.js rename to packages/vm/src/extensions/scratch3_translate/index.ts index e07488e84..33473ac83 100644 --- a/packages/vm/src/extensions/scratch3_translate/index.js +++ b/packages/vm/src/extensions/scratch3_translate/index.ts @@ -6,94 +6,89 @@ import fetchWithTimeout from '../../util/fetch-with-timeout'; import languageNames from 'scratch-translate-extension-languages'; import formatMessage from 'format-message'; +import type { + ExtensionClass, + ExtensionMetadata, + ExtensionMenuItemObject +} from '../../extension-support/extension-metadata'; + /** * Icon svg to be displayed in the blocks category menu, encoded as a data URI. - * @type {string} */ // eslint-disable-next-line max-len const menuIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAACXBIWXMAABYlAAAWJQFJUiTwAAAGAklEQVRYhe1YbUxTVxh+rh02o0KtkOEgKA4U4yeRWCdgxDoxCnH6h22iqSz76aasZlnijzkTBlvS4TJ/LGaJsmiyESe4hAVJvMJGxwQhLKECcRWkpWNZERs6Ctb2Lm97C/fe3n6Jyfzhk5y09z3nPPe57znnPe85DMdxeJ6x6LlW90LgM8BLchR1dXUZeXl5b3Ect+ppXsEwzHBfX98PVVVVY0GbmjW2AdgpaFYP4JxTZ+iLyCVdJFeuXNmdn59fn56enrFkyRIsWhSfk30+H1wuF+x2+1hPT4++oqLiJi/wEoA8AJslXSqdOsOlmARWV1dnlpeXd2ZnZ2fEK0xOqMViGWtoaNh++vRpa9CuZo1ZAJokQlc5dYYROR6RCq1WW56WlhZV3H0H8O9sZIHEQVzEKbTzQooBPBCYz4TlET4oFIosGtZoOHUN+Ph61GYgLuIU2tSscSmAYwAeCcx6NWs8o2aNxVKOkEUi9R55qv428Ng7b3viA/6eAs7dmrctVgD6bYBKGZ6LB4mrk7F/whcmokApfh8BWu6G2mc8ADsktuWmAbtzozGiLUJdu9QQVSC98JUkYNgBfPsboH4Z+GhPoK62FZiaAU7sCrTZmB5VHM3BPjVrrARwUVL1B4CD0vYxLVV68YFNQIICcLrn7SROtTjwEbGIE4iksFIpEVfs1BkeSdvGFUsObAz8Gm8CNTcC/49q42EIEbkLwKfhxCGWIRZC/zrQ/ifgcAWMK5YB+zc8nUBeZFuUORmfQIp/PsHGM/04YMta5oPT6cTs7Cw8Ho+oj9vtzmloaCgPZQtApVI96ejo6K2trR3lOM4nrRftJCzLfq3T6Y7LCfvuNtDL7wepfKgkTz6ZdeHdzePYlq30xz2lUintHhH0UbQ12my2+oKCguMcx7mE7aOHmWHgsxvzzzQP3ysMxMfzt2bxKmNHyZblSE5OjktYEImJidBoNFCr1frOzs5khmHe4Thubp8SCVQoFBwNUUJCwpyNwsfyZGBDOvB2fuCZQAH56KYJKJUpTy1OCOJYvXr1ocbGxjIAPwarRKvYZrNdn5iYEHV8LRW4cBj4oHheXBDT09PPRFwQxKXRaIQpmVjgkSNHfrFardcmJydjIqSMRehtOfzjmMTZmm/8hf5HAnF5vV7RVicSyHGcR6vVHh4YGPjKYrFMkTelq5JAH0B1MzMzUT+iu6cfdwfv+wv9jxchgZomaEFBwcmcnJxVY2NjXQqFQlQ/Pj6O/v7+s2az+U2Hw9Ec7X3tHXfm/v/c2hG3wLCruLm5+VBGRoY2mJGQJ0nc4ODgqZKSkjqKWSzL7olEPjJqx4PRv5CaqvE/OxyTflvWitj3xbBbnUql2kRxjYTRcA4MDHR1d3frguJiIW//NeC9/SVF2LplvcgWK8J6sKWl5UuVSrXO4/HYHj58+FNZWVkLx3HT8Rz0u3vN/t8Ho3aRaH3FgYULrKmpodT8jeBzvDcQ3T1m/5D6RXX0zNmn3TP+uq356xcmkE/NTwLoc+oMTXGpA3CnN7Bi99Hw5s8PL4mlulgFys5BXlwbn4I3qlnjsXgFBr22f+8OrFub7S/79u4Q1cWCEA8KxAmPhRfVrBFy51cK1nJnj+/rvwix0eqVswu5pJDzoPTMKhSZJzQolUoLZSLPCsRFnEI6OYE7I7xPdGYoKiq6YLVaByllWiiIg7iIM5rAYBouBB2yq5w6g+iATWGnqampZGhoqItiJSUP4YrcR9CQUh31JQ7iIk5hm7AXmPxdip5/dNIUCnduYBgm8fLly9tzc3NLwzlSqVTuW7NmzVphQkubwL179+xdXV3HKisrTVJxiJKwnuGHVM2XNjVrPCh3h8IT3+SLLKqrq+tKS0uvrly5UksJKvjsJSkpKd3r9TrkxCGSBxHwIoWXc7zAIOiIOOLUGULOsNHAMIzSZDJ9npmZeSIlJcWfTdPQm0ym8zqd7n257hGPnXxYyePv8py8mVb40ji1+UGZUmFh4Yetra1bzGbzteHh4SlKQNxu961wff7XS3Sau/w0c4VLQF7c8i8IAP4DcHKth/4Ur7MAAAAASUVORK5CYII='; /** * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. - * @type {string} */ // eslint-disable-next-line max-len const blockIconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAAACXBIWXMAABYlAAAWJQFJUiTwAAAN+UlEQVR4Ae1ce2xT1xn/Tkhq4hqHJKRLDAlQGI+GUfFc14HaLmxuGd0ab93GgK6Vmm01y9BUsaU0RfyRFTakaRHq3So6jVapWEUxa9dRuU8x6IAGCoO6wa1KXiSQOE9jkjivO/2u7yWOuff6XvvekFb5SUdx7ON7v/vz9zrnO+cwnudpAokjZYK75DBBYJKYIDBJTBCYJCYITBJfOgIZYzbGWA5jLJ8xNm/z5s334a/4P1omYyzNsPt9WdIYxhiUId/j8azPz89fY7VaF6ampjqi+4TD4Qvd3d0f7t+/fx/HcTVE1M7z/EBS99VDoCikTWzWMdTgYSLqIaIQGs/zwzKyOaqrq1+aOnVqUUZGBqWnp5PFYqG0tDQaHh6mvr4+GhwcpFAoJLSenp4P9+7dW8Zx3Fme5zsTFSxVa0eovtPpnL1ly5YfzJgxw2WxWBYketNEAO25dOmSp7Ky8iBjrFbmoW12u70oOzubMjMzR32QkpJCVqtVeG2322lgYIACgcDK0tLS1+6+++4yxpiH5/krCQkGDYzXiCi3qqrKXVNTE7x48SLf0dHB9/X18WMF3Av3xL0hA2SBTNFyE9E8v9/PX758WbNUuCa+4/F4ymKvp7XF1UDGWK7H43m0sLBwZ05Ozg2/7lgApoiGe3d2dk5ZsWLFcx6Px84Y2xelOcHW1taXiOiRcDgsaJ2gljab0GDKscD1Jk2ahHd3ejwePOtenufb9TySKoEw26qqKhfIy83NFdT/ZiP6oauqqoKMsf2iObeuXr26vKKiwpOVlTXFbrfb8/LyFubk5KyBu1H68aVnGh4e3uZ2u08yxo7pCixKqgrX4XQ6l8JkoOrjDZAJskFGyBolt0UMcGhgJ19yP2rm3dDQwJ85c+akXlNWI9B++PDh38PvjFdANsgIWVUfksheUVHxfZDY3d0t+zT9/f2CP3S73feJWYZFC4FqJmxDtJ06daphRnuiluhrDqJbLcZcD7JBRiJ6Dj5Qrg9jDHfLhlm3tbUdslgsj8i5IvhI+MpNmzaVLlq0yCN+9wMiCvA8H1KSQY1AK3yHFP6ThfcTouePEaVNItrzI6LbpiR/TcgmplOyQmLE4XQ6C3fv3v0y+iEQIbggjZELKiB2+vTpxXl5ecUIRD6fr/nQoUNuxtjbPM/3yN1DLREWPsNNjUBNC9HgMFEKI2q5asglo2VTeo7sioqKP6anpwtBZObMmZSfny9LHokRG5+jn8PhoFtuucVRXFzMQdmVZPiyTyakIblGBqE3/QKZIBLDQafTmavUT/NIRA3XwkSlB4jaFD3FCPqHiJ5+Xb3PNBvRnoeN85UYyiUCmDHg9XoVn8wQDTxRR9RxjQjxLl4DgfH64Fq4pgEY6OjoONTe3q6bRPTH9zBmFsfgsjBEA4vmE/kuE30eiPzfN0B0WYyJGelEWQpx6FJXhNC0FKIcG9Fk0TUVZEWuaQBad+zYUbF79+6FjY2NC+DfpBGKGkBeY2MjBYPBC2VlZU9g1sZUAoFf3zvyGia9/u8RbRrmiSofvrG/1IeEaEn0/E+NkmQEmLVhjPmIaAMisRYSo8nbunXrBq/X6+N5PqzU35QgAt8177bI655+ovPNN/Z50xeJyIyIlswwQ4oI8PAgAWSAFJCjZM56ySMzo/DP7iJKTSEaGibi/jP6M2jfwbNEQzzRpBSin68yS4oItJCYCHlkJoEYcWSKvu9KkOjwxyOf7T9F1DsQ0b6CTGOS6niQIxEJNSVBHhnpA+Xwm28RPfOvSAL9t+NEy2cShcJEr5+P+EcQ+PT9ZkowGiBF8olIsMPhcBFmrnt7e4Voi4Chhzwym0BoYWFexAcODBGVvUbU3RshbxIjWrdobLQvGhKJXq+3xO12z1q3bt2aN9544x2O45A4Neshj8wmENjmJHr8ZaJQ/0iizcRk+fFvmn13eYgk1TLGWjiOwxQWKY1148H0oRwiMohiUYky8Oz3zL5zfIA0qSV6DdM18NWPiF4+FXnNogh84h9EW9cQ3TV7pC+ceVTVTNPooa5OGLLMYkgmE0fcqp8STCPwYhvRn94jauqKBBHJbOED4Q8xAtn1FtH8rxA9WUSUNthJiQy5Zs2aRX6/35usvBqqfrJQrAujmu/3+/3z5s3TJQgCxsEzRP9riuSAUsBYXhCJysCWVyP+EHkgiA0Hr9D9Xw3S/QuJJlvShIlSca4vASr0A5MG0Piuri68vnr69OmyjRs3aip1GqaByPNePRuZCMDwTSIuPY1oy32jTfWFDUQvniB67RzRta4rtDwvSN8tJBrnVT9ZGEIgpupf+C/RgGh90CpMEKwtJFq/XH5aCiOVb0zvpH+fipD3Baj6yUKNQIEOqLcWU4IPx9ANGue6k+iBQvX5PPi6lHD7dc0bD+RJgCxDQ0O0bNmyXU6n8wRjrFsxsKhUshznzp2r0VrSbAny/OcB7eU7VMdQBfuiV/3U8sAQohIcqxZgRHH7NO2/MlIVEitr4xVRVT+bkoiqBCKkIyp1dia8eEkRiHokVtbGK+JV/VQJhM17vd5ahPRAIICZCkMfU8r3xipVSQQaqn7qURjRB0u/ENIRleBYb0aaEQ+Btk4qfXLnqF7bn/oF3bFgjun3jjsWRh7kcrn2VVdXb25pablaW1uLXOl6xWo84LD36A1SHDl6akwk05QHgkTkQwjpsQsstSa/V65cGeUGMIbFMMwIyJFVfdpHj2zopVut6YbcQwmaZ2Ngzl6v9+zatWv/sHjx4u9UVlYKAzMUoOMB5HV1dV09cODAQ/NFOJ1OpxEPAKJ6evtueB/vnTrtM+IWqtA1nYXAwvM81GgAi3CUFi5KQKBobm4WyDt48OCm8vLy93me/xQNSmjEAxw5NqJ91vTJQpNQ/dE4I5AikwxWjuPWZGVlFWM9shLgI+vr6zHDcoHjuAdF8gwN5dd6eulUFEkrlhUKTQI+Q4AxE4lMqOasWrXqafg9uRREquiDvKamppe2bdv2kz179pwwmjyS8X3Lly4SWjSqT38s/2WDkMhkggUBJHbsCuIQndH6+/ubjxw58ju32/0eXKCeCUo9iDbfadMyr2sfzFjyi4ffOkZrnatNoi8xAoX1JjabrRhDnehZZBDn9/v3uFyuV+ItTEwWdQ3NVN9w+fpVViwdMV0QeeTYaeF1W1un0HdWgcNoEQQkYsJd27dvfxa+7dKlS9TQ0NBcV1d36OTJk5sLCwtXuVwujuf5WjPJIxnzvWf18uuvY834TZk80Sjo1kBxdPLp4sWLizBnKr4NewnqLQkmg1gCy575s+LVkOo8UWKOHAlV5cSAgGmadnG/WWCsyZPL/ZSAvmaNTHRroLho2xFdlGaMXRSDxZiQGJvf3bHgdtl+n1y4OOo70WZuFHQRCPKwaHvXrl1/sVqtK7EsYs6cOdsee+yxd8vLy3+Lir/ZJCKvi879Zhbk0fanfinb91dP7hSCCEXlhDnTjJ0M0WzCEnlYZ5eZmbly7ty5woLs2bNnYzxchPfxuaihpiE2r7tnlbJWRUdmMikn1ERgNHl2u33UIkUM5fA/3h8LEpHXRWPFskWKfWNNNva7RkDLZkNF8iTgf7yPFaDoh9VPZpnzvVEaZ7VOVjVJ5H4/fOjbRoswGnG2SIG8pSgu1dXV8UNDQ6pFGHyOfugv7mFT3C5FRHNRVBrLbbN6AdkgI2RNZK+cLvIkyJCYonB9XVW/mwHIBhkhayJVuWxEWyWzVYJkzjabbcGOHTvKUbBT6Kqr6nczANkgo9o2BzVWbEhVMGWllbzrF01JIXwPU16IMwrdTK36JQuxbHEVMiZEoNPpFKaaE62axSPd7KpfMoAskAmyQUa12STFKOz1egODg4PN9fX1Dqxb0TJ1LwG/HuYEg8Hgu5i9Ueo3Hqt+kB3k+Xy+p8QVWqrmoZbGtGOrJ3YrNjc3O6StonhAOTKx4h21D0xvYTYa6+0wOlHb5UMjBat9WMiDtShdXV1TvkjL21TPjcEpQJiB5jhOWM28ZMkSV0FBQTG2gsoBJU/MQp8/f/5tt9v9gZ5F20Yeq4LJXlhNPEjaRjELLEWz1eaYNR57AlWwYTs88iJsj5cDFgxhWz2218fbhq90ToN4zgF+obk4ykRvk7b2azn+JGabf754b9m0S3ceqPCAuTiYAQc0KAGCR53tIgklHQKh6RyCZFr0+QhNTU1x81f0OX78uMe0c2Ni0I7jknDiTzAYxNEiN3SA6VgsFmGV57lz50oDgcA7fr9f2ErQ0dFxlTH2ERE1mVUnwVwlY+z9lpaWB0tKSv4aDocXwOUo+VOkW6FQqJjjOI94gpG+FfsJmFk2TvqB6iudgCGZBzJ5aCsaRif4ztGjR19M9NfWKadl3bp1d+J+uG9bW5uiNra2tkojjpm675OgcLkSiXqGYjBvcWw5z2wC+RGf6uA4bqPP52v67LPPZImMGvPqliuhNdKxqUdvb+8UrJFRW6VAN2Epm+gmmhlj/ySiDzwez4/nz59f2tnZ6ZCOhEJqhvQFlUa1nFUJCS8ylxYcud3umpKSkl2hUGilJFRqaipNnjz5+hEjyLOwoa+7u1tKrk2t2MnIKmygYYzhBI5XqqqqHpgzZ84au93+dRwqgRQGlUaxzqMLSR/AKJ4Gme12uxeuX7/+0YyMjJWxORxGND09PTWNjY3vuFyu/UTUaFYQ0SizRcwOpIU0Q2JVUf8Pa6C/AZGZYuqSL+VkYh6H/3OQS46F7xvLNnGSeZKYOMU3SUwQmCQmCEwSEwQmiQkCkwER/R+aET3lwEIlXgAAAABJRU5ErkJggg=='; /** * The url of the translate server. - * @type {string} */ const serverURL = 'https://translate-service.scratch.mit.edu/'; /** * How long to wait in ms before timing out requests to translate server. - * @type {int} */ const serverTimeoutMs = 10000; // 10 seconds (chosen arbitrarily). +interface GetTranslateArgs { + WORDS: string; + LANGUAGE: string; +} + +type LanguageKeys = keyof typeof languageNames.menuMap; +type LanguageNames = keyof typeof languageNames.nameMap; +type ScratchToGoogleKeys = keyof typeof languageNames.scratchToGoogleMap; + /** * Class for the translate block in Scratch 3.0. - * @class */ -class Scratch3TranslateBlocks { - constructor () { - /** - * Language code of the viewer, based on their locale. - * @type {string} - * @private - */ - this._viewerLanguageCode = this.getViewerLanguageCode(); - - /** - * List of supported language name and language code pairs, for use in the block menu. - * Filled in by getInfo so it is updated when the interface language changes. - * @type {Array.>} - * @private - */ - this._supportedLanguages = []; +class Scratch3TranslateBlocks implements ExtensionClass { + /** + * Language code of the viewer, based on their locale. + */ + private _viewerLanguageCode = this.getViewerLanguageCode(); - /** - * A randomly selected language code, for use as the default value in the language menu. - * Properly filled in getInfo so it is updated when the interface languages changes. - * @type {string} - * @private - */ - this._randomLanguageCode = 'en'; + /** + * List of supported language name and language code pairs, for use in the block menu. + * Filled in by getInfo so it is updated when the interface language changes. + */ + private _supportedLanguages: ExtensionMenuItemObject[] = []; + /** + * A randomly selected language code, for use as the default value in the language menu. + * Properly filled in getInfo so it is updated when the interface languages changes. + */ + private _randomLanguageCode = 'en'; - /** - * The result from the most recent translation. - * @type {string} - * @private - */ - this._translateResult = ''; + /** + * The result from the most recent translation. + */ + private _translateResult = ''; - /** - * The language of the text most recently translated. - * @type {string} - * @private - */ - this._lastLangTranslated = ''; + /** + * The language of the text most recently translated. + */ + private _lastLangTranslated = ''; - /** - * The text most recently translated. - * @type {string} - * @private - */ - this._lastTextTranslated = ''; - } + /** + * The text most recently translated. + */ + private _lastTextTranslated = ''; /** * The key to load & store a target's translate state. - * @returns {string} The key. + * @returns The key. */ - static get STATE_KEY () { - return 'Scratch.translate'; + static get STATE_KEY (): string { + return 'Scratch.translate' as const; } /** - * @returns {object} metadata for this extension and its blocks. + * @returns metadata for this extension and its blocks. */ getInfo () { this._supportedLanguages = this._getSupportedLanguages(this.getViewerLanguageCode()); @@ -151,39 +146,38 @@ class Scratch3TranslateBlocks { items: this._supportedLanguages } } - }; + } as ExtensionMetadata; } /** * Computes a list of language code and name pairs for the given language. - * @param {string} code The language code to get the list of language pairs - * @returns {Array.>} An array of languge name and - * language code pairs. - * @private + * @param code The language code to get the list of language pairs + * @returns An array of languge name and language code pairs. */ - _getSupportedLanguages (code) { + private _getSupportedLanguages (code: LanguageKeys): ExtensionMenuItemObject[] { return languageNames.menuMap[code].map(entry => { const obj = {text: entry.name, value: entry.code}; return obj; }); } + /** * Get the human readable language value for the reporter block. - * @returns {string} the language name of the project viewer. + * @returns the language name of the project viewer. */ - getViewerLanguage () { + getViewerLanguage (): string { this._viewerLanguageCode = this.getViewerLanguageCode(); const names = languageNames.menuMap[this._viewerLanguageCode]; let langNameObj = names.find(obj => obj.code === this._viewerLanguageCode); // If we don't have a name entry yet, try looking it up via the Google langauge // code instead of Scratch's (e.g. for es-419 we look up es to get espanol) - if (!langNameObj && languageNames.scratchToGoogleMap[this._viewerLanguageCode]) { - const lookupCode = languageNames.scratchToGoogleMap[this._viewerLanguageCode]; + if (!langNameObj && languageNames.scratchToGoogleMap[this._viewerLanguageCode as ScratchToGoogleKeys]) { + const lookupCode = languageNames.scratchToGoogleMap[this._viewerLanguageCode as ScratchToGoogleKeys]; langNameObj = names.find(obj => obj.code === lookupCode); } - let langName = this._viewerLanguageCode; + let langName: string = this._viewerLanguageCode; if (langNameObj) { langName = langNameObj.name; } @@ -192,10 +186,10 @@ class Scratch3TranslateBlocks { /** * Get the viewer's language code. - * @returns {string} the language code. + * @returns the language code. */ getViewerLanguageCode () { - const locale = formatMessage.setup().locale; + const locale = formatMessage.setup().locale as string; const viewerLanguages = [locale].concat(navigator.languages); const languageKeys = Object.keys(languageNames.menuMap); // Return the first entry in viewerLanguages that matches @@ -210,16 +204,16 @@ class Scratch3TranslateBlocks { return acc; }, '') || 'en'; - return languageCode.toLowerCase(); + return languageCode.toLowerCase() as LanguageKeys; } /** * Get a language code from a block argument. The arg can be a language code * or a language name, written in any language. - * @param {object} arg A block argument. - * @returns {string} A language code. + * @param arg A block argument. + * @returns A language code. */ - getLanguageCodeFromArg (arg) { + getLanguageCodeFromArg (arg: unknown): string { const languageArg = Cast.toString(arg).toLowerCase(); // Check if the arg matches a language code in the menu. if (Object.prototype.hasOwnProperty.call(languageNames.menuMap, languageArg)) { @@ -227,7 +221,7 @@ class Scratch3TranslateBlocks { } // Check for a dropped-in language name, and convert to a language code. if (Object.prototype.hasOwnProperty.call(languageNames.nameMap, languageArg)) { - return languageNames.nameMap[languageArg]; + return languageNames.nameMap[languageArg as LanguageNames]; } // There are some languages we launched in the language menu that Scratch did not @@ -243,10 +237,10 @@ class Scratch3TranslateBlocks { /** * Translates the text in the translate block to the language specified in the menu. - * @param {object} args - the block arguments. - * @returns {Promise} - a promise that resolves after the response from the translate server. + * @param args - the block arguments. + * @returns a promise that resolves after the response from the translate server. */ - getTranslate (args) { + getTranslate (args: GetTranslateArgs) { // If the text contains only digits 0-9 and nothing else, return it without // making a request. if (/^\d+$/.test(args.WORDS)) return Promise.resolve(args.WORDS); @@ -264,9 +258,10 @@ class Scratch3TranslateBlocks { urlBase += '&text='; urlBase += encodeURIComponent(args.WORDS); + // eslint-disable-next-line @typescript-eslint/no-this-alias const tempThis = this; const translatePromise = fetchWithTimeout(urlBase, {}, serverTimeoutMs) - .then(response => response.text()) + .then((response: Response) => response.text()) .then(responseText => { const translated = JSON.parse(responseText).result; tempThis._translateResult = translated; @@ -276,11 +271,12 @@ class Scratch3TranslateBlocks { tempThis._lastLangTranslated = args.LANGUAGE; return translated; }) - .catch(err => { + .catch((err: unknown) => { log.warn(`error fetching translate result! ${err}`); return ''; }); return translatePromise; } } + export default Scratch3TranslateBlocks; diff --git a/packages/vm/src/extensions/scratch3_video_sensing/index.js b/packages/vm/src/extensions/scratch3_video_sensing/index.js index b43091a5a..ddd2c6555 100644 --- a/packages/vm/src/extensions/scratch3_video_sensing/index.js +++ b/packages/vm/src/extensions/scratch3_video_sensing/index.js @@ -1,10 +1,10 @@ -import Runtime from '../../engine/runtime.js'; +import Runtime from '../../engine/runtime'; import ArgumentType from '../../extension-support/argument-type'; import BlockType from '../../extension-support/block-type'; import Clone from '../../util/clone'; import Cast from '../../util/cast'; import formatMessage from 'format-message'; -import Video from '../../io/video.js'; +import Video from '../../io/video'; import VideoMotion from './library.js'; /** @@ -416,7 +416,7 @@ class Scratch3VideoSensingBlocks { } /** - * @returns {object} metadata for this extension and its blocks. + * @returns metadata for this extension and its blocks. */ getInfo () { // Set the video display properties to defaults the first time diff --git a/packages/vm/src/extensions/scratch3_wedo2/index.js b/packages/vm/src/extensions/scratch3_wedo2/index.ts similarity index 77% rename from packages/vm/src/extensions/scratch3_wedo2/index.js rename to packages/vm/src/extensions/scratch3_wedo2/index.ts index 2557cfc2d..115a9e8bc 100644 --- a/packages/vm/src/extensions/scratch3_wedo2/index.js +++ b/packages/vm/src/extensions/scratch3_wedo2/index.ts @@ -3,27 +3,28 @@ import BlockType from '../../extension-support/block-type'; import Cast from '../../util/cast'; import formatMessage from 'format-message'; import color from '../../util/color'; -import BLE from '../../io/ble.js'; +import BLE from '../../io/ble'; import Base64Util from '../../util/base64-util'; import MathUtil from '../../util/math-util'; import RateLimiter from '../../util/rateLimiter'; import log from '../../util/log'; +import type {ExtensionClass, ExtensionMetadata} from '../../extension-support/extension-metadata'; +import type Runtime from '../../engine/runtime'; + /** * Icon svg to be displayed at the left edge of each extension block, encoded as a data URI. - * @type {string} */ // eslint-disable-next-line max-len const iconURI = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAFAAAABQCAYAAACOEfKtAAAACXBIWXMAABYlAAAWJQFJUiTwAAAF8klEQVR4Ae2cbWxTVRjH/7ctbVc2tyEMNpWBk0VIkLcEjSAQgglTE5HEaKqJi1E/mbCP/dJA0kQbvzgTQ0Ki2T7V6AeYGoEPLJmGKPiyzZDwEpYJCHSbQIcbdLvres1zOa13Xbvdu2eTDp9fst329Lnn5XfPPfece7tphmFAmDkuccdDBDIRgUxEIBMRyEQEMhGBTEQgExHIRAQyEYFMRCATEchEBDIRgUxEIBMRyEQEMhGBTEQgExHIxMPNIByNVQBoBUDb7kgo2KTS9wBoUmFNkVCwW6U3A1gP4JJKHwxHY/S+WcW2RkLBVhV7AMAOAIMAGlWstbyOSCh4QMU2Uoy1PBVL+a7IqZu1vOZIKNg20/azBarGvKxebw9HY22RULADwBFLTBcATQnZl4lVEimN4ssteXQrQfstebQpmW1q30xshyqvxRLbofYnYW9ZYgeV8C5LLOWlzbTxM3ouHI7GPgSwWx3Z0syBSBku6IYnlTbM+uQenJQaMnKHDaqAFnDrcCFbl3G1defEjas0a4N/Vz10OybyvapfrSX1sjpo+WIz0ME7QL3djgtHPTAcjb2mepw/b2ZaGh5NL5RnofR8R99dIC5fHusK5JsrCUpm7TSx21XvbcwTNwnbAsPR2GcA3qaG+H0LsHlDPZ7fca/ujZ+cRW9/Em5vCXzlNVhQUjFpf/3OTSRvXkKJz43Xt1bh1S1LUeq/5+njQ9/iVmLIfL1ieRU2b1iFtavztXNu6TrTi8PfnYI67WdPoOp5przV9Y8iuHdb9rOW9uumPI+vDIElddBckztPOqVn5X36Xj1WVQeynx1sOWbK83jc2PviM/dFXIYNax9H55leXLoyYHsfWwI14JCRRx7x5ckBU1oheYQ+1G9u39lVM0Hej7+cR7w/Yb7e9+5LqChfaLvixcK088BwNNZkAOV02ubK6+odwt3RcfOULSSPGEveG48bNj08If3kqXPmdtO6unkpDzYn0u/TLxrzcumJJ80Ut79sygzoFF6/siw75mUYupOEpmnY0/A0pw33FTsCa+hX5oJhZXgkZb5zub2O20CnL7EwkPeCPm+wI7CEBvi5wuOZ36tJW7X3uGXJXAgxk8P4eNpRPEvgskqfuR0Z/BNGejxvDM3/5gs0pboWv+motqybCc+tqUCzz43kaBJ/X+2eMjZ3ClNsjIzo5ioknXZ2b4AlkKYltLJoaY9jOJm/B0KJbtg4c4F/XOmH3+dF9dLKbBo1OD6QQGV56YQ55ODtO0jcHkZ1VSX8/n9nB9S7RkZ1rFy+NG8ZR9s70TeQQKDEh7vJUdt1Y9/OopXFB2/WcbMpyOexE9mlFS21aLlHMmKHfzBl0QT/hV2bzM9oLXv0xG8YGR0zpdLEn6RT2k+/XjDzoLX2G3u3TZBLUyral/Z5qCyAK1f/sl2/or+IWNel1Eji3MWrpjyCZHWqdNrSe6ieSHFERl4mP+q5GehgHGvvRGal5XI5uzU47f3A/R99YTgdF2wXrmkolr9ToZ5NvTjT4yOhoC2T057CJM/r9WDxoqmXa07R9THcuDVcMO8bt4ag6ynULKvkFjWBTLl0ugZKvNlyqLeSQKfYGgOpgXt2b5zVhlzrS+Dr451YvKg0b95txztxvS8xZ+VuXFuLJ5+oNgV+9c3PuHDxGs6cu+w4v//9RJo6x5bN9UgbBo4cPY1U6j+cSD8orFvzGFYuX4KxsRQGbth6FCICc9m5dY05HtN46AQRqPB5PWjY+ZT5RnMwkxGBFh5ZVmle9Z3MrGbjwfqccrC1vajrV7QCaVCfS6qrJj96nQlFK5CujPRT7MgYyEQEMhGBTGwJpAW4kJ9pBbo0zbx70X7y7AOv8HxP3LyB4YTpb2cZBt2iqL3QEwf9zDbX+waLca439QMeC7a+YBmOxugLiM/OTt2yaOoMoO+H6LOcNwf6xusrthsh/7mIh1yFmYhAJiKQiQhkIgKZiEAmIpCJCGQiApmIQCYikIkIZCICmYhAJiKQiQhkIgKZiEAmIpCJCGQiAjkA+AeOwQKMcWZqHgAAAABJRU5ErkJggg=='; /** * A list of WeDo 2.0 BLE service UUIDs. - * @enum */ const BLEService = { DEVICE_SERVICE: '00001523-1212-efde-1523-785feabcd123', IO_SERVICE: '00004f0e-1212-efde-1523-785feabcd123' -}; +} as const; /** * A list of WeDo 2.0 BLE characteristic UUIDs. @@ -35,8 +36,6 @@ const BLEService = { * - INPUT_VALUES * - INPUT_COMMAND * - OUTPUT_COMMAND - * - * @enum */ const BLECharacteristic = { ATTACHED_IO: '00001527-1212-efde-1523-785feabcd123', @@ -44,30 +43,25 @@ const BLECharacteristic = { INPUT_VALUES: '00001560-1212-efde-1523-785feabcd123', INPUT_COMMAND: '00001563-1212-efde-1523-785feabcd123', OUTPUT_COMMAND: '00001565-1212-efde-1523-785feabcd123' -}; +} as const; /** * A time interval to wait (in milliseconds) in between battery check calls. - * @type {number} */ const BLEBatteryCheckInterval = 5000; /** * A time interval to wait (in milliseconds) while a block that sends a BLE message is running. - * @type {number} */ const BLESendInterval = 100; /** * A maximum number of BLE message sends per second, to be enforced by the rate limiter. - * @type {number} */ const BLESendRateMax = 20; /** * Enum for WeDo 2.0 sensor and output types. - * @readonly - * @enum {number} */ const WeDo2Device = { MOTOR: 1, @@ -75,23 +69,19 @@ const WeDo2Device = { LED: 23, TILT: 34, DISTANCE: 35 -}; +} as const; /** * Enum for connection/port ids assigned to internal WeDo 2.0 output devices. - * @readonly - * @enum {number} */ // TODO: Check for these more accurately at startup? const WeDo2ConnectID = { LED: 6, PIEZO: 5 -}; +} as const; /** * Enum for ids for various output commands on the WeDo 2.0. - * @readonly - * @enum {number} */ const WeDo2Command = { MOTOR_POWER: 1, @@ -99,122 +89,105 @@ const WeDo2Command = { STOP_TONE: 3, WRITE_RGB: 4, SET_VOLUME: 255 -}; +} as const; /** * Enum for modes for input sensors on the WeDo 2.0. - * @enum {number} */ const WeDo2Mode = { TILT: 0, // angle DISTANCE: 0, // detect LED: 1 // RGB -}; +} as const; /** * Enum for units for input sensors on the WeDo 2.0. * * 0 = raw * 1 = percent - * - * @enum {number} */ const WeDo2Unit = { TILT: 0, DISTANCE: 1, LED: 0 -}; +} as const; /** * Manage power, direction, and timers for one WeDo 2.0 motor. */ class WeDo2Motor { /** - * Construct a WeDo 2.0 Motor instance. - * @param {WeDo2} parent - the WeDo 2.0 peripheral which owns this motor. - * @param {int} index - the zero-based index of this motor on its parent peripheral. + * The WeDo 2.0 peripheral which owns this motor. */ - constructor (parent, index) { - /** - * The WeDo 2.0 peripheral which owns this motor. - * @type {WeDo2} - * @private - */ - this._parent = parent; + private _parent: WeDo2; - /** - * The zero-based index of this motor on its parent peripheral. - * @type {int} - * @private - */ - this._index = index; + /** + * The zero-based index of this motor on its parent peripheral. + */ + private _index: number; - /** - * This motor's current direction: 1 for "this way" or -1 for "that way" - * @type {number} - * @private - */ - this._direction = 1; + /** + * This motor's current direction: 1 for "this way" or -1 for "that way" + */ + private _direction = 1; - /** - * This motor's current power level, in the range [0,100]. - * @type {number} - * @private - */ - this._power = 100; + /** + * This motor's current power level, in the range [0,100]. + */ + private _power = 100; - /** - * Is this motor currently moving? - * @type {boolean} - * @private - */ - this._isOn = false; + /** + * Is this motor currently moving? + */ + private _isOn = false; - /** - * If the motor has been turned on or is actively braking for a specific duration, this is the timeout ID for - * the end-of-action handler. Cancel this when changing plans. - * @type {object} - * @private - */ - this._pendingTimeoutId = null; + /** + * If the motor has been turned on or is actively braking for a specific duration, this is the timeout ID for + * the end-of-action handler. Cancel this when changing plans. + */ + private _pendingTimeoutId: number | null = null; - /** - * The starting time for the pending timeout. - * @type {object} - * @private - */ - this._pendingTimeoutStartTime = null; + /** + * The starting time for the pending timeout. + */ + private _pendingTimeoutStartTime: number | null = null; - /** - * The delay/duration of the pending timeout. - * @type {object} - * @private - */ - this._pendingTimeoutDelay = null; + /** + * The delay/duration of the pending timeout. + */ + private _pendingTimeoutDelay: number | null = null; + + /** + * Construct a WeDo 2.0 Motor instance. + * @param parent - the WeDo 2.0 peripheral which owns this motor. + * @param index - the zero-based index of this motor on its parent peripheral. + */ + constructor (parent: WeDo2, index: number) { + this._parent = parent; + this._index = index; this.startBraking = this.startBraking.bind(this); this.turnOff = this.turnOff.bind(this); } /** - * @returns {number} - the duration of active braking after a call to startBraking(). Afterward, turn the motor off. - * @class + * @returns the duration of active braking after a call to startBraking(). Afterward, turn the motor off. */ static get BRAKE_TIME_MS () { return 1000; } /** - * @returns {int} - this motor's current direction: 1 for "this way" or -1 for "that way" + * @returns this motor's current direction: 1 for "this way" or -1 for "that way" */ get direction () { return this._direction; } /** - * @param {int} value - this motor's new direction: 1 for "this way" or -1 for "that way" + * @param value - this motor's new direction: 1 for "this way" or -1 for "that way" */ - set direction (value) { + set direction (value: number) { if (value < 0) { this._direction = -1; } else { @@ -223,16 +196,16 @@ class WeDo2Motor { } /** - * @returns {int} - this motor's current power level, in the range [0,100]. + * @returns this motor's current power level, in the range [0,100]. */ get power () { return this._power; } /** - * @param {int} value - this motor's new power level, in the range [0,100]. + * @param value - this motor's new power level, in the range [0,100]. */ - set power (value) { + set power (value: number) { const p = Math.max(0, Math.min(value, 100)); // Lego Wedo 2.0 hub only turns motors at power range [30 - 100], so // map value from [0 - 100] to [30 - 100]. @@ -245,21 +218,21 @@ class WeDo2Motor { } /** - * @returns {boolean} - true if this motor is currently moving, false if this motor is off or braking. + * @returns true if this motor is currently moving, false if this motor is off or braking. */ get isOn () { return this._isOn; } /** - * @returns {boolean} - time, in milliseconds, of when the pending timeout began. + * @returns time, in milliseconds, of when the pending timeout began. */ get pendingTimeoutStartTime () { return this._pendingTimeoutStartTime; } /** - * @returns {boolean} - delay, in milliseconds, of the pending timeout. + * @returns delay, in milliseconds, of the pending timeout. */ get pendingTimeoutDelay () { return this._pendingTimeoutDelay; @@ -285,9 +258,9 @@ class WeDo2Motor { /** * Turn this motor on for a specific duration. - * @param {number} milliseconds - run the motor for this long. + * @param milliseconds - run the motor for this long. */ - turnOnFor (milliseconds) { + turnOnFor (milliseconds: number) { if (this._power === 0) return; milliseconds = Math.max(0, milliseconds); @@ -315,7 +288,7 @@ class WeDo2Motor { /** * Turn this motor off. - * @param {boolean} [useLimiter] - if true, use the rate limiter + * @param useLimiter - if true, use the rate limiter */ turnOff (useLimiter = true) { if (this._power === 0) return; @@ -333,9 +306,8 @@ class WeDo2Motor { /** * Clear the motor action timeout, if any. Safe to call even when there is no pending timeout. - * @private */ - _clearTimeout () { + private _clearTimeout () { if (this._pendingTimeoutId !== null) { clearTimeout(this._pendingTimeoutId); this._pendingTimeoutId = null; @@ -346,13 +318,12 @@ class WeDo2Motor { /** * Set a new motor action timeout, after clearing an existing one if necessary. - * @param {Function} callback - to be called at the end of the timeout. - * @param {int} delay - wait this many milliseconds before calling the callback. - * @private + * @param callback - to be called at the end of the timeout. + * @param delay - wait this many milliseconds before calling the callback. */ - _setNewTimeout (callback, delay) { + private _setNewTimeout (callback: () => void, delay: number) { this._clearTimeout(); - const timeoutID = setTimeout(() => { + const timeoutID = window.setTimeout(() => { if (this._pendingTimeoutId === timeoutID) { this._pendingTimeoutId = null; this._pendingTimeoutStartTime = null; @@ -370,70 +341,61 @@ class WeDo2Motor { * Manage communication with a WeDo 2.0 peripheral over a Bluetooth Low Energy client socket. */ class WeDo2 { + /** + * The Scratch 3.0 runtime used to trigger the green flag button. + */ + private _runtime: Runtime; - constructor (runtime, extensionId) { + /** + * The id of the extension this peripheral belongs to. + */ + private _extensionId: string; - /** - * The Scratch 3.0 runtime used to trigger the green flag button. - * @type {Runtime} - * @private - */ - this._runtime = runtime; - this._runtime.on('PROJECT_STOP_ALL', this.stopAll.bind(this)); + /** + * A list of the ids of the motors or sensors in ports 1 and 2. + */ + private _ports: number[] = [0, 0]; - /** - * The id of the extension this peripheral belongs to. - */ - this._extensionId = extensionId; + /** + * The motors which this WeDo 2.0 could possibly have. + */ + private _motors: (WeDo2Motor | null)[] = [null, null]; - /** - * A list of the ids of the motors or sensors in ports 1 and 2. - * @type {string[]} - * @private - */ - this._ports = ['none', 'none']; + /** + * The most recently received value for each sensor. + */ + private _sensors = { + tiltX: 0, + tiltY: 0, + distance: 0 + }; - /** - * The motors which this WeDo 2.0 could possibly have. - * @type {WeDo2Motor[]} - * @private - */ - this._motors = [null, null]; + /** + * The Bluetooth connection socket for reading/writing peripheral data. + */ + private _ble: BLE | null = null; - /** - * The most recently received value for each sensor. - * @type {Record} - * @private - */ - this._sensors = { - tiltX: 0, - tiltY: 0, - distance: 0 - }; + /** + * A rate limiter utility, to help limit the rate at which we send BLE messages + * over the socket to Scratch Link to a maximum number of sends per second. + */ + private _rateLimiter: RateLimiter; + + /** + * An interval id for the battery check interval. + */ + private _batteryLevelIntervalId: number | null = null; + + constructor (runtime: Runtime, extensionId: string) { + this._runtime = runtime; + this._runtime.on('PROJECT_STOP_ALL', this.stopAll.bind(this)); + + this._extensionId = extensionId; - /** - * The Bluetooth connection socket for reading/writing peripheral data. - * @type {BLE} - * @private - */ - this._ble = null; this._runtime.registerPeripheralExtension(extensionId, this); - /** - * A rate limiter utility, to help limit the rate at which we send BLE messages - * over the socket to Scratch Link to a maximum number of sends per second. - * @type {RateLimiter} - * @private - */ this._rateLimiter = new RateLimiter(BLESendRateMax); - /** - * An interval id for the battery check interval. - * @type {number} - * @private - */ - this._batteryLevelIntervalId = null; - this.reset = this.reset.bind(this); this._onConnect = this._onConnect.bind(this); this._onMessage = this._onMessage.bind(this); @@ -441,21 +403,21 @@ class WeDo2 { } /** - * @returns {number} - the latest value received for the tilt sensor's tilt about the X axis. + * @returns the latest value received for the tilt sensor's tilt about the X axis. */ get tiltX () { return this._sensors.tiltX; } /** - * @returns {number} - the latest value received for the tilt sensor's tilt about the Y axis. + * @returns the latest value received for the tilt sensor's tilt about the Y axis. */ get tiltY () { return this._sensors.tiltY; } /** - * @returns {number} - the latest value received from the distance sensor. + * @returns the latest value received from the distance sensor. */ get distance () { return this._sensors.distance; @@ -463,10 +425,10 @@ class WeDo2 { /** * Access a particular motor on this peripheral. - * @param {int} index - the zero-based index of the desired motor. - * @returns {WeDo2Motor} - the WeDo2Motor instance, if any, at that index. + * @param index - the zero-based index of the desired motor. + * @returns the WeDo2Motor instance, if any, at that index. */ - motor (index) { + motor (index: number) { return this._motors[index]; } @@ -486,10 +448,10 @@ class WeDo2 { /** * Set the WeDo 2.0 peripheral's LED to a specific color. - * @param {int} inputRGB - a 24-bit RGB color in 0xRRGGBB format. - * @returns {Promise} - a promise of the completion of the set led send operation. + * @param inputRGB - a 24-bit RGB color in 0xRRGGBB format. + * @returns a promise of the completion of the set led send operation. */ - setLED (inputRGB) { + setLED (inputRGB: number) { const rgb = [ (inputRGB >> 16) & 0x000000FF, (inputRGB >> 8) & 0x000000FF, @@ -507,7 +469,7 @@ class WeDo2 { /** * Sets the input mode of the LED to RGB. - * @returns {Promise} - a promise returned by the send operation. + * @returns a promise returned by the send operation. */ setLEDMode () { const cmd = this.generateInputCommand( @@ -524,7 +486,7 @@ class WeDo2 { /** * Switch off the LED on the WeDo 2.0. - * @returns {Promise} - a promise of the completion of the stop led send operation. + * @returns a promise of the completion of the stop led send operation. */ stopLED () { const cmd = this.generateOutputCommand( @@ -538,11 +500,11 @@ class WeDo2 { /** * Play a tone from the WeDo 2.0 peripheral for a specific amount of time. - * @param {int} tone - the pitch of the tone, in Hz. - * @param {int} milliseconds - the duration of the note, in milliseconds. - * @returns {Promise} - a promise of the completion of the play tone send operation. + * @param tone - the pitch of the tone, in Hz. + * @param milliseconds - the duration of the note, in milliseconds. + * @returns a promise of the completion of the play tone send operation. */ - playTone (tone, milliseconds) { + playTone (tone: number, milliseconds: number) { const cmd = this.generateOutputCommand( WeDo2ConnectID.PIEZO, WeDo2Command.PLAY_TONE, @@ -559,7 +521,7 @@ class WeDo2 { /** * Stop the tone playing from the WeDo 2.0 peripheral, if any. - * @returns {Promise} - a promise that the command sent. + * @returns a promise that the command sent. */ stopTone () { const cmd = this.generateOutputCommand( @@ -598,9 +560,9 @@ class WeDo2 { /** * Called by the runtime when user wants to connect to a certain WeDo 2.0 peripheral. - * @param {number} id - the id of the peripheral to connect to. + * @param id - the id of the peripheral to connect to. */ - connect (id) { + connect (id: number) { if (this._ble) { this._ble.connectPeripheral(id); } @@ -621,7 +583,7 @@ class WeDo2 { * Reset all the state and timeout/interval ids. */ reset () { - this._ports = ['none', 'none']; + this._ports = [0, 0]; this._motors = [null, null]; this._sensors = { tiltX: 0, @@ -637,7 +599,7 @@ class WeDo2 { /** * Called by the runtime to detect whether the WeDo 2.0 peripheral is connected. - * @returns {boolean} - the connected state. + * @returns the connected state. */ isConnected () { let connected = false; @@ -649,19 +611,19 @@ class WeDo2 { /** * Write a message to the WeDo 2.0 peripheral BLE socket. - * @param {number} uuid - the UUID of the characteristic to write to - * @param {Array} message - the message to write. - * @param {boolean} [useLimiter] - if true, use the rate limiter - * @returns {Promise} - a promise result of the write operation + * @param uuid - the UUID of the characteristic to write to + * @param message - the message to write. + * @param useLimiter - if true, use the rate limiter + * @returns a promise result of the write operation */ - send (uuid, message, useLimiter = true) { + send (uuid: string, message: number[], useLimiter = true) { if (!this.isConnected()) return Promise.resolve(); if (useLimiter) { if (!this._rateLimiter.okayToSend()) return Promise.resolve(); } - return this._ble.write( + return this._ble!.write( BLEService.IO_SERVICE, uuid, Base64Util.uint8ArrayToBase64(message), @@ -675,12 +637,12 @@ class WeDo2 { * * This sends a command to the WeDo 2.0 to actuate the specified outputs. * - * @param {number} connectID - the port (Connect ID) to send a command to. - * @param {number} commandID - the id of the byte command. - * @param {Array} values - the list of values to write to the command. - * @returns {Array} - a generated output command. + * @param connectID - the port (Connect ID) to send a command to. + * @param commandID - the id of the byte command. + * @param values - the list of values to write to the command. + * @returns - a generated output command. */ - generateOutputCommand (connectID, commandID, values = null) { + generateOutputCommand (connectID: number, commandID: number, values: number[] | null = null): number[] { let command = [connectID, commandID]; if (values) { command = command.concat( @@ -700,15 +662,22 @@ class WeDo2 { * This sends a command to the WeDo 2.0 that sets that input format * of the specified inputs and sets value change notifications. * - * @param {number} connectID - the port (Connect ID) to send a command to. - * @param {number} type - the type of input sensor. - * @param {number} mode - the mode of the input sensor. - * @param {number} delta - the delta change needed to trigger notification. - * @param {Array} units - the unit of the input sensor value. - * @param {boolean} enableNotifications - whether to enable notifications. - * @returns {Array} - a generated input command. - */ - generateInputCommand (connectID, type, mode, delta, units, enableNotifications) { + * @param connectID - the port (Connect ID) to send a command to. + * @param type - the type of input sensor. + * @param mode - the mode of the input sensor. + * @param delta - the delta change needed to trigger notification. + * @param units - the unit of the input sensor value. + * @param enableNotifications - whether to enable notifications. + * @returns - a generated input command. + */ + generateInputCommand ( + connectID: number, + type: number, + mode: number, + delta: number, + units: number, + enableNotifications: boolean + ) { const command = [ 1, // Command ID = 1 = "Sensor Format" 2, // Command Type = 2 = "Write" @@ -728,12 +697,11 @@ class WeDo2 { /** * Sets LED mode and initial color and starts reading data from peripheral after BLE has connected. - * @private */ - _onConnect () { + private _onConnect () { this.setLEDMode(); this.setLED(0x0000FF); - this._ble.startNotifications( + this._ble!.startNotifications( BLEService.DEVICE_SERVICE, BLECharacteristic.ATTACHED_IO, this._onMessage @@ -743,10 +711,9 @@ class WeDo2 { /** * Process the sensor data from the incoming BLE characteristic. - * @param {object} base64 - the incoming BLE data. - * @private + * @param base64 - the incoming BLE data. */ - _onMessage (base64) { + private _onMessage (base64: string) { const data = Base64Util.base64ToUint8Array(base64); // log.info(data); @@ -790,8 +757,8 @@ class WeDo2 { * for some reason, the BLE socket will get an error back and automatically * close the socket. */ - _checkBatteryLevel () { - this._ble.read( + private _checkBatteryLevel () { + this._ble!.read( BLEService.DEVICE_SERVICE, BLECharacteristic.LOW_VOLTAGE_ALERT, false @@ -802,11 +769,10 @@ class WeDo2 { * Register a new sensor or motor connected at a port. Store the type of * sensor or motor internally, and then register for notifications on input * values if it is a sensor. - * @param {number} connectID - the port to register a sensor or motor on. - * @param {number} type - the type ID of the sensor or motor - * @private + * @param connectID - the port to register a sensor or motor on. + * @param type - the type ID of the sensor or motor */ - _registerSensorOrMotor (connectID, type) { + private _registerSensorOrMotor (connectID: number, type: number) { // Record which port is connected to what type of device this._ports[connectID - 1] = type; @@ -815,7 +781,7 @@ class WeDo2 { this._motors[connectID - 1] = new WeDo2Motor(this, connectID - 1); } else { // Set input format for tilt or distance sensor - const typeString = type === WeDo2Device.DISTANCE ? 'DISTANCE' : 'TILT'; + const typeString = type === WeDo2Device.DISTANCE ? 'DISTANCE' as const : 'TILT' as const; const cmd = this.generateInputCommand( connectID, type, @@ -826,7 +792,7 @@ class WeDo2 { ); this.send(BLECharacteristic.INPUT_COMMAND, cmd); - this._ble.startNotifications( + this._ble!.startNotifications( BLEService.IO_SERVICE, BLECharacteristic.INPUT_VALUES, this._onMessage @@ -836,10 +802,9 @@ class WeDo2 { /** * Clear the sensor or motor present at port 1 or 2. - * @param {number} connectID - the port to clear. - * @private + * @param connectID - the port to clear. */ - _clearPort (connectID) { + private _clearPort (connectID: number) { const type = this._ports[connectID - 1]; if (type === WeDo2Device.TILT) { this._sensors.tiltX = this._sensors.tiltY = 0; @@ -847,38 +812,32 @@ class WeDo2 { if (type === WeDo2Device.DISTANCE) { this._sensors.distance = 0; } - this._ports[connectID - 1] = 'none'; + this._ports[connectID - 1] = 0; this._motors[connectID - 1] = null; } } /** * Enum for motor specification. - * @readonly - * @enum {string} */ const WeDo2MotorLabel = { DEFAULT: 'motor', A: 'motor A', B: 'motor B', ALL: 'all motors' -}; +} as const; /** * Enum for motor direction specification. - * @readonly - * @enum {string} */ const WeDo2MotorDirection = { FORWARD: 'this way', BACKWARD: 'that way', REVERSE: 'reverse' -}; +} as const; /** * Enum for tilt sensor direction. - * @readonly - * @enum {string} */ const WeDo2TiltDirection = { UP: 'up', @@ -886,22 +845,73 @@ const WeDo2TiltDirection = { LEFT: 'left', RIGHT: 'right', ANY: 'any' -}; +} as const; + +interface MotorOnForArgs { + MOTOR_ID: string; + DURATION: unknown; +} + +interface MotorOnArgs { + MOTOR_ID: string; +} + +interface MotorOffArgs { + MOTOR_ID: string; +} + +interface StartMotorPowerArgs { + MOTOR_ID: string; + POWER: unknown; +} + +interface SetMotorDirectionArgs { + MOTOR_ID: string; + MOTOR_DIRECTION: unknown; +} + +interface SetLightHueArgs { + HUE: unknown; +} + +interface PlayNoteForArgs { + NOTE: unknown; + DURATION: unknown; +} + +interface WhenDistanceArgs { + OP: unknown; + REFERENCE: unknown; +} + +interface WhenTiltedArgs { + TILT_DIRECTION_ANY: string; +} + +interface GetTiltAngleArgs { + TILT_DIRECTION: string; +} /** * Scratch 3.0 blocks to interact with a LEGO WeDo 2.0 peripheral. */ -class Scratch3WeDo2Blocks { +class Scratch3WeDo2Blocks implements ExtensionClass { + /** + * The Scratch 3.0 runtime. + */ + runtime: Runtime; + + private _peripheral: WeDo2; /** - * @returns {string} - the ID of this extension. + * @returns the ID of this extension. */ static get EXTENSION_ID () { - return 'wedo2'; + return 'wedo2' as const; } /** - * @returns {number} - the tilt sensor counts as "tilted" if its tilt angle meets or exceeds this threshold. + * @returns the tilt sensor counts as "tilted" if its tilt angle meets or exceeds this threshold. */ static get TILT_THRESHOLD () { return 15; @@ -909,13 +919,9 @@ class Scratch3WeDo2Blocks { /** * Construct a set of WeDo 2.0 blocks. - * @param {Runtime} runtime - the Scratch 3.0 runtime. + * @param runtime - the Scratch 3.0 runtime. */ - constructor (runtime) { - /** - * The Scratch 3.0 runtime. - * @type {Runtime} - */ + constructor (runtime: Runtime) { this.runtime = runtime; // Create a new WeDo 2.0 peripheral instance @@ -923,7 +929,7 @@ class Scratch3WeDo2Blocks { } /** - * @returns {object} metadata for this extension and its blocks. + * @returns metadata for this extension and its blocks. */ getInfo () { return { @@ -1292,21 +1298,19 @@ class Scratch3WeDo2Blocks { items: ['<', '>'] } } - }; + } as ExtensionMetadata; } /** * Turn specified motor(s) on for a specified duration. - * @param {object} args - the block's arguments. - * @property {MotorID} MOTOR_ID - the motor(s) to activate. - * @property {int} DURATION - the amount of time to run the motors. - * @returns {Promise} - a promise which will resolve at the end of the duration. + * @param args - the block's arguments. + * @returns a promise which will resolve at the end of the duration. */ - motorOnFor (args) { + motorOnFor (args: MotorOnForArgs) { // TODO: cast args.MOTOR_ID? let durationMS = Cast.toNumber(args.DURATION) * 1000; durationMS = MathUtil.clamp(durationMS, 0, 15000); - return new Promise(resolve => { + return new Promise(resolve => { this._forEachMotor(args.MOTOR_ID, motorIndex => { const motor = this._peripheral.motor(motorIndex); if (motor) { @@ -1321,11 +1325,10 @@ class Scratch3WeDo2Blocks { /** * Turn specified motor(s) on indefinitely. - * @param {object} args - the block's arguments. - * @property {MotorID} MOTOR_ID - the motor(s) to activate. - * @returns {Promise} - a Promise that resolves after some delay. + * @param args - the block's arguments. + * @returns a Promise that resolves after some delay. */ - motorOn (args) { + motorOn (args: MotorOnArgs) { // TODO: cast args.MOTOR_ID? this._forEachMotor(args.MOTOR_ID, motorIndex => { const motor = this._peripheral.motor(motorIndex); @@ -1334,7 +1337,7 @@ class Scratch3WeDo2Blocks { } }); - return new Promise(resolve => { + return new Promise(resolve => { window.setTimeout(() => { resolve(); }, BLESendInterval); @@ -1343,11 +1346,10 @@ class Scratch3WeDo2Blocks { /** * Turn specified motor(s) off. - * @param {object} args - the block's arguments. - * @property {MotorID} MOTOR_ID - the motor(s) to deactivate. - * @returns {Promise} - a Promise that resolves after some delay. + * @param args - the block's arguments. + * @returns a Promise that resolves after some delay. */ - motorOff (args) { + motorOff (args: MotorOffArgs) { // TODO: cast args.MOTOR_ID? this._forEachMotor(args.MOTOR_ID, motorIndex => { const motor = this._peripheral.motor(motorIndex); @@ -1356,7 +1358,7 @@ class Scratch3WeDo2Blocks { } }); - return new Promise(resolve => { + return new Promise(resolve => { window.setTimeout(() => { resolve(); }, BLESendInterval); @@ -1365,12 +1367,10 @@ class Scratch3WeDo2Blocks { /** * Turn specified motor(s) off. - * @param {object} args - the block's arguments. - * @property {MotorID} MOTOR_ID - the motor(s) to be affected. - * @property {int} POWER - the new power level for the motor(s). - * @returns {Promise} - a Promise that resolves after some delay. + * @param args - the block's arguments. + * @returns a Promise that resolves after some delay. */ - startMotorPower (args) { + startMotorPower (args: StartMotorPowerArgs) { // TODO: cast args.MOTOR_ID? this._forEachMotor(args.MOTOR_ID, motorIndex => { const motor = this._peripheral.motor(motorIndex); @@ -1380,7 +1380,7 @@ class Scratch3WeDo2Blocks { } }); - return new Promise(resolve => { + return new Promise(resolve => { window.setTimeout(() => { resolve(); }, BLESendInterval); @@ -1390,12 +1390,10 @@ class Scratch3WeDo2Blocks { /** * Set the direction of rotation for specified motor(s). * If the direction is 'reverse' the motor(s) will be reversed individually. - * @param {object} args - the block's arguments. - * @property {MotorID} MOTOR_ID - the motor(s) to be affected. - * @property {MotorDirection} MOTOR_DIRECTION - the new direction for the motor(s). - * @returns {Promise} - a Promise that resolves after some delay. + * @param args - the block's arguments. + * @returns a Promise that resolves after some delay. */ - setMotorDirection (args) { + setMotorDirection (args: SetMotorDirectionArgs) { // TODO: cast args.MOTOR_ID? this._forEachMotor(args.MOTOR_ID, motorIndex => { const motor = this._peripheral.motor(motorIndex); @@ -1411,13 +1409,14 @@ class Scratch3WeDo2Blocks { motor.direction = -motor.direction; break; default: - log.warn(`Unknown motor direction in setMotorDirection: ${args.DIRECTION}`); + log.warn(`Unknown motor direction in setMotorDirection: ${args.MOTOR_DIRECTION}`); break; } // keep the motor on if it's running, and update the pending timeout if needed if (motor.isOn) { - if (motor.pendingTimeoutDelay) { - motor.turnOnFor(motor.pendingTimeoutStartTime + motor.pendingTimeoutDelay - Date.now()); + // eslint-disable-next-line no-negated-condition + if (motor.pendingTimeoutDelay !== null) { + motor.turnOnFor(motor.pendingTimeoutStartTime! + motor.pendingTimeoutDelay - Date.now()); } else { motor.turnOn(); } @@ -1425,7 +1424,7 @@ class Scratch3WeDo2Blocks { } }); - return new Promise(resolve => { + return new Promise(resolve => { window.setTimeout(() => { resolve(); }, BLESendInterval); @@ -1434,11 +1433,10 @@ class Scratch3WeDo2Blocks { /** * Set the LED's hue. - * @param {object} args - the block's arguments. - * @property {number} HUE - the hue to set, in the range [0,100]. - * @returns {Promise} - a Promise that resolves after some delay. + * @param args - the block's arguments. + * @returns a Promise that resolves after some delay. */ - setLightHue (args) { + setLightHue (args: SetLightHueArgs) { // Convert from [0,100] to [0,360] let inputHue = Cast.toNumber(args.HUE); inputHue = MathUtil.wrapClamp(inputHue, 0, 100); @@ -1450,7 +1448,7 @@ class Scratch3WeDo2Blocks { this._peripheral.setLED(rgbDecimal); - return new Promise(resolve => { + return new Promise(resolve => { window.setTimeout(() => { resolve(); }, BLESendInterval); @@ -1459,17 +1457,15 @@ class Scratch3WeDo2Blocks { /** * Make the WeDo 2.0 peripheral play a MIDI note for the specified duration. - * @param {object} args - the block's arguments. - * @property {number} NOTE - the MIDI note to play. - * @property {number} DURATION - the duration of the note, in seconds. - * @returns {Promise} - a promise which will resolve at the end of the duration. + * @param args - the block's arguments. + * @returns a promise which will resolve at the end of the duration. */ - playNoteFor (args) { + playNoteFor (args: PlayNoteForArgs) { let durationMS = Cast.toNumber(args.DURATION) * 1000; durationMS = MathUtil.clamp(durationMS, 0, 3000); const note = MathUtil.clamp(Cast.toNumber(args.NOTE), 25, 125); // valid WeDo 2.0 sounds if (durationMS === 0) return; // WeDo 2.0 plays duration '0' forever - return new Promise(resolve => { + return new Promise(resolve => { const tone = this._noteToTone(note); this._peripheral.playTone(tone, durationMS); @@ -1480,12 +1476,10 @@ class Scratch3WeDo2Blocks { /** * Compare the distance sensor's value to a reference. - * @param {object} args - the block's arguments. - * @property {string} OP - the comparison operation: '<' or '>'. - * @property {number} REFERENCE - the value to compare against. - * @returns {boolean} - the result of the comparison, or false on error. + * @param args - the block's arguments. + * @returns the result of the comparison, or false on error. */ - whenDistance (args) { + whenDistance (args: WhenDistanceArgs) { switch (args.OP) { case '<': return this._peripheral.distance < Cast.toNumber(args.REFERENCE); @@ -1499,16 +1493,15 @@ class Scratch3WeDo2Blocks { /** * Test whether the tilt sensor is currently tilted. - * @param {object} args - the block's arguments. - * @property {TiltDirection} TILT_DIRECTION_ANY - the tilt direction to test (up, down, left, right, or any). - * @returns {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction. + * @param args - the block's arguments. + * @returns true if the tilt sensor is tilted past a threshold in the specified direction. */ - whenTilted (args) { + whenTilted (args: WhenTiltedArgs) { return this._isTilted(args.TILT_DIRECTION_ANY); } /** - * @returns {number} - the distance sensor's value, scaled to the [0,100] range. + * @returns the distance sensor's value, scaled to the [0,100] range. */ getDistance () { return this._peripheral.distance; @@ -1516,31 +1509,28 @@ class Scratch3WeDo2Blocks { /** * Test whether the tilt sensor is currently tilted. - * @param {object} args - the block's arguments. - * @property {TiltDirection} TILT_DIRECTION_ANY - the tilt direction to test (up, down, left, right, or any). - * @returns {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction. + * @param args - the block's arguments. + * @returns true if the tilt sensor is tilted past a threshold in the specified direction. */ - isTilted (args) { + isTilted (args: WhenTiltedArgs) { return this._isTilted(args.TILT_DIRECTION_ANY); } /** - * @param {object} args - the block's arguments. - * @property {TiltDirection} TILT_DIRECTION - the direction (up, down, left, right) to check. - * @returns {number} - the tilt sensor's angle in the specified direction. + * @param args - the block's arguments. + * @returns the tilt sensor's angle in the specified direction. * Note that getTiltAngle(up) = -getTiltAngle(down) and getTiltAngle(left) = -getTiltAngle(right). */ - getTiltAngle (args) { + getTiltAngle (args: GetTiltAngleArgs) { return this._getTiltAngle(args.TILT_DIRECTION); } /** * Test whether the tilt sensor is currently tilted. - * @param {TiltDirection} direction - the tilt direction to test (up, down, left, right, or any). - * @returns {boolean} - true if the tilt sensor is tilted past a threshold in the specified direction. - * @private + * @param direction - the tilt direction to test (up, down, left, right, or any). + * @returns true if the tilt sensor is tilted past a threshold in the specified direction. */ - _isTilted (direction) { + private _isTilted (direction: string) { switch (direction) { case WeDo2TiltDirection.ANY: return this._getTiltAngle(WeDo2TiltDirection.UP) >= Scratch3WeDo2Blocks.TILT_THRESHOLD || @@ -1553,12 +1543,11 @@ class Scratch3WeDo2Blocks { } /** - * @param {TiltDirection} direction - the direction (up, down, left, right) to check. - * @returns {number} - the tilt sensor's angle in the specified direction. + * @param direction - the direction (up, down, left, right) to check. + * @returns the tilt sensor's angle in the specified direction. * Note that getTiltAngle(up) = -getTiltAngle(down) and getTiltAngle(left) = -getTiltAngle(right). - * @private */ - _getTiltAngle (direction) { + private _getTiltAngle (direction: string) { switch (direction) { case WeDo2TiltDirection.UP: return this._peripheral.tiltY > 45 ? 256 - this._peripheral.tiltY : -this._peripheral.tiltY; @@ -1570,17 +1559,17 @@ class Scratch3WeDo2Blocks { return this._peripheral.tiltX > 45 ? this._peripheral.tiltX - 256 : this._peripheral.tiltX; default: log.warn(`Unknown tilt direction in _getTiltAngle: ${direction}`); + return 0; } } /** * Call a callback for each motor indexed by the provided motor ID. - * @param {MotorID} motorID - the ID specifier. - * @param {Function} callback - the function to call with the numeric motor index for each motor. - * @private + * @param motorID - the ID specifier. + * @param callback - the function to call with the numeric motor index for each motor. */ - _forEachMotor (motorID, callback) { - let motors; + private _forEachMotor (motorID: string, callback: (index: number) => void) { + let motors: number[]; switch (motorID) { case WeDo2MotorLabel.A: motors = [0]; @@ -1603,11 +1592,10 @@ class Scratch3WeDo2Blocks { } /** - * @param {number} midiNote - the MIDI note value to convert. - * @returns {number} - the frequency, in Hz, corresponding to that MIDI note value. - * @private + * @param midiNote - the MIDI note value to convert. + * @returns the frequency, in Hz, corresponding to that MIDI note value. */ - _noteToTone (midiNote) { + private _noteToTone (midiNote: number) { // Note that MIDI note 69 is A4, 440 Hz return 440 * Math.pow(2, (midiNote - 69) / 12); } diff --git a/packages/vm/src/import/load-costume.js b/packages/vm/src/import/load-costume.ts similarity index 77% rename from packages/vm/src/import/load-costume.js rename to packages/vm/src/import/load-costume.ts index a95a341a5..1cd37546d 100644 --- a/packages/vm/src/import/load-costume.js +++ b/packages/vm/src/import/load-costume.ts @@ -1,8 +1,16 @@ import StringUtil from '../util/string-util'; import log from '../util/log'; import {loadSvgString, serializeSvgToString} from 'clipcc-svg-renderer'; - -const loadVector_ = function (costume, runtime, rotationCenter, optVersion) { +import type {Costume} from '../sprites/sprite'; +import type Runtime from '../engine/runtime'; +import type {DataFormat} from 'clipcc-storage'; + +const loadVector_ = function ( + costume: Costume, + runtime: Runtime, + rotationCenter?: [number, number], + optVersion?: number +): Promise { return new Promise(resolve => { let svgString = costume.asset.decodeText(); // SVG Renderer load fixes "quirks" associated with Scratch 2 projects @@ -14,13 +22,15 @@ const loadVector_ = function (costume, runtime, rotationCenter, optVersion) { // If the string changed, put back into storage if (svgString !== fixedSvgString) { svgString = fixedSvgString; - const storage = runtime.storage; + const {storage} = runtime; + if (!storage) throw new Error('No storage present on runtime'); costume.asset.encodeTextData(fixedSvgString, storage.DataFormat.SVG, true); - costume.assetId = costume.asset.assetId; + costume.assetId = costume.asset.assetId!; costume.md5 = `${costume.assetId}.${costume.dataFormat}`; } } + if (!runtime.renderer) throw new Error('No renderer present on runtime'); // createSVGSkin does the right thing if rotationCenter isn't provided, so it's okay if it's // undefined here costume.skinId = runtime.renderer.createSVGSkin(svgString, rotationCenter); @@ -44,10 +54,8 @@ const canvasPool = (function () { * collection. */ class CanvasPool { - constructor () { - this.pool = []; - this.clearSoon = null; - } + pool: HTMLCanvasElement[] = []; + clearSoon: Promise | null = null; /** * After a short wait period clear the pool to let the VM collect @@ -65,7 +73,7 @@ const canvasPool = (function () { /** * Return a canvas. Create the canvas if the pool is empty. - * @returns {HTMLCanvasElement} A canvas element. + * @returns A canvas element. */ create () { return this.pool.pop() || document.createElement('canvas'); @@ -73,9 +81,9 @@ const canvasPool = (function () { /** * Release the canvas to be reused. - * @param {HTMLCanvasElement} canvas A canvas element. + * @param canvas A canvas element. */ - release (canvas) { + release (canvas: HTMLCanvasElement) { this.clear(); this.pool.push(canvas); } @@ -89,16 +97,15 @@ const canvasPool = (function () { * If the costume has bitmapResolution 1, it will be converted to bitmapResolution 2 here (the standard for Scratch 3) * If the costume has a text layer asset, which is a text part from Scratch 1.4, then this function * will merge the two image assets. See the issue LLK/scratch-vm#672 for more information. - * @param {!object} costume - the Scratch costume object. - * @param {!Runtime} runtime - Scratch runtime, used to access the v2BitmapAdapter - * @param {?object} rotationCenter - optionally passed in coordinates for the center of rotation for the image. If + * @param costume - the Scratch costume object. + * @param runtime - Scratch runtime, used to access the v2BitmapAdapter + * @param rotationCenter - optionally passed in coordinates for the center of rotation for the image. If * none is given, the rotation center of the costume will be set to the middle of the costume later on. - * @property {number} costume.bitmapResolution - the resolution scale for a bitmap costume. - * @returns {?Promise} - a promise which will resolve to an object {canvas, rotationCenter, assetMatchesBase}, + * @returns a promise which will resolve to an object {canvas, rotationCenter, assetMatchesBase}, * or reject on error. * assetMatchesBase is true if the asset matches the base layer; false if it required adjustment */ -const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) { +const fetchBitmapCanvas_ = function (costume: Costume, runtime: Runtime, rotationCenter?: [number, number]) { if (!costume || !costume.asset) { // TODO: We can probably remove this check... return Promise.reject('Costume load failed. Assets were missing.'); } @@ -113,11 +120,11 @@ const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) { if (typeof createImageBitmap !== 'undefined') { return createImageBitmap( - new Blob([asset.data], {type: asset.assetType.contentType}) + new Blob([asset.data as unknown as ArrayBuffer], {type: asset.assetType.contentType}) ); } - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { const image = new Image(); image.onload = function () { resolve(image); @@ -136,11 +143,11 @@ const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) { const mergeCanvas = canvasPool.create(); const scale = costume.bitmapResolution === 1 ? 2 : 1; - mergeCanvas.width = baseImageElement.width; - mergeCanvas.height = baseImageElement.height; + mergeCanvas.width = baseImageElement!.width; + mergeCanvas.height = baseImageElement!.height; - const ctx = mergeCanvas.getContext('2d'); - ctx.drawImage(baseImageElement, 0, 0); + const ctx = mergeCanvas.getContext('2d')!; + ctx.drawImage(baseImageElement!, 0, 0); if (textImageElement) { ctx.drawImage(textImageElement, 0, 0); } @@ -152,7 +159,7 @@ const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) { // resize may cause errors. let canvas = mergeCanvas; if (scale !== 1) { - canvas = runtime.v2BitmapAdapter.resize(mergeCanvas, canvas.width * scale, canvas.height * scale); + canvas = runtime.v2BitmapAdapter!.resize(mergeCanvas, canvas.width * scale, canvas.height * scale); } // By scaling, we've converted it to bitmap resolution 2 @@ -183,10 +190,10 @@ const fetchBitmapCanvas_ = function (costume, runtime, rotationCenter) { }); }; -const loadBitmap_ = function (costume, runtime, _rotationCenter) { +const loadBitmap_ = function (costume: Costume, runtime: Runtime, _rotationCenter?: [number, number]) { return fetchBitmapCanvas_(costume, runtime, _rotationCenter) .then(fetched => { - const updateCostumeAsset = function (dataURI) { + const updateCostumeAsset = function (dataURI: string) { if (!runtime.v2BitmapAdapter) { // TODO: This might be a bad practice since the returned // promise isn't acted on. If this is something we should be @@ -198,6 +205,7 @@ const loadBitmap_ = function (costume, runtime, _rotationCenter) { } const storage = runtime.storage; + if (!storage) throw new Error('No storage present on runtime'); costume.asset = storage.createAsset( storage.AssetType.ImageBitmap, storage.DataFormat.PNG, @@ -206,7 +214,7 @@ const loadBitmap_ = function (costume, runtime, _rotationCenter) { true // generate md5 ); costume.dataFormat = storage.DataFormat.PNG; - costume.assetId = costume.asset.assetId; + costume.assetId = costume.asset.assetId!; costume.md5 = `${costume.assetId}.${costume.dataFormat}`; }; @@ -231,6 +239,7 @@ const loadBitmap_ = function (costume, runtime, _rotationCenter) { // TODO: costume.bitmapResolution will always be 2 at this point because of fetchBitmapCanvas_, so we don't // need to pass it in here. + if (!runtime.renderer) throw new Error('No renderer present on runtime'); costume.skinId = runtime.renderer.createBitmapSkin(canvas, costume.bitmapResolution, center); canvasPool.release(mergeCanvas); const renderSize = runtime.renderer.getSkinSize(costume.skinId); @@ -250,7 +259,7 @@ const loadBitmap_ = function (costume, runtime, _rotationCenter) { // Handle all manner of costume errors with a Gray Question Mark (default costume) // and preserve as much of the original costume data as possible // Returns a promise of a costume -const handleCostumeLoadError = function (costume, runtime) { +const handleCostumeLoadError = function (costume: Costume, runtime: Runtime) { // Keep track of the old asset information until we're done loading the default costume const oldAsset = costume.asset; // could be null const oldAssetId = costume.assetId; @@ -259,6 +268,7 @@ const handleCostumeLoadError = function (costume, runtime) { const oldBitmapResolution = costume.bitmapResolution; const oldDataFormat = costume.dataFormat; + if (!runtime.storage) throw new Error('No storage present on runtime'); const AssetType = runtime.storage.AssetType; const isVector = costume.dataFormat === AssetType.ImageVector.runtimeFormat; @@ -266,24 +276,22 @@ const handleCostumeLoadError = function (costume, runtime) { costume.assetId = isVector ? runtime.storage.defaultAssetId.ImageVector : runtime.storage.defaultAssetId.ImageBitmap; - costume.asset = runtime.storage.get(costume.assetId); + costume.asset = runtime.storage.get(costume.assetId)!; costume.md5 = `${costume.assetId}.${costume.asset.dataFormat}`; const defaultCostumePromise = (isVector) ? loadVector_(costume, runtime) : loadBitmap_(costume, runtime); return defaultCostumePromise.then(loadedCostume => { - loadedCostume.broken = {}; - loadedCostume.broken.assetId = oldAssetId; - loadedCostume.broken.md5 = `${oldAssetId}.${oldDataFormat}`; - - // Should be null if we got here because the costume was missing - loadedCostume.broken.asset = oldAsset; - loadedCostume.broken.dataFormat = oldDataFormat; - - loadedCostume.broken.rotationCenterX = oldRotationX; - loadedCostume.broken.rotationCenterY = oldRotationY; - loadedCostume.broken.bitmapResolution = oldBitmapResolution; + loadedCostume.broken = { + asset: oldAsset, + assetId: oldAssetId, + md5: `${oldAssetId}.${oldDataFormat}`, + dataFormat: oldDataFormat, + rotationCenterX: oldRotationX, + rotationCenterY: oldRotationY, + bitmapResolution: oldBitmapResolution + }; return loadedCostume; }); }; @@ -291,26 +299,25 @@ const handleCostumeLoadError = function (costume, runtime) { /** * Initialize a costume from an asset asynchronously. * Do not call this unless there is a renderer attached. - * @param {!object} costume - the Scratch costume object. - * @property {int} skinId - the ID of the costume's render skin, once installed. - * @property {number} rotationCenterX - the X component of the costume's origin. - * @property {number} rotationCenterY - the Y component of the costume's origin. - * @property {number} [bitmapResolution] - the resolution scale for a bitmap costume. - * @property {!Asset} costume.asset - the asset of the costume loaded from storage. - * @param {!Runtime} runtime - Scratch runtime, used to access the storage module. - * @param {?int} [optVersion] - Version of Scratch that the costume comes from. If this is set + * @param costume - the Scratch costume object. + * @param runtime - Scratch runtime, used to access the storage module. + * @param optVersion - Version of Scratch that the costume comes from. If this is set * to 2, scratch 3 will perform an upgrade step to handle quirks in SVGs from Scratch 2.0. - * @returns {Promise} - a promise which will resolve after skinId is set, or null on error. + * @returns a promise which will resolve after skinId is set, or null on error. */ -const loadCostumeFromAsset = function (costume, runtime, optVersion) { - costume.assetId = costume.asset.assetId; +const loadCostumeFromAsset = function (costume: Costume, runtime: Runtime, optVersion?: number) { + costume.assetId = costume.asset.assetId!; const renderer = runtime.renderer; if (!renderer) { log.warn('No rendering module present; cannot load costume: ', costume.name); return Promise.resolve(costume); } + if (!runtime.storage) { + log.warn('No storage module present; cannot load costume asset: ', costume.assetId); + return Promise.resolve(costume); + } const AssetType = runtime.storage.AssetType; - let rotationCenter; + let rotationCenter!: [number, number]; // Use provided rotation center and resolution if they are defined. Bitmap resolution // should only ever be 1 or 2. if (typeof costume.rotationCenterX === 'number' && !isNaN(costume.rotationCenterX) && @@ -325,7 +332,7 @@ const loadCostumeFromAsset = function (costume, runtime, optVersion) { }); } - return loadBitmap_(costume, runtime, rotationCenter, optVersion) + return loadBitmap_(costume, runtime, rotationCenter) .catch(error => { log.warn(`Error loading bitmap image: ${error}`); return handleCostumeLoadError(costume, runtime); @@ -336,21 +343,17 @@ const loadCostumeFromAsset = function (costume, runtime, optVersion) { /** * Load a costume's asset into memory asynchronously. * Do not call this unless there is a renderer attached. - * @param {!string} md5ext - the MD5 and extension of the costume to be loaded. - * @param {!object} costume - the Scratch costume object. - * @property {int} skinId - the ID of the costume's render skin, once installed. - * @property {number} rotationCenterX - the X component of the costume's origin. - * @property {number} rotationCenterY - the Y component of the costume's origin. - * @property {number} [bitmapResolution] - the resolution scale for a bitmap costume. - * @param {!Runtime} runtime - Scratch runtime, used to access the storage module. - * @param {?int} optVersion - Version of Scratch that the costume comes from. If this is set + * @param md5ext - the MD5 and extension of the costume to be loaded. + * @param costume - the Scratch costume object. + * @param runtime - Scratch runtime, used to access the storage module. + * @param optVersion - Version of Scratch that the costume comes from. If this is set * to 2, scratch 3 will perform an upgrade step to handle quirks in SVGs from Scratch 2.0. - * @returns {?Promise} - a promise which will resolve after skinId is set, or null on error. + * @returns a promise which will resolve after skinId is set, or null on error. */ -const loadCostume = function (md5ext, costume, runtime, optVersion) { +const loadCostume = function (md5ext: string, costume: Costume, runtime: Runtime, optVersion?: number) { const idParts = StringUtil.splitFirst(md5ext, '.'); const md5 = idParts[0]; - const ext = idParts[1].toLowerCase(); + const ext = idParts[1]!.toLowerCase() as DataFormat; costume.dataFormat = ext; if (costume.asset) { @@ -376,7 +379,7 @@ const loadCostume = function (md5ext, costume, runtime, optVersion) { let textLayerPromise; if (costume.textLayerMD5) { - textLayerPromise = runtime.storage.load(AssetType.ImageBitmap, costume.textLayerMD5, 'png'); + textLayerPromise = runtime.storage.load(AssetType.ImageBitmap, costume.textLayerMD5, 'png' as DataFormat); } else { textLayerPromise = Promise.resolve(null); } diff --git a/packages/vm/src/import/load-sound.js b/packages/vm/src/import/load-sound.ts similarity index 58% rename from packages/vm/src/import/load-sound.js rename to packages/vm/src/import/load-sound.ts index 9af3779e9..691ce8acb 100644 --- a/packages/vm/src/import/load-sound.js +++ b/packages/vm/src/import/load-sound.ts @@ -1,18 +1,19 @@ import StringUtil from '../util/string-util'; import log from '../util/log'; - +import type {Sound} from '../sprites/sprite'; +import type {Asset, DataFormat} from 'clipcc-storage'; +import type Runtime from '../engine/runtime'; +import type SoundBank from '../../../audio/dist/types/SoundBank'; /** * Initialize a sound from an asset asynchronously. - * @param {!object} sound - the Scratch sound object. - * @property {string} md5 - the MD5 and extension of the sound to be loaded. - * @property {Buffer} data - sound data will be written here once loaded. - * @param {!Asset} soundAsset - the asset loaded from storage. - * @param {!Runtime} runtime - Scratch runtime, used to access the storage module. - * @param {SoundBank} soundBank - Scratch Audio SoundBank to add sounds to. - * @returns {!Promise} - a promise which will resolve to the sound when ready. + * @param sound - the Scratch sound object. + * @param soundAsset - the asset loaded from storage. + * @param runtime - Scratch runtime, used to access the storage module. + * @param soundBank - Scratch Audio SoundBank to add sounds to. + * @returns A promise which will resolve to the sound when ready. */ -const loadSoundFromAsset = function (sound, soundAsset, runtime, soundBank) { - sound.assetId = soundAsset.assetId; +const loadSoundFromAsset = function (sound: Sound, soundAsset: Asset, runtime: Runtime, soundBank: SoundBank | null) { + sound.assetId = soundAsset.assetId!; if (!runtime.audioEngine) { log.warn('No audio engine present; cannot load sound asset: ', sound.md5); return Promise.resolve(sound); @@ -20,8 +21,8 @@ const loadSoundFromAsset = function (sound, soundAsset, runtime, soundBank) { return runtime.audioEngine.decodeSoundPlayer(Object.assign( {}, sound, - {data: soundAsset.data} - )).then(soundPlayer => { + {data: soundAsset.data as {buffer: ArrayBuffer}} + ))!.then(soundPlayer => { sound.soundId = soundPlayer.id; // Set the sound sample rate and sample count based on the // the audio buffer from the audio engine since the sound @@ -41,7 +42,7 @@ const loadSoundFromAsset = function (sound, soundAsset, runtime, soundBank) { // Handle sound loading errors by replacing the runtime sound with the // default sound from storage, but keeping track of the original sound metadata // in a `broken` field -const handleSoundLoadError = function (sound, runtime, soundBank) { +const handleSoundLoadError = function (sound: Sound, runtime: Runtime, soundBank: SoundBank | null) { // Keep track of the old asset information until we're done loading the default sound const oldAsset = sound.asset; // could be null const oldAssetId = sound.assetId; @@ -51,22 +52,21 @@ const handleSoundLoadError = function (sound, runtime, soundBank) { const oldDataFormat = sound.dataFormat; // Use default asset if original fails to load - sound.assetId = runtime.storage.defaultAssetId.Sound; - sound.asset = runtime.storage.get(sound.assetId); + sound.assetId = runtime.storage!.defaultAssetId.Sound; + sound.asset = runtime.storage!.get(sound.assetId)!; sound.md5 = `${sound.assetId}.${sound.asset.dataFormat}`; return loadSoundFromAsset(sound, sound.asset, runtime, soundBank).then(loadedSound => { - loadedSound.broken = {}; - loadedSound.broken.assetId = oldAssetId; - loadedSound.broken.md5 = `${oldAssetId}.${oldDataFormat}`; - - // Should be null if we got here because the sound was missing - loadedSound.broken.asset = oldAsset; - - loadedSound.broken.sampleCount = oldSample; - loadedSound.broken.rate = oldRate; - loadedSound.broken.format = oldFormat; - loadedSound.broken.dataFormat = oldDataFormat; + loadedSound.broken = { + assetId: oldAssetId, + // Should be null if we got here because the sound was missing + asset: oldAsset, + format: oldFormat, + md5: `${oldAssetId}.${oldDataFormat}`, + dataFormat: oldDataFormat, + rate: oldRate, + sampleCount: oldSample + }; return loadedSound; }); @@ -74,21 +74,19 @@ const handleSoundLoadError = function (sound, runtime, soundBank) { /** * Load a sound's asset into memory asynchronously. - * @param {!object} sound - the Scratch sound object. - * @property {string} md5 - the MD5 and extension of the sound to be loaded. - * @property {Buffer} data - sound data will be written here once loaded. - * @param {!Runtime} runtime - Scratch runtime, used to access the storage module. - * @param {SoundBank} soundBank - Scratch Audio SoundBank to add sounds to. - * @returns {!Promise} - a promise which will resolve to the sound when ready. + * @param sound - the Scratch sound object. + * @param runtime - Scratch runtime, used to access the storage module. + * @param soundBank - Scratch Audio SoundBank to add sounds to. + * @returns A promise which will resolve to the sound when ready. */ -const loadSound = function (sound, runtime, soundBank) { +const loadSound = function (sound: Sound, runtime: Runtime, soundBank: SoundBank | null) { if (!runtime.storage) { log.warn('No storage module present; cannot load sound asset: ', sound.md5); return Promise.resolve(sound); } const idParts = StringUtil.splitFirst(sound.md5, '.'); const md5 = idParts[0]; - const ext = idParts[1].toLowerCase(); + const ext = idParts[1]!.toLowerCase() as DataFormat; sound.dataFormat = ext; return ( (sound.asset && Promise.resolve(sound.asset)) || diff --git a/packages/vm/src/index.ts b/packages/vm/src/index.ts index a339d345e..a7325b5ea 100644 --- a/packages/vm/src/index.ts +++ b/packages/vm/src/index.ts @@ -1,8 +1,13 @@ -import VirtualMachine from './virtual-machine.js'; +import VirtualMachine from './virtual-machine'; import ArgumentType from './extension-support/argument-type'; import BlockType from './extension-support/block-type'; export default VirtualMachine; export {ArgumentType, BlockType}; +// Types +export type * as extensions from './extension-support/extension-metadata'; export type * as schema from './serialization/schema'; +export type * as sprites from './sprites/'; +export type * as engine from './engine/'; +export type * from './virtual-machine'; diff --git a/packages/vm/src/io/ble.js b/packages/vm/src/io/ble.ts similarity index 54% rename from packages/vm/src/io/ble.js rename to packages/vm/src/io/ble.ts index 1778f29e4..033e2604c 100644 --- a/packages/vm/src/io/ble.js +++ b/packages/vm/src/io/ble.ts @@ -1,24 +1,45 @@ import JSONRPC from '../util/jsonrpc'; +import Runtime from '../engine/runtime'; +import ScratchLinkWebSocket from '../util/scratch-link-websocket'; class BLE extends JSONRPC { + _socket: ScratchLinkWebSocket; + _sendMessage!: (jsonMessageObject: object) => void; + + _availablePeripherals: Record; + _connectCallback: () => void; + _connected: boolean; + _characteristicDidChangeCallback: ((message: string) => void) | null; + _resetCallback: (() => void) | null; + _discoverTimeoutID: number | null; + _extensionId: string; + _peripheralOptions: object; + _runtime: Runtime; + /** * A BLE peripheral socket object. It handles connecting, over web sockets, to * BLE peripherals, and reading and writing data to them. - * @param {Runtime} runtime - the Runtime for sending/receiving GUI update events. - * @param {string} extensionId - the id of the extension using this socket. - * @param {object} peripheralOptions - the list of options for peripheral discovery. - * @param {object} connectCallback - a callback for connection. - * @param {object} resetCallback - a callback for resetting extension state. + * @param runtime - the Runtime for sending/receiving GUI update events. + * @param extensionId - the id of the extension using this socket. + * @param peripheralOptions - the list of options for peripheral discovery. + * @param connectCallback - a callback for connection. + * @param resetCallback - a callback for resetting extension state. */ - constructor (runtime, extensionId, peripheralOptions, connectCallback, resetCallback = null) { + constructor ( + runtime: Runtime, + extensionId: string, + peripheralOptions: object, + connectCallback: () => void, + resetCallback: (() => void) | null = null + ) { super(); - this._socket = runtime.getScratchLinkSocket('BLE'); + this._socket = runtime.getScratchLinkSocket('BLE') as ScratchLinkWebSocket; this._socket.setOnOpen(this.requestPeripheral.bind(this)); - this._socket.setOnClose(this.handleDisconnectError.bind(this)); - this._socket.setOnError(this._handleRequestError.bind(this)); - this._socket.setHandleMessage(this._handleMessage.bind(this)); + this._socket.setOnClose(() => this.handleDisconnectError()); + this._socket.setOnError(() => this._handleRequestError()); + this._socket.setHandleMessage(this._handleMessage.bind(this) as (json: unknown) => void); this._sendMessage = this._socket.sendMessage.bind(this._socket); @@ -46,25 +67,25 @@ class BLE extends JSONRPC { } this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000); this.sendRemoteRequest('discover', this._peripheralOptions) - .catch(e => { - this._handleRequestError(e); + .catch(() => { + this._handleRequestError(); }); } /** * Try connecting to the input peripheral id, and then call the connect * callback if connection is successful. - * @param {number} id - the id of the peripheral to connect to + * @param id - the id of the peripheral to connect to */ - connectPeripheral (id) { + connectPeripheral (id: number) { this.sendRemoteRequest('connect', {peripheralId: id}) .then(() => { this._connected = true; - this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTED); + this._runtime.emit(Runtime.PERIPHERAL_CONNECTED); this._connectCallback(); }) - .catch(e => { - this._handleRequestError(e); + .catch(() => { + this._handleRequestError(); }); } @@ -85,45 +106,54 @@ class BLE extends JSONRPC { } // Sets connection status icon to orange - this._runtime.emit(this._runtime.constructor.PERIPHERAL_DISCONNECTED); + this._runtime.emit(Runtime.PERIPHERAL_DISCONNECTED); } /** - * @returns {boolean} whether the peripheral is connected. + * Whether the peripheral is connected. */ - isConnected () { + isConnected (): boolean { return this._connected; } /** * Start receiving notifications from the specified ble service. - * @param {number} serviceId - the ble service to read. - * @param {number} characteristicId - the ble characteristic to get notifications from. - * @param {object} onCharacteristicChanged - callback for characteristic change notifications. - * @returns {Promise} - a promise from the remote startNotifications request. + * @param serviceId - the ble service to read. + * @param characteristicId - the ble characteristic to get notifications from. + * @param onCharacteristicChanged - callback for characteristic change notifications. + * @returns a promise from the remote startNotifications request. */ - startNotifications (serviceId, characteristicId, onCharacteristicChanged = null) { - const params = { + startNotifications ( + serviceId: number | string, + characteristicId: number | string, + onCharacteristicChanged: ((message: string) => void) | null = null + ) { + const params: Record = { serviceId, characteristicId }; this._characteristicDidChangeCallback = onCharacteristicChanged; return this.sendRemoteRequest('startNotifications', params) - .catch(e => { - this.handleDisconnectError(e); + .catch(() => { + this.handleDisconnectError(); }); } /** * Read from the specified ble service. - * @param {number} serviceId - the ble service to read. - * @param {number} characteristicId - the ble characteristic to read. - * @param {boolean} optStartNotifications - whether to start receiving characteristic change notifications. - * @param {object} onCharacteristicChanged - callback for characteristic change notifications. - * @returns {Promise} - a promise from the remote read request. + * @param serviceId - the ble service to read. + * @param characteristicId - the ble characteristic to read. + * @param optStartNotifications - whether to start receiving characteristic change notifications. + * @param onCharacteristicChanged - callback for characteristic change notifications. + * @returns a promise from the remote read request. */ - read (serviceId, characteristicId, optStartNotifications = false, onCharacteristicChanged = null) { - const params = { + read ( + serviceId: number | string, + characteristicId: number | string, + optStartNotifications = false, + onCharacteristicChanged: ((message: unknown) => void) | null = null + ) { + const params: Record = { serviceId, characteristicId }; @@ -134,22 +164,28 @@ class BLE extends JSONRPC { this._characteristicDidChangeCallback = onCharacteristicChanged; } return this.sendRemoteRequest('read', params) - .catch(e => { - this.handleDisconnectError(e); + .catch(() => { + this.handleDisconnectError(); }); } /** * Write data to the specified ble service. - * @param {number} serviceId - the ble service to write. - * @param {number} characteristicId - the ble characteristic to write. - * @param {string} message - the message to send. - * @param {string} encoding - the message encoding type. - * @param {boolean} withResponse - if true, resolve after peripheral's response. - * @returns {Promise} - a promise from the remote send request. + * @param serviceId - the ble service to write. + * @param characteristicId - the ble characteristic to write. + * @param message - the message to send. + * @param encoding - the message encoding type. + * @param withResponse - if true, resolve after peripheral's response. + * @returns a promise from the remote send request. */ - write (serviceId, characteristicId, message, encoding = null, withResponse = null) { - const params = {serviceId, characteristicId, message}; + write ( + serviceId: number | string, + characteristicId: number | string, + message: string, + encoding: string | null = null, + withResponse: boolean | null = null + ): Promise { + const params: Record = {serviceId, characteristicId, message}; if (encoding) { params.encoding = encoding; } @@ -157,23 +193,23 @@ class BLE extends JSONRPC { params.withResponse = withResponse; } return this.sendRemoteRequest('write', params) - .catch(e => { - this.handleDisconnectError(e); + .catch(() => { + this.handleDisconnectError(); }); } /** * Handle a received call from the socket. - * @param {string} method - a received method label. - * @param {object} params - a received list of parameters. - * @returns {object} - optional return value. + * @param method - a received method label. + * @param params - a received list of parameters. + * @returns optional return value. */ - didReceiveCall (method, params) { + didReceiveCall (method: string, params: Record): unknown { switch (method) { case 'didDiscoverPeripheral': - this._availablePeripherals[params.peripheralId] = params; + this._availablePeripherals[params.peripheralId as number] = params; this._runtime.emit( - this._runtime.constructor.PERIPHERAL_LIST_UPDATE, + Runtime.PERIPHERAL_LIST_UPDATE, this._availablePeripherals ); if (this._discoverTimeoutID) { @@ -181,9 +217,9 @@ class BLE extends JSONRPC { } break; case 'userDidPickPeripheral': - this._availablePeripherals[params.peripheralId] = params; + this._availablePeripherals[params.peripheralId as number] = params; this._runtime.emit( - this._runtime.constructor.USER_PICKED_PERIPHERAL, + Runtime.USER_PICKED_PERIPHERAL, this._availablePeripherals ); if (this._discoverTimeoutID) { @@ -192,7 +228,7 @@ class BLE extends JSONRPC { break; case 'userDidNotPickPeripheral': this._runtime.emit( - this._runtime.constructor.PERIPHERAL_SCAN_TIMEOUT + Runtime.PERIPHERAL_SCAN_TIMEOUT ); if (this._discoverTimeoutID) { window.clearTimeout(this._discoverTimeoutID); @@ -200,7 +236,7 @@ class BLE extends JSONRPC { break; case 'characteristicDidChange': if (this._characteristicDidChangeCallback) { - this._characteristicDidChangeCallback(params.message); + this._characteristicDidChangeCallback(params.message as string); } break; case 'ping': @@ -219,7 +255,7 @@ class BLE extends JSONRPC { * Disconnect the socket, and if the extension using this socket has a * reset callback, call it. Finally, emit an error to the runtime. */ - handleDisconnectError (/* e */) { + handleDisconnectError () { // log.error(`BLE error: ${JSON.stringify(e)}`); if (!this._connected) return; @@ -230,7 +266,7 @@ class BLE extends JSONRPC { this._resetCallback(); } - this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTION_LOST_ERROR, { + this._runtime.emit(Runtime.PERIPHERAL_CONNECTION_LOST_ERROR, { message: `Scratch lost connection to`, extensionId: this._extensionId }); @@ -239,7 +275,7 @@ class BLE extends JSONRPC { _handleRequestError (/* e */) { // log.error(`BLE error: ${JSON.stringify(e)}`); - this._runtime.emit(this._runtime.constructor.PERIPHERAL_REQUEST_ERROR, { + this._runtime.emit(Runtime.PERIPHERAL_REQUEST_ERROR, { message: `Scratch lost connection to`, extensionId: this._extensionId }); @@ -249,7 +285,7 @@ class BLE extends JSONRPC { if (this._discoverTimeoutID) { window.clearTimeout(this._discoverTimeoutID); } - this._runtime.emit(this._runtime.constructor.PERIPHERAL_SCAN_TIMEOUT); + this._runtime.emit(Runtime.PERIPHERAL_SCAN_TIMEOUT); } } diff --git a/packages/vm/src/io/bt.js b/packages/vm/src/io/bt.ts similarity index 60% rename from packages/vm/src/io/bt.js rename to packages/vm/src/io/bt.ts index fb906992f..0942dae32 100644 --- a/packages/vm/src/io/bt.js +++ b/packages/vm/src/io/bt.ts @@ -1,25 +1,48 @@ import JSONRPC from '../util/jsonrpc'; +import Runtime from '../engine/runtime'; +import ScratchLinkWebSocket from '../util/scratch-link-websocket'; class BT extends JSONRPC { + _socket: ScratchLinkWebSocket; + _sendMessage!: (jsonMessageObject: object) => void; + + _availablePeripherals: Record; + _connectCallback: () => void; + _connected: boolean; + _characteristicDidChangeCallback: ((message: unknown) => void) | null; + _resetCallback: (() => void) | null; + _discoverTimeoutID: number | null; + _extensionId: string; + _peripheralOptions: object; + _messageCallback: (params: unknown) => void; + _runtime: Runtime; + /** * A BT peripheral socket object. It handles connecting, over web sockets, to * BT peripherals, and reading and writing data to them. - * @param {Runtime} runtime - the Runtime for sending/receiving GUI update events. - * @param {string} extensionId - the id of the extension using this socket. - * @param {object} peripheralOptions - the list of options for peripheral discovery. - * @param {object} connectCallback - a callback for connection. - * @param {object} resetCallback - a callback for resetting extension state. - * @param {object} messageCallback - a callback for message sending. + * @param runtime - the Runtime for sending/receiving GUI update events. + * @param extensionId - the id of the extension using this socket. + * @param peripheralOptions - the list of options for peripheral discovery. + * @param connectCallback - a callback for connection. + * @param resetCallback - a callback for resetting extension state. + * @param messageCallback - a callback for message sending. */ - constructor (runtime, extensionId, peripheralOptions, connectCallback, resetCallback = null, messageCallback) { + constructor ( + runtime: Runtime, + extensionId: string, + peripheralOptions: object, + connectCallback: () => void, + resetCallback: (() => void) | null = null, + messageCallback: (params: unknown) => void + ) { super(); - this._socket = runtime.getScratchLinkSocket('BT'); + this._socket = runtime.getScratchLinkSocket('BT') as ScratchLinkWebSocket; this._socket.setOnOpen(this.requestPeripheral.bind(this)); - this._socket.setOnError(this._handleRequestError.bind(this)); - this._socket.setOnClose(this.handleDisconnectError.bind(this)); - this._socket.setHandleMessage(this._handleMessage.bind(this)); + this._socket.setOnError(() => this._handleRequestError()); + this._socket.setOnClose(() => this.handleDisconnectError()); + this._socket.setHandleMessage(this._handleMessage.bind(this) as (json: unknown) => void); this._sendMessage = this._socket.sendMessage.bind(this._socket); @@ -49,29 +72,29 @@ class BT extends JSONRPC { this._discoverTimeoutID = window.setTimeout(this._handleDiscoverTimeout.bind(this), 15000); this.sendRemoteRequest('discover', this._peripheralOptions) .catch( - e => this._handleRequestError(e) + () => this._handleRequestError() ); } /** * Try connecting to the input peripheral id, and then call the connect * callback if connection is successful. - * @param {number} id - the id of the peripheral to connect to - * @param {string} pin - an optional pin for pairing + * @param id - the id of the peripheral to connect to + * @param pin - an optional pin for pairing */ - connectPeripheral (id, pin = null) { - const params = {peripheralId: id}; + connectPeripheral (id: number, pin: string | null = null) { + const params: Record = {peripheralId: id}; if (pin) { params.pin = pin; } this.sendRemoteRequest('connect', params) .then(() => { this._connected = true; - this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTED); + this._runtime.emit(Runtime.PERIPHERAL_CONNECTED); this._connectCallback(); }) - .catch(e => { - this._handleRequestError(e); + .catch(() => { + this._handleRequestError(); }); } @@ -92,36 +115,36 @@ class BT extends JSONRPC { } // Sets connection status icon to orange - this._runtime.emit(this._runtime.constructor.PERIPHERAL_DISCONNECTED); + this._runtime.emit(Runtime.PERIPHERAL_DISCONNECTED); } /** - * @returns {boolean} whether the peripheral is connected. + * Whether the peripheral is connected. */ - isConnected () { + isConnected (): boolean { return this._connected; } - sendMessage (options) { + sendMessage (options: object): Promise { return this.sendRemoteRequest('send', options) - .catch(e => { - this.handleDisconnectError(e); + .catch(() => { + this.handleDisconnectError(); }); } /** * Handle a received call from the socket. - * @param {string} method - a received method label. - * @param {object} params - a received list of parameters. - * @returns {object} - optional return value. + * @param method - a received method label. + * @param params - a received list of parameters. + * @returns optional return value. */ - didReceiveCall (method, params) { + didReceiveCall (method: string, params: Record): unknown { // TODO: Add peripheral 'undiscover' handling switch (method) { case 'didDiscoverPeripheral': - this._availablePeripherals[params.peripheralId] = params; + this._availablePeripherals[params.peripheralId as number] = params; this._runtime.emit( - this._runtime.constructor.PERIPHERAL_LIST_UPDATE, + Runtime.PERIPHERAL_LIST_UPDATE, this._availablePeripherals ); if (this._discoverTimeoutID) { @@ -129,9 +152,9 @@ class BT extends JSONRPC { } break; case 'userDidPickPeripheral': - this._availablePeripherals[params.peripheralId] = params; + this._availablePeripherals[params.peripheralId as number] = params; this._runtime.emit( - this._runtime.constructor.USER_PICKED_PERIPHERAL, + Runtime.USER_PICKED_PERIPHERAL, this._availablePeripherals ); if (this._discoverTimeoutID) { @@ -140,7 +163,7 @@ class BT extends JSONRPC { break; case 'userDidNotPickPeripheral': this._runtime.emit( - this._runtime.constructor.PERIPHERAL_SCAN_TIMEOUT + Runtime.PERIPHERAL_SCAN_TIMEOUT ); if (this._discoverTimeoutID) { window.clearTimeout(this._discoverTimeoutID); @@ -176,7 +199,7 @@ class BT extends JSONRPC { this._resetCallback(); } - this._runtime.emit(this._runtime.constructor.PERIPHERAL_CONNECTION_LOST_ERROR, { + this._runtime.emit(Runtime.PERIPHERAL_CONNECTION_LOST_ERROR, { message: `Scratch lost connection to`, extensionId: this._extensionId }); @@ -185,7 +208,7 @@ class BT extends JSONRPC { _handleRequestError (/* e */) { // log.error(`BT error: ${JSON.stringify(e)}`); - this._runtime.emit(this._runtime.constructor.PERIPHERAL_REQUEST_ERROR, { + this._runtime.emit(Runtime.PERIPHERAL_REQUEST_ERROR, { message: `Scratch lost connection to`, extensionId: this._extensionId }); @@ -195,7 +218,7 @@ class BT extends JSONRPC { if (this._discoverTimeoutID) { window.clearTimeout(this._discoverTimeoutID); } - this._runtime.emit(this._runtime.constructor.PERIPHERAL_SCAN_TIMEOUT); + this._runtime.emit(Runtime.PERIPHERAL_SCAN_TIMEOUT); } } diff --git a/packages/vm/src/io/clock.ts b/packages/vm/src/io/clock.ts index 3d3aecb38..d4a1a15bc 100644 --- a/packages/vm/src/io/clock.ts +++ b/packages/vm/src/io/clock.ts @@ -23,18 +23,18 @@ class Clock { return this._projectTimer.timeElapsed() / 1000; } - pause (): void { + pause () { this._paused = true; this._pausedTime = this._projectTimer.timeElapsed(); } - resume (): void { + resume () { this._paused = false; const dt = this._projectTimer.timeElapsed() - this._pausedTime!; this._projectTimer.startTime += dt; } - resetProjectTimer (): void { + resetProjectTimer () { this._projectTimer.start(); } } diff --git a/packages/vm/src/io/cloud.js b/packages/vm/src/io/cloud.ts similarity index 51% rename from packages/vm/src/io/cloud.js rename to packages/vm/src/io/cloud.ts index 94d45cfd9..5187165e2 100644 --- a/packages/vm/src/io/cloud.js +++ b/packages/vm/src/io/cloud.ts @@ -1,94 +1,86 @@ import Variable from '../engine/variable'; import log from '../util/log'; +import type Runtime from '../engine/runtime'; +import type Target from '../engine/target'; + +interface VarUpdateData { + /** The name of the variable to update */ + name: string; + /** The scalar value to update the variable with */ + value: string | number; +} -class Cloud { - /** - * @typedef updateVariable - * @param {string} name The name of the cloud variable to update on the server - * @param {(string | number)} value The value to update the cloud variable with. - */ +interface CloudIOData { + /** A VarUpdateData message indicating a cloud variable update */ + varUpdate?: VarUpdateData; +} + +export interface CloudProvider { + /** A function which sends a cloud variable update to the cloud data server. */ + updateVariable: (name: string, value: string | number) => void; + /** A function which closes the connection to the cloud data server. */ + requestCloseConnection: () => void; + createVariable: (name: string, value: unknown) => void; + renameVariable: (oldName: string, newName: string) => void; + deleteVariable: (name: string) => void; +} +class Cloud { /** - * A cloud data provider, responsible for managing the connection to the - * cloud data server and for posting data about cloud data activity to - * this IO device. - * @typedef {object} CloudProvider - * @property {updateVariable} updateVariable A function which sends a cloud variable - * update to the cloud data server. - * @property {Function} requestCloseConnection A function which closes - * the connection to the cloud data server. + * Reference to the cloud data provider, responsible for mananging + * the web socket connection to the cloud data server. */ + provider: CloudProvider | null = null; /** - * Part of a cloud io data post indicating a cloud variable update. - * @typedef {object} VarUpdateData - * @property {string} name The name of the variable to update - * @property {(number | string)} value The scalar value to update the variable with + * Reference to the runtime that owns this cloud io device. */ + runtime: Runtime; /** - * A cloud io data post message. - * @typedef {object} CloudIOData - * @property {VarUpdateData} varUpdate A {@link VarUpdateData} message indicating - * a cloud variable update + * Reference to the stage target which owns the cloud variables + * in the project. */ + stage: Target | null = null; /** * Cloud IO Device responsible for sending and receiving messages from * cloud provider (mananging the cloud server connection) and interacting * with cloud variables in the current project. - * @param {Runtime} runtime The runtime context for this cloud io device. + * @param runtime The runtime context for this cloud io device. */ - constructor (runtime) { - /** - * Reference to the cloud data provider, responsible for mananging - * the web socket connection to the cloud data server. - * @type {?CloudProvider} - */ - this.provider = null; - - /** - * Reference to the runtime that owns this cloud io device. - * @type {!Runtime} - */ + constructor (runtime: Runtime) { this.runtime = runtime; - - /** - * Reference to the stage target which owns the cloud variables - * in the project. - * @type {?Target} - */ - this.stage = null; } /** * Set a reference to the cloud data provider. - * @param {CloudProvider} provider The cloud data provider + * @param provider The cloud data provider */ - setProvider (provider) { + setProvider (provider: CloudProvider) { this.provider = provider; } /** * Set a reference to the stage target which owns the * cloud variables in the project. - * @param {Target} stage The stage target + * @param stage The stage target */ - setStage (stage) { + setStage (stage: Target) { this.stage = stage; } /** * Handle incoming data to this io device. - * @param {CloudIOData} data The {@link CloudIOData} object to process + * @param data The CloudIOData object to process */ - postData (data) { + postData (data: CloudIOData) { if (data.varUpdate) { this.updateCloudVariable(data.varUpdate); } } - requestCreateVariable (variable) { + requestCreateVariable (variable: Variable) { if (this.runtime.canAddCloudVariable()) { if (this.provider) { this.provider.createVariable(variable.name, variable.value); @@ -102,10 +94,10 @@ class Cloud { /** * Request the cloud data provider to update the given variable with * the given value. Does nothing if this io device does not have a provider set. - * @param {string} name The name of the variable to update - * @param {string | number} value The value to update the variable with + * @param name The name of the variable to update + * @param value The value to update the variable with */ - requestUpdateVariable (name, value) { + requestUpdateVariable (name: string, value: string | number) { if (this.provider) { this.provider.updateVariable(name, value); } @@ -114,10 +106,10 @@ class Cloud { /** * Request the cloud data provider to rename the variable with the given name * to the given new name. Does nothing if this io device does not have a provider set. - * @param {string} oldName The name of the variable to rename - * @param {string | number} newName The new name for the variable + * @param oldName The name of the variable to rename + * @param newName The new name for the variable */ - requestRenameVariable (oldName, newName) { + requestRenameVariable (oldName: string, newName: string) { if (this.provider) { this.provider.renameVariable(oldName, newName); } @@ -126,9 +118,9 @@ class Cloud { /** * Request the cloud data provider to delete the variable with the given name * Does nothing if this io device does not have a provider set. - * @param {string} name The name of the variable to delete + * @param name The name of the variable to delete */ - requestDeleteVariable (name) { + requestDeleteVariable (name: string) { if (this.provider) { this.provider.deleteVariable(name); } @@ -137,13 +129,13 @@ class Cloud { /** * Update a cloud variable in the runtime based on the message received * from the cloud provider. - * @param {VarData} varUpdate A {@link VarData} object describing + * @param varUpdate A VarUpdateData object describing * a cloud variable update received from the cloud data provider. */ - updateCloudVariable (varUpdate) { + updateCloudVariable (varUpdate: VarUpdateData) { const varName = varUpdate.name; - const variable = this.stage.lookupVariableByNameAndType(varName, Variable.SCALAR_TYPE); + const variable = this.stage!.lookupVariableByNameAndType(varName, Variable.SCALAR_TYPE); if (!variable || !variable.isCloud) { log.warn(`Received an update for a cloud variable that does not exist: ${varName}`); return; diff --git a/packages/vm/src/io/joystick.ts b/packages/vm/src/io/joystick.ts index 36f528658..5153da9c5 100644 --- a/packages/vm/src/io/joystick.ts +++ b/packages/vm/src/io/joystick.ts @@ -13,7 +13,7 @@ class Joystick { public runtime: Runtime ) {} - postData (data: Record): void { + postData (data: Record) { if (Object.prototype.hasOwnProperty.call(data, 'x')) this._x = data.x; if (Object.prototype.hasOwnProperty.call(data, 'y')) this._y = data.y; if (Object.prototype.hasOwnProperty.call(data, 'distance')) this._distance = data.distance; diff --git a/packages/vm/src/io/keyboard.ts b/packages/vm/src/io/keyboard.ts index 933a4bb74..ec0b20a67 100644 --- a/packages/vm/src/io/keyboard.ts +++ b/packages/vm/src/io/keyboard.ts @@ -26,7 +26,6 @@ class Keyboard { * An uppercase string of length one; * except for special key names for arrow keys and space (e.g. 'left arrow'). * Can be a non-english unicode letter like: æ ø ש נ 手 廿. - * @type {Array.} */ _keysPressed: string[] = []; @@ -113,7 +112,7 @@ class Keyboard { * @param data.key The key from the DOM event. * @param data.isDown Whether the key is being pressed or released. */ - postData (data: { key: string; isDown: boolean }): void { + postData (data: { key: string; isDown: boolean }) { if (!data.key) return; const scratchKey = this._keyStringToScratchKey(data.key); if (scratchKey === '') return; diff --git a/packages/vm/src/io/mouse.js b/packages/vm/src/io/mouse.ts similarity index 60% rename from packages/vm/src/io/mouse.js rename to packages/vm/src/io/mouse.ts index 91f3886b5..c751571ad 100644 --- a/packages/vm/src/io/mouse.js +++ b/packages/vm/src/io/mouse.ts @@ -1,27 +1,32 @@ import MathUtil from '../util/math-util'; +import type Runtime from '../engine/runtime'; +import type RenderedTarget from '../sprites/rendered-target'; class Mouse { - constructor (runtime) { - this._x = 0; - this._y = 0; - /** - * Press state for [left, midlle, right] - */ - this._isDown = [false, false, false]; + _x = 0; + _y = 0; + _clientX = 0; + _clientY = 0; + _scratchX = 0; + _scratchY = 0; + /** + * Press state for [left, midlle, right] + */ + _isDown: [boolean, boolean, boolean] = [false, false, false]; + constructor ( /** * Reference to the owning Runtime. * Can be used, for example, to activate hats. - * @type {!Runtime} */ - this.runtime = runtime; - } + public runtime: Runtime + ) {} /** * Activate "event_whenthisspriteclicked" hats. - * @param {Target} target to trigger hats on. + * @param target to trigger hats on. * @private */ - _activateClickHats (target) { + _activateClickHats (target: RenderedTarget) { // Activate both "this sprite clicked" and "stage clicked" // They were separated into two opcodes for labeling, // but should act the same way. @@ -35,16 +40,16 @@ class Mouse { /** * Find a target by XY location - * @param {number} x X position to be sent to the renderer. - * @param {number} y Y position to be sent to the renderer. - * @returns {Target} the target at that location + * @param x X position to be sent to the renderer. + * @param y Y position to be sent to the renderer. + * @returns the target at that location * @private */ - _pickTarget (x, y) { + _pickTarget (x: number, y: number) { if (this.runtime.renderer) { const drawableID = this.runtime.renderer.pick(x, y); for (let i = 0; i < this.runtime.targets.length; i++) { - const target = this.runtime.targets[i]; + const target = this.runtime.targets[i] as RenderedTarget; if (Object.prototype.hasOwnProperty.call(target, 'drawableID') && target.drawableID === drawableID) { return target; @@ -57,12 +62,28 @@ class Mouse { /** * Mouse DOM event handler. - * @param {object} data Data from DOM event. + * @param data Data from DOM event. + * @param data.x X position of the mouse relative to the canvas. + * @param data.y Y position of the mouse relative to the canvas. + * @param data.isDown Whether the mouse is down. + * @param data.canvasWidth Width of the canvas, used for scaling mouse coordinates. + * @param data.canvasHeight Height of the canvas, used for scaling mouse coordinates. + * @param data.button Button number (0 for left, 1 for middle, 2 for right). + * @param data.wasDragged Whether the mouse was dragged between the last mouse down and mouse up. + * Used to prevent click hats from activating after dragging. */ - postData (data) { + postData (data: { + x?: number; + y?: number; + isDown?: boolean; + canvasWidth: number; + canvasHeight: number; + button: number; + wasDragged?: boolean; + }) { const halfWidth = this.runtime.stageWidth / 2; const halfHeight = this.runtime.stageHeight / 2; - if (data.x) { + if (typeof data.x === 'number') { this._clientX = data.x; this._scratchX = MathUtil.clamp( this.runtime.stageWidth * ((data.x / data.canvasWidth) - 0.5), @@ -70,7 +91,7 @@ class Mouse { halfWidth ); } - if (data.y) { + if (typeof data.y === 'number') { this._clientY = data.y; this._scratchY = MathUtil.clamp( -this.runtime.stageHeight * ((data.y / data.canvasHeight) - 0.5), @@ -89,10 +110,11 @@ class Mouse { if (data.wasDragged) return; // Do not activate click hats for clicks outside canvas bounds - if (!(data.x > 0 && data.x < data.canvasWidth && - data.y > 0 && data.y < data.canvasHeight)) return; + if (!(data.x! > 0 && data.x! < data.canvasWidth && + data.y! > 0 && data.y! < data.canvasHeight)) return; - const target = this._pickTarget(data.x, data.y); + const target = this._pickTarget(data.x!, data.y!); + if (!target) return; const isNewMouseDown = !previousDownState && data.isDown; const isNewMouseUp = previousDownState && !data.isDown; @@ -108,52 +130,52 @@ class Mouse { /** * Get the X position of the mouse in client coordinates. - * @returns {number} Non-clamped X position of the mouse cursor. + * @returns Non-clamped X position of the mouse cursor. */ - getClientX () { + getClientX (): number { return this._clientX; } /** * Get the Y position of the mouse in client coordinates. - * @returns {number} Non-clamped Y position of the mouse cursor. + * @returns Non-clamped Y position of the mouse cursor. */ - getClientY () { + getClientY (): number { return this._clientY; } /** * Get the X position of the mouse in scratch coordinates. - * @returns {number} Clamped and integer rounded X position of the mouse cursor. + * @returns Clamped and integer rounded X position of the mouse cursor. */ - getScratchX () { + getScratchX (): number { return this.runtime.limitOptions.accurateCoordinates ? this._scratchX : Math.round(this._scratchX); } /** * Get the Y position of the mouse in scratch coordinates. - * @returns {number} Clamped and integer rounded Y position of the mouse cursor. + * @returns Clamped and integer rounded Y position of the mouse cursor. */ - getScratchY () { + getScratchY (): number { return this.runtime.limitOptions.accurateCoordinates ? this._scratchY : Math.round(this._scratchY); } /** * Get the down state of the mouse. - * @returns {boolean} Is the mouse down? + * @returns Is the mouse down? */ - getIsDown () { + getIsDown (): boolean { return this._isDown[0]; } /** * Get the down state of the mouse. - * @param {number} button Button number. - * @returns {boolean} Is the mouse down? + * @param button Button number. + * @returns Is the mouse down? */ - getMousePressed (button) { + getMousePressed (button: number): boolean { return this._isDown[button]; } } diff --git a/packages/vm/src/io/mouseWheel.ts b/packages/vm/src/io/mouseWheel.ts index 4e6901cf5..aa3a94136 100644 --- a/packages/vm/src/io/mouseWheel.ts +++ b/packages/vm/src/io/mouseWheel.ts @@ -14,7 +14,7 @@ class MouseWheel { * @param data.deltaY Amount of vertical scroll. Negative value indicates scrolling up, * positive value indicates scrolling down. */ - postData (data: { deltaY: number }): void { + postData (data: { deltaY: number }) { const matchFields: Record = {}; if (data.deltaY < 0) { matchFields.KEY_OPTION = 'up arrow'; diff --git a/packages/vm/src/io/userData.ts b/packages/vm/src/io/userData.ts index 96df6da4e..305707fed 100644 --- a/packages/vm/src/io/userData.ts +++ b/packages/vm/src/io/userData.ts @@ -6,7 +6,7 @@ class UserData { * @param data Data posted to this ioDevice. * @param data.username The username to set for this user data device. */ - postData (data: {username: string}): void { + postData (data: {username: string}) { this._username = data.username; } diff --git a/packages/vm/src/io/video.js b/packages/vm/src/io/video.ts similarity index 56% rename from packages/vm/src/io/video.js rename to packages/vm/src/io/video.ts index 983e060e5..daf54db2c 100644 --- a/packages/vm/src/io/video.js +++ b/packages/vm/src/io/video.ts @@ -1,78 +1,92 @@ import StageLayering from '../engine/stage-layering'; +import type Runtime from '../engine/runtime'; + +export interface VideoProvider { + /** Requests camera access from the user, and upon success, enables the video feed */ + enableVideo: () => Promise; + /** Turns off the video feed */ + disableVideo: () => void; + /** Return frame data from the video feed in specified dimensions, format, and mirroring. */ + getFrame: (frameInfo: { + dimensions: [number, number]; + mirror: boolean; + format: string; + cacheTimeout: number; + }) => ImageData | null; + videoReady: boolean; + /** Set the dimensions of the video stream, usually called when stage size changed. */ + setDimensions: (width: number, height: number) => void; +} class Video { - constructor (runtime) { - this.runtime = runtime; + runtime: Runtime; + + provider: VideoProvider | null = null; + + /** + * Id representing a Scratch Renderer skin the video is rendered to for + * previewing. + */ + _skinId = -1; + + /** + * Id for a drawable using the video's skin that will render as a video + * preview. + */ + _drawable = -1; + + /** + * Store the last state of the video transparency ghost effect + */ + _ghost = 0; - /** - * @typedef VideoProvider - * @property {Function} enableVideo - Requests camera access from the user, and upon success, - * enables the video feed - * @property {Function} disableVideo - Turns off the video feed - * @property {Function} getFrame - Return frame data from the video feed in - * specified dimensions, format, and mirroring. - */ - this.provider = null; - - /** - * Id representing a Scratch Renderer skin the video is rendered to for - * previewing. - * @type {number} - */ - this._skinId = -1; - - /** - * Id for a drawable using the video's skin that will render as a video - * preview. - * @type {Drawable} - */ - this._drawable = -1; - - /** - * Store the last state of the video transparency ghost effect - * @type {number} - */ - this._ghost = 0; - - /** - * Store a flag that allows the preview to be forced transparent. - * @type {number} - */ - this._forceTransparentPreview = false; + /** + * Store a flag that allows the preview to be forced transparent. + */ + _forceTransparentPreview = false; + + _frameCacheTimeout?: number; + + _renderPreviewFrame: (() => void) | null = null; + + _renderPreviewTimeout?: ReturnType; + + mirror?: boolean; + + constructor (runtime: Runtime) { + this.runtime = runtime; } static get FORMAT_IMAGE_DATA () { - return 'image-data'; + return 'image-data' as const; } static get FORMAT_CANVAS () { - return 'canvas'; + return 'canvas' as const; } /** * Dimensions the video stream is analyzed at after its rendered to the * sample canvas. - * @type {Array.} * @deprecated Now follows actual stage size */ - static get DIMENSIONS () { + static get DIMENSIONS (): [number, number] { return [480, 360]; } /** * Order preview drawable is inserted at in the renderer. - * @type {number} */ - static get ORDER () { + static get ORDER (): number { return 1; } /** * Set a video provider for this device. A default implementation of * a video provider can be found in scratch-gui/src/lib/video/video-provider - * @param {VideoProvider} provider - Video provider to use + * @param provider - Video provider to use */ - setProvider (provider) { + setProvider (provider: VideoProvider) { this.provider = provider; } @@ -81,55 +95,61 @@ class Video { * * ioDevices.video.requestVideo() * - * @returns {Promise.