From b77024579113b5214b2728c98e61ac2ce6db3285 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Tue, 12 May 2026 15:43:14 +0800 Subject: [PATCH 01/40] :truck: chore(vm): migrate block.js, including logic fixes Signed-off-by: SimonShiki --- .../src/events/block_comment_collapse.ts | 2 +- packages/block/src/events/func_change.ts | 4 +- packages/block/src/events/index.ts | 0 packages/block/src/index.ts | 13 + packages/vm/src/engine/adapter.ts | 3 +- .../vm/src/engine/blocks-execute-cache.ts | 6 +- .../vm/src/engine/{blocks.js => blocks.ts} | 702 ++++++++++-------- packages/vm/src/engine/runtime.js | 3 +- packages/vm/src/engine/target.js | 6 +- packages/vm/src/serialization/sb2.js | 2 +- packages/vm/src/serialization/sb3.js | 2 +- packages/vm/src/serialization/schema.ts | 20 +- packages/vm/src/sprites/sprite.ts | 2 +- packages/vm/test/fixtures/events.json | 8 +- packages/vm/test/integration/sb3-roundtrip.js | 2 +- packages/vm/test/unit/blocks_event.js | 2 +- packages/vm/test/unit/engine_blocks.js | 2 +- .../test/unit/project_changed_state_blocks.js | 275 ++++--- 18 files changed, 628 insertions(+), 426 deletions(-) create mode 100644 packages/block/src/events/index.ts rename packages/vm/src/engine/{blocks.js => blocks.ts} (71%) 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/events/index.ts b/packages/block/src/events/index.ts new file mode 100644 index 000000000..e69de29bb 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/vm/src/engine/adapter.ts b/packages/vm/src/engine/adapter.ts index 3bfe5444b..d3da5532f 100644 --- a/packages/vm/src/engine/adapter.ts +++ b/packages/vm/src/engine/adapter.ts @@ -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/blocks-execute-cache.ts b/packages/vm/src/engine/blocks-execute-cache.ts index 0f06808dc..ab25569f2 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) => object; /** * A private method shared with execute to build an object containing the block @@ -43,7 +43,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)! }; 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..fbb885f1b 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 {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}>; +} + +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) { 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]; + 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,17 +993,17 @@ 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 + target: targetId ? runtime.getTargetById(targetId) ?? null : 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,10 @@ 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) { const currTarget = this.runtime.getEditingTarget(); if (!currTarget) return; if (!Object.prototype.hasOwnProperty.call(currTarget.comments, commentId)) { @@ -1013,12 +1089,12 @@ class Blocks { * @param {Array | null} 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 +1112,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 +1130,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 +1153,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 +1174,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 +1188,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 +1205,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 +1235,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 +1250,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 +1262,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 +1277,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 +1292,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 +1307,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 +1333,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 +1417,7 @@ class Blocks { } let value = blockField.value; if (typeof value === 'string') { - value = xmlEscape(blockField.value); + value = xmlEscape(blockField.value!); } xmlString += `>${value}`; } @@ -1352,10 +1432,10 @@ 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 @@ -1364,18 +1444,18 @@ class Blocks { /** * 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 +1507,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 +1517,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 +1556,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 +1575,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 +1587,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 +1607,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 +1630,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 +1641,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/runtime.js b/packages/vm/src/engine/runtime.js index d2710d2d4..96a364344 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.js @@ -1,7 +1,7 @@ import EventEmitter from 'events'; import {OrderedMap} from 'immutable'; import ArgumentType from '../extension-support/argument-type'; -import Blocks from './blocks.js'; +import Blocks from './blocks'; import {getScripts as getCachedScriptsByOpcode} from './blocks-runtime-cache'; import BlockType from '../extension-support/block-type'; import Profiler from './profiler'; @@ -502,7 +502,6 @@ class Runtime extends EventEmitter { /** * Map to look up all monitor block information by opcode. * @type {Record} - * @private */ this.monitorBlockInfo = {}; diff --git a/packages/vm/src/engine/target.js b/packages/vm/src/engine/target.js index 7dc979209..a7e397fb8 100644 --- a/packages/vm/src/engine/target.js +++ b/packages/vm/src/engine/target.js @@ -1,5 +1,5 @@ import EventEmitter from 'events'; -import Blocks from './blocks.js'; +import Blocks from './blocks'; import Variable from '../engine/variable'; import Comment from '../engine/comment'; import uid from '../util/uid'; @@ -267,7 +267,7 @@ class Target extends EventEmitter { * @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 {boolean} [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) { @@ -285,7 +285,7 @@ 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 {string} [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. diff --git a/packages/vm/src/serialization/sb2.js b/packages/vm/src/serialization/sb2.js index afca5d253..39719da0c 100644 --- a/packages/vm/src/serialization/sb2.js +++ b/packages/vm/src/serialization/sb2.js @@ -9,7 +9,7 @@ * @typedef {number} int */ -import Blocks from '../engine/blocks.js'; +import Blocks from '../engine/blocks'; import RenderedTarget from '../sprites/rendered-target.js'; import Sprite from '../sprites/sprite'; diff --git a/packages/vm/src/serialization/sb3.js b/packages/vm/src/serialization/sb3.js index 82f8d450e..b8e130ced 100644 --- a/packages/vm/src/serialization/sb3.js +++ b/packages/vm/src/serialization/sb3.js @@ -6,7 +6,7 @@ import vmPackage from '../../package.json'; -import Blocks from '../engine/blocks.js'; +import Blocks from '../engine/blocks'; import Sprite from '../sprites/sprite'; import Variable from '../engine/variable'; import Comment from '../engine/comment'; diff --git a/packages/vm/src/serialization/schema.ts b/packages/vm/src/serialization/schema.ts index ecb5c7916..a40748316 100644 --- a/packages/vm/src/serialization/schema.ts +++ b/packages/vm/src/serialization/schema.ts @@ -1,3 +1,5 @@ +import type {BlockCommentState} from 'clipcc-block'; + /* eslint-disable @typescript-eslint/no-explicit-any */ export interface SB3Project { targets: SB3Target[]; @@ -173,7 +175,7 @@ export interface VMBlock { y?: number; mutation?: VMMutation; comment?: string; - commentData?: unknown; + commentData?: BlockCommentState; isMonitored?: boolean; targetId?: string | null; } @@ -195,14 +197,14 @@ export interface VMMutation { tagName?: string; children?: VMMutation[]; proccode?: string; - argumentids?: string; - argumentnames?: string; - argumentdefaults?: string; - warp?: string | boolean; - hasnext?: string | boolean; - return?: string | boolean; - global?: string | boolean; - generateshadows?: string | boolean; + argumentids?: string[]; + argumentnames?: string[]; + argumentdefaults?: string[]; + warp?: boolean; + hasnext?: boolean; + return?: boolean; + global?: boolean; + generateshadows?: boolean; blockInfo?: Record; [key: string]: unknown; } diff --git a/packages/vm/src/sprites/sprite.ts b/packages/vm/src/sprites/sprite.ts index 35497584c..eac301fd8 100644 --- a/packages/vm/src/sprites/sprite.ts +++ b/packages/vm/src/sprites/sprite.ts @@ -1,5 +1,5 @@ import RenderedTarget from './rendered-target.js'; -import Blocks from '../engine/blocks.js'; +import Blocks from '../engine/blocks'; import {loadSoundFromAsset} from '../import/load-sound.js'; import {loadCostumeFromAsset} from '../import/load-costume.js'; import newBlockIds from '../util/new-block-ids'; diff --git a/packages/vm/test/fixtures/events.json b/packages/vm/test/fixtures/events.json index bb9a41f59..bf4f77c4b 100644 --- a/packages/vm/test/fixtures/events.json +++ b/packages/vm/test/fixtures/events.json @@ -87,10 +87,12 @@ }, "createcommentUpdatePosition": { "name": "comment", - "type": "comment_create", + "type": "comment_move", "commentId": "a comment", - "x": 10, - "y": 20 + "newCoordinate_": { + "x": 10, + "y": 20 + } }, "mockVariableBlock": { "name": "block", diff --git a/packages/vm/test/integration/sb3-roundtrip.js b/packages/vm/test/integration/sb3-roundtrip.js index 347f05d33..66f405010 100644 --- a/packages/vm/test/integration/sb3-roundtrip.js +++ b/packages/vm/test/integration/sb3-roundtrip.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Blocks from '../../src/engine/blocks.js'; +import Blocks from '../../src/engine/blocks'; import Clone from '../../src/util/clone'; import {loadCostume} from '../../src/import/load-costume.js'; import {loadSound} from '../../src/import/load-sound.js'; diff --git a/packages/vm/test/unit/blocks_event.js b/packages/vm/test/unit/blocks_event.js index 53feacdf8..3e17e6c40 100644 --- a/packages/vm/test/unit/blocks_event.js +++ b/packages/vm/test/unit/blocks_event.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Blocks from '../../src/engine/blocks.js'; +import Blocks from '../../src/engine/blocks'; import BlockUtility from '../../src/engine/block-utility'; import Event from '../../src/blocks/scratch3_event'; import Runtime from '../../src/engine/runtime.js'; diff --git a/packages/vm/test/unit/engine_blocks.js b/packages/vm/test/unit/engine_blocks.js index f67f18169..c0ca98ad9 100644 --- a/packages/vm/test/unit/engine_blocks.js +++ b/packages/vm/test/unit/engine_blocks.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Blocks from '../../src/engine/blocks.js'; +import Blocks from '../../src/engine/blocks'; import Variable from '../../src/engine/variable'; import adapter from '../../src/engine/adapter'; import events from '../fixtures/events.json'; diff --git a/packages/vm/test/unit/project_changed_state_blocks.js b/packages/vm/test/unit/project_changed_state_blocks.js index 826c82e66..04c364cfc 100644 --- a/packages/vm/test/unit/project_changed_state_blocks.js +++ b/packages/vm/test/unit/project_changed_state_blocks.js @@ -250,15 +250,16 @@ test('Deleting a variable should emit a project changed event', t => { test('Creating a block comment should emit a project changed event', t => { blockContainer.blocklyListen({ - type: 'comment_create', + type: 'block_comment_create', blockId: 'a new block', - commentId: 'a new comment', - height: 250, - width: 400, - x: -40, - y: 27, - collapsed: false, - text: 'comment' + commentId: 'a new comment' + }); + blockContainer.blocklyListen({ + type: 'change', + blockId: 'a new block', + element: 'comment', + name: 'a new comment', + newValue: 'comment' }); t.equal(projectChanged, true); @@ -268,45 +269,129 @@ test('Creating a block comment should emit a project changed event', t => { test('Creating a workspace comment should emit a project changed event', t => { blockContainer.blocklyListen({ type: 'comment_create', - blockId: null, commentId: 'a new comment', - height: 250, - width: 400, - x: -40, - y: 27, - collapsed: false, - text: 'comment' + json: { + height: 250, + width: 400, + x: -40, + y: 27, + collapsed: false, + text: 'comment' + } }); t.equal(projectChanged, true); t.end(); }); -test('Changing a comment should emit a project changed event', t => { +test('Changing a workspace comment should emit a project changed event', t => { blockContainer.blocklyListen({ type: 'comment_create', - blockId: null, commentId: 'a new comment', - height: 250, - width: 400, - x: -40, - y: 27, - collapsed: false, - text: 'comment' + json: { + height: 250, + width: 400, + x: -40, + y: 27, + collapsed: false, + text: 'comment' + } + }); + + projectChanged = false; + + blockContainer.blocklyListen({ + type: 'comment_collapse', + commentId: 'a new comment', + newCollapsed: true + }); + + t.equal(projectChanged, true); + + projectChanged = false; + + blockContainer.blocklyListen({ + type: 'comment_resize', + commentId: 'a new comment', + newSize: {width: 300, height: 100}, + oldSize: {width: 200, height: 200} }); + t.equal(projectChanged, true); + projectChanged = false; blockContainer.blocklyListen({ type: 'comment_change', blockId: null, commentId: 'a new comment', - newContents_: { - collapsed: true - }, - oldContents_: { - collapsed: false - } + newContents_: 'comment', + oldContents_: 'commant' + }); + + t.equal(projectChanged, true); + + t.end(); +}); + +test('Changing a block comment should emit a project changed event', t => { + blockContainer.blocklyListen({ + type: 'block_comment_create', + blockId: 'a new block', + commentId: 'a new comment' + }); + blockContainer.blocklyListen({ + type: 'change', + element: 'comment', + blockId: 'a new block', + name: 'a new comment', + newValue: '' + }); + + projectChanged = false; + + blockContainer.blocklyListen({ + type: 'change', + element: 'comment', + blockId: 'a new block', + name: 'a new comment', + newValue: 'comment', + oldValue: '' + }); + + t.equal(projectChanged, true); + + projectChanged = false; + + blockContainer.blocklyListen({ + type: 'block_comment_resize', + blockId: 'a new block', + commentId: 'a new comment', + newSize: {width: 300, height: 100}, + oldSize: {width: 200, height: 200} + }); + + t.equal(projectChanged, true); + + projectChanged = false; + + blockContainer.blocklyListen({ + type: 'block_comment_move', + blockId: 'a new block', + commentId: 'a new comment', + newCoordinate_: {x: -35, y: 50}, + oldCoordinate_: {x: -40, y: 27} + }); + + t.equal(projectChanged, true); + + projectChanged = false; + + blockContainer.blocklyListen({ + type: 'block_comment_collapse', + blockId: 'a new block', + commentId: 'a new comment', + newCollapsed: true }); t.equal(projectChanged, true); @@ -318,12 +403,28 @@ test('Attempting to change a comment that does not exist should not emit a proje type: 'comment_change', blockId: null, commentId: 'a new comment', - newContents_: { - collapsed: true - }, - oldContents_: { - collapsed: false - } + newContents_: 'comment', + oldContents_: '' + }); + blockContainer.blocklyListen({ + type: 'comment_resize', + blockId: null, + commentId: 'a new comment', + newSize: {width: 300, height: 100}, + oldSize: {width: 200, height: 200} + }); + blockContainer.blocklyListen({ + type: 'comment_move', + blockId: null, + commentId: 'a new comment', + newCoordinate_: {x: -35, y: 50}, + oldCoordinate_: {x: -40, y: 27} + }); + blockContainer.blocklyListen({ + type: 'comment_collapse', + blockId: null, + commentId: 'a new comment', + newCollapsed: true }); t.equal(projectChanged, false); @@ -332,29 +433,31 @@ test('Attempting to change a comment that does not exist should not emit a proje test('Deleting a block comment should emit a project changed event', t => { blockContainer.blocklyListen({ - type: 'comment_create', + type: 'block_comment_create', blockId: 'a new block', - commentId: 'a new comment', - height: 250, - width: 400, - x: -40, - y: 27, - collapsed: false, - text: 'comment' + commentId: 'a new comment' + }); + blockContainer.blocklyListen({ + type: 'change', + element: 'comment', + blockId: 'a new block', + name: 'a new comment', + newValue: '' }); projectChanged = false; blockContainer.blocklyListen({ - type: 'comment_delete', + type: 'block_comment_delete', blockId: 'a new block', - commentId: 'a new comment', - height: 250, - width: 400, - x: -40, - y: 27, - collapsed: false, - text: 'comment' + commentId: 'a new comment' + }); + blockContainer.blocklyListen({ + type: 'change', + element: 'comment', + blockId: 'a new block', + name: 'a new comment', + newValue: null }); t.equal(projectChanged, true); @@ -364,28 +467,30 @@ test('Deleting a block comment should emit a project changed event', t => { test('Deleting a workspace comment should emit a project changed event', t => { blockContainer.blocklyListen({ type: 'comment_create', - blockId: null, commentId: 'a new comment', - height: 250, - width: 400, - x: -40, - y: 27, - collapsed: false, - text: 'comment' + json: { + height: 250, + width: 400, + x: -40, + y: 27, + collapsed: false, + text: 'comment' + } }); projectChanged = false; blockContainer.blocklyListen({ type: 'comment_delete', - blockId: null, commentId: 'a new comment', - height: 250, - width: 400, - x: -40, - y: 27, - collapsed: false, - text: 'comment' + json: { + height: 250, + width: 400, + x: -40, + y: 27, + collapsed: false, + text: 'comment' + } }); t.equal(projectChanged, true); @@ -395,14 +500,15 @@ test('Deleting a workspace comment should emit a project changed event', t => { test('Deleting a comment that does not exist should not emit a project changed event', t => { blockContainer.blocklyListen({ type: 'comment_delete', - blockId: null, commentId: 'a new comment', - height: 250, - width: 400, - x: -40, - y: 27, - collapsed: false, - text: 'comment' + json: { + height: 250, + width: 400, + x: -40, + y: 27, + collapsed: false, + text: 'comment' + } }); t.equal(projectChanged, false); @@ -414,28 +520,23 @@ test('Moving a comment should emit a project changed event', t => { type: 'comment_create', blockId: null, commentId: 'a new comment', - height: 250, - width: 400, - x: -40, - y: 27, - collapsed: false, - text: 'comment' + json: { + height: 250, + width: 400, + x: -40, + y: 27, + collapsed: false, + text: 'comment' + } }); projectChanged = false; blockContainer.blocklyListen({ type: 'comment_move', - blockId: null, commentId: 'a new comment', - oldCoordinate_: { - x: -40, - y: 27 - }, - newCoordinate_: { - x: -35, - y: 50 - } + newCoordinate_: {x: -35, y: 50}, + oldCoordinate_: {x: -40, y: 27} }); t.equal(projectChanged, true); From 6c5a79030998e0fef90d62d30b0705b2992ba20c Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Tue, 12 May 2026 17:05:53 +0800 Subject: [PATCH 02/40] :truck: chore(vm): migrate execute.js Signed-off-by: SimonShiki --- .../vm/src/engine/blocks-execute-cache.ts | 14 +- .../vm/src/engine/{execute.js => execute.ts} | 397 ++++++++++-------- packages/vm/src/engine/profiler.ts | 1 + packages/vm/src/engine/runtime.js | 8 +- packages/vm/src/engine/sequencer.ts | 2 +- packages/vm/src/engine/thread.ts | 5 +- .../hat-threads-run-every-frame.js | 2 +- 7 files changed, 239 insertions(+), 190 deletions(-) rename packages/vm/src/engine/{execute.js => execute.ts} (68%) diff --git a/packages/vm/src/engine/blocks-execute-cache.ts b/packages/vm/src/engine/blocks-execute-cache.ts index ab25569f2..043258ff1 100644 --- a/packages/vm/src/engine/blocks-execute-cache.ts +++ b/packages/vm/src/engine/blocks-execute-cache.ts @@ -15,7 +15,7 @@ export interface CachedBlockData { mutation?: VMMutation; } -export 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 @@ export 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); @@ -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/execute.js b/packages/vm/src/engine/execute.ts similarity index 68% rename from packages/vm/src/engine/execute.js rename to packages/vm/src/engine/execute.ts index e4649803b..b72004c3b 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 {PrimitiveHandler} from './runtime'; +import type Sequencer from './sequencer'; /** * Single BlockUtility instance reused by execute for every pritimive ran. @@ -13,9 +19,9 @@ 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'. @@ -23,41 +29,66 @@ const blockFunctionProfilerFrame = 'blockFunction'; */ 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 +101,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 +125,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 +142,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 +176,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 +210,132 @@ 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. + * @type {string} + */ + opcode: string; - /** - * Block operation code for this block. - * @type {string} - */ - this.opcode = cached.opcode; + /** + * Original block object containing argument values for static fields. + * @type {object} + */ + 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. + * @type {object} + */ + inputs: Record; - /** - * Original block object containing argument values for executable inputs. - * @type {object} - */ - this.inputs = cached.inputs; + /** + * Procedure mutation. + * @type {?object} + */ + 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: PrimitiveHandler | 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 +355,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 +381,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 +403,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 +438,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 +465,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 +483,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 +495,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 +524,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 +558,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 +607,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 +648,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 +658,7 @@ const execute = function (sequencer, thread) { } return { opCached: reportedCached.id, - inputValue: reportedValues ? reportedValues[inputName] : null + inputValue: reportedValues ? reportedValues[inputName!] : null }; }); @@ -628,14 +671,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 +696,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/profiler.ts b/packages/vm/src/engine/profiler.ts index 0f3d3ef8f..176e82af9 100644 --- a/packages/vm/src/engine/profiler.ts +++ b/packages/vm/src/engine/profiler.ts @@ -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.js index 96a364344..2482dd6ba 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.js @@ -6,7 +6,7 @@ import {getScripts as getCachedScriptsByOpcode} from './blocks-runtime-cache'; import BlockType from '../extension-support/block-type'; import Profiler 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'; @@ -463,7 +463,7 @@ class Runtime extends EventEmitter { /** * Map to look up a block's execution order. * Keys are opcode for block, values are order array of its arguments. - * @type {Record>} + * @type {Record} */ this._orders = {}; @@ -1878,7 +1878,7 @@ class Runtime extends EventEmitter { /** * 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. + * @returns The execution order array of given opcode. */ getExecutionOrders (opcode) { return Object.prototype.hasOwnProperty.call(this._orders, opcode) && this._orders[opcode]; @@ -2625,7 +2625,7 @@ class Runtime extends EventEmitter { /** * 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 {unknown} value Value to show associated with the block. */ visualReport (blockId, value) { this.emit(Runtime.VISUAL_REPORT, {id: blockId, value: String(value)}); diff --git a/packages/vm/src/engine/sequencer.ts b/packages/vm/src/engine/sequencer.ts index ac1e6bd1b..6f7d3c3b9 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'; /** diff --git a/packages/vm/src/engine/thread.ts b/packages/vm/src/engine/thread.ts index 5d529460b..1c368b857 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 @@ -24,11 +25,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. */ diff --git a/packages/vm/test/integration/hat-threads-run-every-frame.js b/packages/vm/test/integration/hat-threads-run-every-frame.js index 6f64ab7a3..8e32f8dc0 100644 --- a/packages/vm/test/integration/hat-threads-run-every-frame.js +++ b/packages/vm/test/integration/hat-threads-run-every-frame.js @@ -5,7 +5,7 @@ import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; import Thread from '../../src/engine/thread'; import Runtime from '../../src/engine/runtime.js'; -import execute from '../../src/engine/execute.js'; +import execute from '../../src/engine/execute'; const projectUri = path.resolve(__dirname, '../fixtures/timer-greater-than-hat.sb2'); const project = readFileToBuffer(projectUri); From a61bc885ed22c6e5b03bc56432bcb12554ef7598 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Tue, 12 May 2026 17:38:55 +0800 Subject: [PATCH 03/40] :truck: chore(vm): migrate serialize-assets.js Signed-off-by: SimonShiki --- ...erialize-assets.js => serialize-assets.ts} | 30 ++++++++++--------- packages/vm/src/virtual-machine.js | 2 +- .../vm/test/integration/sb2_corrupted_png.js | 2 +- .../vm/test/integration/sb2_corrupted_svg.js | 2 +- .../vm/test/integration/sb2_missing_png.js | 2 +- .../vm/test/integration/sb2_missing_svg.js | 2 +- .../vm/test/integration/sb3_corrupted_png.js | 2 +- .../test/integration/sb3_corrupted_sound.js | 2 +- .../vm/test/integration/sb3_corrupted_svg.js | 2 +- .../vm/test/integration/sb3_missing_png.js | 2 +- .../vm/test/integration/sb3_missing_sound.js | 2 +- .../vm/test/integration/sb3_missing_svg.js | 2 +- .../test/integration/sprite2_corrupted_png.js | 2 +- .../test/integration/sprite2_corrupted_svg.js | 2 +- .../test/integration/sprite2_missing_png.js | 2 +- .../test/integration/sprite2_missing_svg.js | 2 +- .../test/integration/sprite3_corrupted_png.js | 2 +- .../test/integration/sprite3_corrupted_svg.js | 2 +- .../test/integration/sprite3_missing_png.js | 2 +- .../test/integration/sprite3_missing_svg.js | 2 +- 20 files changed, 35 insertions(+), 33 deletions(-) rename packages/vm/src/serialization/{serialize-assets.js => serialize-assets.ts} (62%) diff --git a/packages/vm/src/serialization/serialize-assets.js b/packages/vm/src/serialization/serialize-assets.ts similarity index 62% rename from packages/vm/src/serialization/serialize-assets.js rename to packages/vm/src/serialization/serialize-assets.ts index f849c6897..4631f1bfb 100644 --- a/packages/vm/src/serialization/serialize-assets.js +++ b/packages/vm/src/serialization/serialize-assets.ts @@ -1,15 +1,17 @@ +import type Runtime from '../engine/runtime'; + /** * Serialize all the assets of the given type ('sounds' or 'costumes') * in the provided runtime into an array of file descriptors. * A file descriptor is an object containing the name of the file * to be written and the contents of the file, the serialized asset. - * @param {Runtime} runtime The runtime with the assets to be serialized - * @param {string} assetType The type of assets to be serialized: 'sounds' | 'costumes' - * @param {string=} optTargetId Optional target id to serialize assets for - * @returns {Array} An array of file descriptors for each asset + * @param runtime The runtime with the assets to be serialized + * @param assetType The type of assets to be serialized: 'sounds' | 'costumes' + * @param optTargetId Optional target id to serialize assets for + * @returns An array of file descriptors for each asset */ -const serializeAssets = function (runtime, assetType, optTargetId) { - const targets = optTargetId ? [runtime.getTargetById(optTargetId)] : runtime.targets; +const serializeAssets = function (runtime: Runtime, assetType: 'sounds' | 'costumes', optTargetId?: string) { + const targets = optTargetId ? [runtime.getTargetById(optTargetId)!] : runtime.targets; const assetDescs = []; for (let i = 0; i < targets.length; i++) { const currTarget = targets[i]; @@ -34,11 +36,11 @@ const serializeAssets = function (runtime, assetType, optTargetId) { * in the specified target into an array of file descriptors. * A file descriptor is an object containing the name of the file * to be written and the contents of the file, the serialized sound. - * @param {Runtime} runtime The runtime with the sounds to be serialized - * @param {string=} optTargetId Optional targetid for serializing sounds of a single target - * @returns {Array} An array of file descriptors for each sound + * @param runtime The runtime with the sounds to be serialized + * @param optTargetId Optional targetid for serializing sounds of a single target + * @returns An array of file descriptors for each sound */ -const serializeSounds = function (runtime, optTargetId) { +const serializeSounds = function (runtime: Runtime, optTargetId?: string) { return serializeAssets(runtime, 'sounds', optTargetId); }; @@ -46,11 +48,11 @@ const serializeSounds = function (runtime, optTargetId) { * Serialize all the costumes in the provided runtime into an array of file * descriptors. A file descriptor is an object containing the name of the file * to be written and the contents of the file, the serialized costume. - * @param {Runtime} runtime The runtime with the costumes to be serialized - * @param {string} optTargetId Optional targetid for serializing costumes of a single target - * @returns {Array} An array of file descriptors for each costume + * @param runtime The runtime with the costumes to be serialized + * @param optTargetId Optional targetid for serializing costumes of a single target + * @returns An array of file descriptors for each costume */ -const serializeCostumes = function (runtime, optTargetId) { +const serializeCostumes = function (runtime: Runtime, optTargetId?: string) { return serializeAssets(runtime, 'costumes', optTargetId); }; diff --git a/packages/vm/src/virtual-machine.js b/packages/vm/src/virtual-machine.js index 465a1b2f4..367d1a0b6 100644 --- a/packages/vm/src/virtual-machine.js +++ b/packages/vm/src/virtual-machine.js @@ -19,7 +19,7 @@ import Variable from './engine/variable'; import newBlockIds from './util/new-block-ids'; import {loadCostume} from './import/load-costume.js'; import {loadSound} from './import/load-sound.js'; -import {serializeSounds, serializeCostumes} from './serialization/serialize-assets.js'; +import {serializeSounds, serializeCostumes} from './serialization/serialize-assets'; import uid from './util/uid'; import 'canvas-toBlob'; diff --git a/packages/vm/test/integration/sb2_corrupted_png.js b/packages/vm/test/integration/sb2_corrupted_png.js index 4c2953708..e14339fb1 100644 --- a/packages/vm/test/integration/sb2_corrupted_png.js +++ b/packages/vm/test/integration/sb2_corrupted_png.js @@ -15,7 +15,7 @@ import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; +import {serializeCostumes} from '../../src/serialization/serialize-assets'; const projectUri = path.resolve(__dirname, '../fixtures/corrupt_png.sb2'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/sb2_corrupted_svg.js b/packages/vm/test/integration/sb2_corrupted_svg.js index 6d0716d5f..79e91e5b8 100644 --- a/packages/vm/test/integration/sb2_corrupted_svg.js +++ b/packages/vm/test/integration/sb2_corrupted_svg.js @@ -15,7 +15,7 @@ import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; +import {serializeCostumes} from '../../src/serialization/serialize-assets'; const projectUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sb2'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/sb2_missing_png.js b/packages/vm/test/integration/sb2_missing_png.js index f1a59139a..4fce8f52b 100644 --- a/packages/vm/test/integration/sb2_missing_png.js +++ b/packages/vm/test/integration/sb2_missing_png.js @@ -14,7 +14,7 @@ import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; +import {serializeCostumes} from '../../src/serialization/serialize-assets'; const projectUri = path.resolve(__dirname, '../fixtures/missing_png.sb2'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/sb2_missing_svg.js b/packages/vm/test/integration/sb2_missing_svg.js index e1d4878f0..4dac6a7c5 100644 --- a/packages/vm/test/integration/sb2_missing_svg.js +++ b/packages/vm/test/integration/sb2_missing_svg.js @@ -14,7 +14,7 @@ import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; +import {serializeCostumes} from '../../src/serialization/serialize-assets'; const projectUri = path.resolve(__dirname, '../fixtures/missing_svg.sb2'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/sb3_corrupted_png.js b/packages/vm/test/integration/sb3_corrupted_png.js index 96fed65c3..dcd326c46 100644 --- a/packages/vm/test/integration/sb3_corrupted_png.js +++ b/packages/vm/test/integration/sb3_corrupted_png.js @@ -15,7 +15,7 @@ import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; +import {serializeCostumes} from '../../src/serialization/serialize-assets'; const projectUri = path.resolve(__dirname, '../fixtures/corrupt_png.sb3'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/sb3_corrupted_sound.js b/packages/vm/test/integration/sb3_corrupted_sound.js index 90ae37ece..40cc3d9b2 100644 --- a/packages/vm/test/integration/sb3_corrupted_sound.js +++ b/packages/vm/test/integration/sb3_corrupted_sound.js @@ -13,7 +13,7 @@ import md5 from 'js-md5'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeSounds} from '../../src/serialization/serialize-assets.js'; +import {serializeSounds} from '../../src/serialization/serialize-assets'; const projectUri = path.resolve(__dirname, '../fixtures/corrupt_sound.sb3'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/sb3_corrupted_svg.js b/packages/vm/test/integration/sb3_corrupted_svg.js index a40b449ca..657d92ab4 100644 --- a/packages/vm/test/integration/sb3_corrupted_svg.js +++ b/packages/vm/test/integration/sb3_corrupted_svg.js @@ -14,7 +14,7 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; +import {serializeCostumes} from '../../src/serialization/serialize-assets'; const projectUri = path.resolve(__dirname, '../fixtures/corrupt_svg.sb3'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/sb3_missing_png.js b/packages/vm/test/integration/sb3_missing_png.js index 3da005f32..070bdfeb2 100644 --- a/packages/vm/test/integration/sb3_missing_png.js +++ b/packages/vm/test/integration/sb3_missing_png.js @@ -14,7 +14,7 @@ import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; +import {serializeCostumes} from '../../src/serialization/serialize-assets'; const projectUri = path.resolve(__dirname, '../fixtures/missing_png.sb3'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/sb3_missing_sound.js b/packages/vm/test/integration/sb3_missing_sound.js index 68da36ff5..35a30b850 100644 --- a/packages/vm/test/integration/sb3_missing_sound.js +++ b/packages/vm/test/integration/sb3_missing_sound.js @@ -11,7 +11,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeSounds} from '../../src/serialization/serialize-assets.js'; +import {serializeSounds} from '../../src/serialization/serialize-assets'; const projectUri = path.resolve(__dirname, '../fixtures/missing_sound.sb3'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/sb3_missing_svg.js b/packages/vm/test/integration/sb3_missing_svg.js index 261904f41..75df8a550 100644 --- a/packages/vm/test/integration/sb3_missing_svg.js +++ b/packages/vm/test/integration/sb3_missing_svg.js @@ -13,7 +13,7 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; +import {serializeCostumes} from '../../src/serialization/serialize-assets'; const projectUri = path.resolve(__dirname, '../fixtures/missing_svg.sb3'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/sprite2_corrupted_png.js b/packages/vm/test/integration/sprite2_corrupted_png.js index dbbc86552..94858cde7 100644 --- a/packages/vm/test/integration/sprite2_corrupted_png.js +++ b/packages/vm/test/integration/sprite2_corrupted_png.js @@ -16,7 +16,7 @@ import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; +import {serializeCostumes} from '../../src/serialization/serialize-assets'; const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/sprite2_corrupted_svg.js b/packages/vm/test/integration/sprite2_corrupted_svg.js index 873f55ce8..491e2b40b 100644 --- a/packages/vm/test/integration/sprite2_corrupted_svg.js +++ b/packages/vm/test/integration/sprite2_corrupted_svg.js @@ -16,7 +16,7 @@ import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; +import {serializeCostumes} from '../../src/serialization/serialize-assets'; const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/sprite2_missing_png.js b/packages/vm/test/integration/sprite2_missing_png.js index ec476f5a3..1656e3d5a 100644 --- a/packages/vm/test/integration/sprite2_missing_png.js +++ b/packages/vm/test/integration/sprite2_missing_png.js @@ -14,7 +14,7 @@ import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; +import {serializeCostumes} from '../../src/serialization/serialize-assets'; // The particular project that we're loading doesn't matter for this test const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); diff --git a/packages/vm/test/integration/sprite2_missing_svg.js b/packages/vm/test/integration/sprite2_missing_svg.js index 84875d5b1..af28ef4b8 100644 --- a/packages/vm/test/integration/sprite2_missing_svg.js +++ b/packages/vm/test/integration/sprite2_missing_svg.js @@ -14,7 +14,7 @@ import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; +import {serializeCostumes} from '../../src/serialization/serialize-assets'; // The particular project that we're loading doesn't matter for this test const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); diff --git a/packages/vm/test/integration/sprite3_corrupted_png.js b/packages/vm/test/integration/sprite3_corrupted_png.js index 5cce84f38..35aa215c8 100644 --- a/packages/vm/test/integration/sprite3_corrupted_png.js +++ b/packages/vm/test/integration/sprite3_corrupted_png.js @@ -16,7 +16,7 @@ import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; +import {serializeCostumes} from '../../src/serialization/serialize-assets'; const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/sprite3_corrupted_svg.js b/packages/vm/test/integration/sprite3_corrupted_svg.js index 2c240237d..d9063da42 100644 --- a/packages/vm/test/integration/sprite3_corrupted_svg.js +++ b/packages/vm/test/integration/sprite3_corrupted_svg.js @@ -15,7 +15,7 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import {extractAsset, readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; +import {serializeCostumes} from '../../src/serialization/serialize-assets'; const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/sprite3_missing_png.js b/packages/vm/test/integration/sprite3_missing_png.js index 39f1ad2ed..266979be9 100644 --- a/packages/vm/test/integration/sprite3_missing_png.js +++ b/packages/vm/test/integration/sprite3_missing_png.js @@ -14,7 +14,7 @@ import FakeRenderer from '../fixtures/fake-renderer.js'; import FakeBitmapAdapter from '../fixtures/fake-bitmap-adapter.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; +import {serializeCostumes} from '../../src/serialization/serialize-assets'; // The particular project that we're loading doesn't matter for this test const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); diff --git a/packages/vm/test/integration/sprite3_missing_svg.js b/packages/vm/test/integration/sprite3_missing_svg.js index 6d997abac..cbc58885e 100644 --- a/packages/vm/test/integration/sprite3_missing_svg.js +++ b/packages/vm/test/integration/sprite3_missing_svg.js @@ -13,7 +13,7 @@ import makeTestStorage from '../fixtures/make-test-storage.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/index'; -import {serializeCostumes} from '../../src/serialization/serialize-assets.js'; +import {serializeCostumes} from '../../src/serialization/serialize-assets'; // The particular project that we're loading doesn't matter for this test const projectUri = path.resolve(__dirname, '../fixtures/default.sb3'); From 5c2e09f457f2dfbb442070eabd6958cb830169ab Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Tue, 12 May 2026 17:50:32 +0800 Subject: [PATCH 04/40] :truck: chore(vm): migrate sb2_specmap.ts Signed-off-by: SimonShiki --- packages/vm/src/serialization/sb2.js | 2 +- .../{sb2_specmap.js => sb2_specmap.ts} | 58 +++++++++++++------ 2 files changed, 42 insertions(+), 18 deletions(-) rename packages/vm/src/serialization/{sb2_specmap.js => sb2_specmap.ts} (97%) diff --git a/packages/vm/src/serialization/sb2.js b/packages/vm/src/serialization/sb2.js index 39719da0c..8cac5b559 100644 --- a/packages/vm/src/serialization/sb2.js +++ b/packages/vm/src/serialization/sb2.js @@ -18,7 +18,7 @@ import log from '../util/log'; import uid from '../util/uid'; import StringUtil from '../util/string-util'; import MathUtil from '../util/math-util'; -import specMap from './sb2_specmap.js'; +import specMap from './sb2_specmap'; import Comment from '../engine/comment'; import Variable from '../engine/variable'; import MonitorRecord from '../engine/monitor-record'; diff --git a/packages/vm/src/serialization/sb2_specmap.js b/packages/vm/src/serialization/sb2_specmap.ts similarity index 97% rename from packages/vm/src/serialization/sb2_specmap.js rename to packages/vm/src/serialization/sb2_specmap.ts index 1373b50b6..7d706a75d 100644 --- a/packages/vm/src/serialization/sb2_specmap.js +++ b/packages/vm/src/serialization/sb2_specmap.ts @@ -24,24 +24,47 @@ import Variable from '../engine/variable'; -/** - * @typedef {object} SB2SpecMap_blockInfo - * @property {string} opcode - the Scratch 3.0 block opcode. Use 'extensionID.opcode' for extension opcodes. - * @property {Array.} argMap - metadata for this block's arguments. - */ +interface SB2SpecMapInputInfo { + type: 'input'; + /** + * the scratch-blocks shadow type for this arg + */ + inputOp: string; + /** + * the name this argument will take when provided to the block implementation + */ + inputName: string; + variableType?: string; +} -/** - * @typedef {object} SB2SpecMap_argInfo - * @property {string} type - the type of this arg (such as 'input' or 'field') - * @property {string} inputOp - the scratch-blocks shadow type for this arg - * @property {string} inputName - the name this argument will take when provided to the block implementation - */ +interface SB2SpecMapFieldInfo { + type: 'field'; + /** + * the name this field will take when provided to the block implementation + */ + fieldName: string; + variableType?: string; +} + +type SB2SpecMapArgInfo = SB2SpecMapInputInfo | SB2SpecMapFieldInfo; + +interface SB2SpecMapBlockInfo { + /** + * the Scratch 3.0 block opcode. Use 'extensionID_opcode' for extension opcodes. + */ + opcode: string; + /** + * metadata for this block's arguments. + */ + argMap: SB2SpecMapArgInfo[]; +} + +type SB2SpecMapValue = SB2SpecMapBlockInfo | ((args: string[]) => SB2SpecMapBlockInfo); /** * Mapping of Scratch 2.0 opcode to Scratch 3.0 block metadata. - * @type {Record} */ -const specMap = { +const specMap: Record = { 'forward:': { opcode: 'motion_movesteps', argMap: [ @@ -746,6 +769,7 @@ const specMap = { } ] }, + // @ts-expect-error Special case, lazy to type 'whenSensorGreaterThan': ([, sensor]) => { if (sensor === 'video motion') { return { @@ -1643,11 +1667,11 @@ const specMap = { /** * Add to the specMap entries for an opcode from a Scratch 2.0 extension. Two entries will be made with the same * metadata; this is done to support projects saved by both older and newer versions of the Scratch 2.0 editor. - * @param {string} sb2Extension - the Scratch 2.0 name of the extension - * @param {string} sb2Opcode - the Scratch 2.0 opcode - * @param {SB2SpecMap_blockInfo} blockInfo - the Scratch 3.0 block info + * @param sb2Extension - the Scratch 2.0 name of the extension + * @param sb2Opcode - the Scratch 2.0 opcode + * @param blockInfo - the Scratch 3.0 block info */ -const addExtensionOp = function (sb2Extension, sb2Opcode, blockInfo) { +const addExtensionOp = function (sb2Extension: string, sb2Opcode: string, blockInfo: SB2SpecMapBlockInfo) { /** * This string separates the name of an extension and the name of an opcode in more recent Scratch 2.0 projects. * Earlier projects used '.' as a separator, up until we added the 'LEGO WeDo 2.0' extension... From b649d9f688290e95eeeb011ef7f5c1ec1d5d02e9 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Tue, 12 May 2026 18:21:12 +0800 Subject: [PATCH 05/40] :truck: chore(vm): migrate target.js Signed-off-by: SimonShiki --- packages/vm/src/blocks/scratch3_looks.ts | 4 +- packages/vm/src/blocks/scratch3_sound.ts | 2 +- packages/vm/src/engine/blocks.ts | 5 +- .../vm/src/engine/{target.js => target.ts} | 331 +++++++++--------- packages/vm/src/engine/variable.ts | 2 +- packages/vm/src/sprites/rendered-target.js | 2 +- packages/vm/src/util/variable-util.ts | 2 +- packages/vm/test/unit/blocks_event.js | 2 +- packages/vm/test/unit/engine_target.js | 2 +- packages/vm/test/unit/io_cloud.js | 2 +- packages/vm/test/unit/util_variable.js | 2 +- 11 files changed, 186 insertions(+), 170 deletions(-) rename packages/vm/src/engine/{target.js => target.ts} (75%) diff --git a/packages/vm/src/blocks/scratch3_looks.ts b/packages/vm/src/blocks/scratch3_looks.ts index 7851a9b44..23e22956a 100644 --- a/packages/vm/src/blocks/scratch3_looks.ts +++ b/packages/vm/src/blocks/scratch3_looks.ts @@ -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'; @@ -126,7 +126,7 @@ class Scratch3LooksBlocks implements CategoryPrototype { bubbleState = Clone.simple(Scratch3LooksBlocks.DEFAULT_BUBBLE_STATE); target.setCustomState(Scratch3LooksBlocks.STATE_KEY, bubbleState); } - return bubbleState; + return bubbleState as BubbleState; } /** diff --git a/packages/vm/src/blocks/scratch3_sound.ts b/packages/vm/src/blocks/scratch3_sound.ts index 1fcf0367c..5c8a5243f 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) as SoundState; if (!soundState) { soundState = Clone.simple(Scratch3SoundBlocks.DEFAULT_SOUND_STATE); target.setCustomState(Scratch3SoundBlocks.STATE_KEY, soundState); diff --git a/packages/vm/src/engine/blocks.ts b/packages/vm/src/engine/blocks.ts index fbb885f1b..98b658311 100644 --- a/packages/vm/src/engine/blocks.ts +++ b/packages/vm/src/engine/blocks.ts @@ -613,7 +613,7 @@ class Blocks { return; } const comment = currTarget!.comments[e.commentId!]; - comment.minimized = e.newCollapsed; + comment.minimized = !!e.newCollapsed; this.emitProjectChanged(); } break; @@ -1072,6 +1072,9 @@ class Blocks { * @param newText New text for comment */ 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)) { 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 a7e397fb8..1e3f49b67 100644 --- a/packages/vm/src/engine/target.js +++ b/packages/vm/src/engine/target.ts @@ -1,4 +1,5 @@ import EventEmitter from 'events'; + import Blocks from './blocks'; import Variable from '../engine/variable'; import Comment from '../engine/comment'; @@ -8,9 +9,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 +28,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 = {}; + + /** + * Currently known values for edge-activated hats. + * Keys are block ID for the hat; values are the currently known values. + */ + _edgeActivatedHatValues: Record = {}; + + /** + * Whether this target is the stage. Set by subclasses. + */ + isStage!: boolean; /** - * @param {Runtime} runtime Reference to the runtime. - * @param {?Blocks} blocks Blocks instance for the blocks owned by this target. - * @class + * @param runtime Reference to the runtime. + * @param blocks Blocks instance for the blocks owned by this target. */ - constructor (runtime, blocks) { + 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 +113,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 +130,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 +150,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 +180,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 +196,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 +217,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 +252,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 +272,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 +292,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 +323,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 +375,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 +401,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 +417,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 +443,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 +470,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) { + getCustomState (stateId: string) { return this._customState[stateId]; } /** * 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: unknown) { this._customState[stateId] = newValue; } @@ -493,7 +500,7 @@ class Target extends EventEmitter { this._customState = {}; if (this.runtime) { - this.runtime.removeExecutable(this); + this.runtime.removeExecutable(this as unknown as RenderedTarget); } } @@ -504,11 +511,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: string, skipStage?: boolean): string[] { if (typeof type !== 'string') type = Variable.SCALAR_TYPE; skipStage = skipStage || false; const targetVariables = Object.values(this.variables) @@ -518,21 +525,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 +556,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 +577,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 +595,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,11 +612,11 @@ 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 { - const newVar = new Variable(null, varName, varType); + const newVar = new Variable(null, varName, varType, false); newVarId = newVar.id; sprite.variables[newVarId] = newVar; } @@ -633,11 +646,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 +700,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: string): string | null => { const conflict = stage.lookupVariableByNameAndType(name, type); if (conflict) { const newName = StringUtil.unusedName( @@ -700,20 +713,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: string): string[] => { const namesOfType = varNamesByType[type]; if (namesOfType) return namesOfType; varNamesByType[type] = this.runtime.getAllVarNamesOfType(type); @@ -738,7 +751,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 +792,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 +800,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/variable.ts b/packages/vm/src/engine/variable.ts index 8c8827c25..1dfee9d9f 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: boolean) { this.id = id || uid(); this.name = name; this.type = type; diff --git a/packages/vm/src/sprites/rendered-target.js b/packages/vm/src/sprites/rendered-target.js index 525ca5c16..9bb210cde 100644 --- a/packages/vm/src/sprites/rendered-target.js +++ b/packages/vm/src/sprites/rendered-target.js @@ -2,7 +2,7 @@ import MathUtil from '../util/math-util'; import StringUtil from '../util/string-util'; import Cast from '../util/cast'; import Clone from '../util/clone'; -import Target from '../engine/target.js'; +import Target from '../engine/target'; import StageLayering from '../engine/stage-layering'; /** diff --git a/packages/vm/src/util/variable-util.ts b/packages/vm/src/util/variable-util.ts index fd08312b6..96d7af9c1 100644 --- a/packages/vm/src/util/variable-util.ts +++ b/packages/vm/src/util/variable-util.ts @@ -4,7 +4,7 @@ import type {VMField} from '../serialization/schema'; type VarRefMap = Record; -interface VarReference { +export interface VarReference { referencingField: VMField; type: VariableType; } diff --git a/packages/vm/test/unit/blocks_event.js b/packages/vm/test/unit/blocks_event.js index 3e17e6c40..46c923605 100644 --- a/packages/vm/test/unit/blocks_event.js +++ b/packages/vm/test/unit/blocks_event.js @@ -3,7 +3,7 @@ import Blocks from '../../src/engine/blocks'; import BlockUtility from '../../src/engine/block-utility'; import Event from '../../src/blocks/scratch3_event'; import Runtime from '../../src/engine/runtime.js'; -import Target from '../../src/engine/target.js'; +import Target from '../../src/engine/target'; import Thread from '../../src/engine/thread'; import Variable from '../../src/engine/variable'; diff --git a/packages/vm/test/unit/engine_target.js b/packages/vm/test/unit/engine_target.js index b03ebfb57..050c57f8d 100644 --- a/packages/vm/test/unit/engine_target.js +++ b/packages/vm/test/unit/engine_target.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Target from '../../src/engine/target.js'; +import Target from '../../src/engine/target'; import Variable from '../../src/engine/variable'; import adapter from '../../src/engine/adapter'; import Runtime from '../../src/engine/runtime.js'; diff --git a/packages/vm/test/unit/io_cloud.js b/packages/vm/test/unit/io_cloud.js index 41265ed61..fd5db1ec0 100644 --- a/packages/vm/test/unit/io_cloud.js +++ b/packages/vm/test/unit/io_cloud.js @@ -1,6 +1,6 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Cloud from '../../src/io/cloud.js'; -import Target from '../../src/engine/target.js'; +import Target from '../../src/engine/target'; import Variable from '../../src/engine/variable'; import Runtime from '../../src/engine/runtime.js'; diff --git a/packages/vm/test/unit/util_variable.js b/packages/vm/test/unit/util_variable.js index 50c7c1785..d31fb4225 100644 --- a/packages/vm/test/unit/util_variable.js +++ b/packages/vm/test/unit/util_variable.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import Target from '../../src/engine/target.js'; +import Target from '../../src/engine/target'; import Runtime from '../../src/engine/runtime.js'; import VariableUtil from '../../src/util/variable-util'; From 7efe120fee715f6872f9d3e94313392a234c7bd4 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Tue, 12 May 2026 19:17:31 +0800 Subject: [PATCH 06/40] :truck: chore(vm): migrate rendered-target.js Signed-off-by: SimonShiki --- packages/render/src/RenderWebGL.js | 2 +- packages/vm/src/blocks/scratch3_looks.ts | 2 +- packages/vm/src/blocks/scratch3_motion.ts | 1 + .../extension-support/extension-metadata.ts | 11 + .../vm/src/extensions/scratch3_pen/index.js | 2 +- packages/vm/src/serialization/sb2.js | 2 +- ...{rendered-target.js => rendered-target.ts} | 611 +++++++++--------- packages/vm/src/sprites/sprite.ts | 12 +- packages/vm/test/integration/addSprite.js | 2 +- .../vm/test/integration/import_nested_sb2.js | 2 +- packages/vm/test/integration/import_sb2.js | 2 +- .../vm/test/integration/internal-extension.js | 2 +- packages/vm/test/unit/blocks_looks.js | 2 +- packages/vm/test/unit/blocks_motion.js | 2 +- packages/vm/test/unit/blocks_sensing.js | 2 +- packages/vm/test/unit/engine_sequencer.js | 2 +- packages/vm/test/unit/engine_thread.js | 2 +- packages/vm/test/unit/serialization_sb2.js | 2 +- .../vm/test/unit/sprites_rendered-target.js | 2 +- packages/vm/test/unit/virtual-machine.js | 2 +- packages/vm/test/unit/vm_collectAssets.js | 2 +- 21 files changed, 342 insertions(+), 327 deletions(-) rename packages/vm/src/sprites/{rendered-target.js => rendered-target.ts} (68%) diff --git a/packages/render/src/RenderWebGL.js b/packages/render/src/RenderWebGL.js index fe127004f..120a8b403 100644 --- a/packages/render/src/RenderWebGL.js +++ b/packages/render/src/RenderWebGL.js @@ -508,7 +508,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)) { diff --git a/packages/vm/src/blocks/scratch3_looks.ts b/packages/vm/src/blocks/scratch3_looks.ts index 23e22956a..04321d274 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'; 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/extension-support/extension-metadata.ts b/packages/vm/src/extension-support/extension-metadata.ts index 6fc4bbae8..9b738198c 100644 --- a/packages/vm/src/extension-support/extension-metadata.ts +++ b/packages/vm/src/extension-support/extension-metadata.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ +import type Runtime from '../engine/runtime'; import type ArgumentType from './argument-type'; import type BlockType from './block-type'; import type ReporterScope from './reporter-scope'; @@ -21,6 +22,11 @@ export interface ExtensionMetadata { blocks: Array; /** Map of menu name to metadata for each of this extension's menus. */ menus?: Record; + /** + * New target type(s). + * @todo Not implemented by VM. + */ + targetTypes?: string[]; } /** @@ -94,3 +100,8 @@ export interface ExtensionMenuItemComplex { /** The human-readable label of this menu item in the menu. */ text: string; } + +export interface ExtensionClass { + new (runtime: Runtime): unknown; + getInfo(): ExtensionMetadata; +} diff --git a/packages/vm/src/extensions/scratch3_pen/index.js b/packages/vm/src/extensions/scratch3_pen/index.js index a30bb867b..2f122e904 100644 --- a/packages/vm/src/extensions/scratch3_pen/index.js +++ b/packages/vm/src/extensions/scratch3_pen/index.js @@ -6,7 +6,7 @@ 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'; diff --git a/packages/vm/src/serialization/sb2.js b/packages/vm/src/serialization/sb2.js index 8cac5b559..6a8a98441 100644 --- a/packages/vm/src/serialization/sb2.js +++ b/packages/vm/src/serialization/sb2.js @@ -11,7 +11,7 @@ import Blocks from '../engine/blocks'; -import RenderedTarget from '../sprites/rendered-target.js'; +import RenderedTarget from '../sprites/rendered-target'; import Sprite from '../sprites/sprite'; import Color from '../util/color'; import log from '../util/log'; diff --git a/packages/vm/src/sprites/rendered-target.js b/packages/vm/src/sprites/rendered-target.ts similarity index 68% rename from packages/vm/src/sprites/rendered-target.js rename to packages/vm/src/sprites/rendered-target.ts index 9bb210cde..fe066e17d 100644 --- a/packages/vm/src/sprites/rendered-target.js +++ b/packages/vm/src/sprites/rendered-target.ts @@ -5,177 +5,173 @@ import Clone from '../util/clone'; import Target from '../engine/target'; import StageLayering from '../engine/stage-layering'; -/** - * @typedef {import('../../../render/dist/types/Rectangle')} Rectangle - */ +import type Sprite from './sprite'; +import type {Costume, Sound} from './sprite'; +import type Runtime from '../engine/runtime'; +import type {StageLayer} from '../engine/stage-layering'; +import type RenderWebGL from 'clipcc-render'; +import type Rectangle from '../../../render/dist/types/Rectangle'; + +interface SpriteInfoData { + x: number; + y: number; + direction: number; + draggable: boolean; + rotationStyle: string; + visible: boolean; + size: number; + force: boolean; +} -/** - * @typedef {import('../engine/runtime').default} Runtime - */ +interface Fence { + left: number; + right: number; + top: number; + bottom: number; +} /** * Rendered target: instance of a sprite (clone), or the stage. */ class RenderedTarget extends Target { /** - * @param {!Sprite} sprite Reference to the parent sprite. - * @param {Runtime} runtime Reference to the runtime. - * @class + * Reference to the sprite that this is a render of. + */ + sprite: Sprite; + /** + * Reference to the global renderer for this VM, if one exists. + */ + renderer: RenderWebGL | null = null; + /** + * ID of the drawable for this rendered target, + * returned by the renderer, if rendered. + */ + drawableID: number | undefined | null = null; + + /** + * Drag state of this rendered target. If true, x/y position can't be + * changed by blocks. + */ + dragging = false; + + /** + * Map of current graphic effect values. + */ + effects: Record = { + color: 0, + fisheye: 0, + whirl: 0, + pixelate: 0, + mosaic: 0, + brightness: 0, + ghost: 0 + }; + + /** + * Whether this represents an "original" non-clone rendered-target for a sprite, + * i.e., created by the editor and not clone blocks. + */ + isOriginal = true; + + /** + * Whether this rendered target represents the Scratch stage. + */ + isStage = false; + + /** + * Scratch X coordinate. Currently should range from -240 to 240. + */ + x = 0; + + /** + * Scratch Y coordinate. Currently should range from -180 to 180. + */ + y = 0; + + /** + * Scratch direction. Currently should range from -179 to 180. + */ + direction = 90; + + /** + * Whether the rendered target is draggable on the stage + */ + draggable = false; + + /** + * Whether the rendered target is currently visible. + */ + visible = true; + + /** + * Size of rendered target as a percent of costume size. + */ + size = 100; + + /** + * Currently selected costume index. + */ + currentCostume = 0; + + /** + * Current rotation style. + */ + rotationStyle: string = RenderedTarget.ROTATION_STYLE_ALL_AROUND; + + /** + * Loudness for sound playback for this target, as a percentage. + */ + volume = 100; + + /** + * Current tempo (used by the music extension). + * This property is global to the project and stored in the stage. + */ + tempo = 60; + + /** + * The transparency of the video (used by extensions with camera input). + * This property is global to the project and stored in the stage. + */ + videoTransparency = 50; + + /** + * The state of the video input (used by extensions with camera input). + * This property is global to the project and stored in the stage. + * + * Defaults to ON. This setting does not turn the video by itself. A + * video extension once loaded will set the video device to this + * setting. Set to ON when a video extension is added in the editor the + * video will start ON. If the extension is loaded as part of loading a + * saved project the extension will see the value set when the stage + * was loaded from the saved values including the video state. + */ + videoState: string = RenderedTarget.VIDEO_STATE.ON; + + /** + * The language to use for speech synthesis, in the text2speech extension. + * It is initialized to null so that on extension load, we can check for + * this and try setting it using the editor locale. + */ + textToSpeechLanguage: string | null = null; + + /** + * @param sprite Reference to the parent sprite. + * @param runtime Reference to the runtime. */ - constructor (sprite, runtime) { + constructor (sprite: Sprite, runtime: Runtime) { super(runtime, sprite.blocks); - /** - * Reference to the sprite that this is a render of. - * @type {!Sprite} - */ this.sprite = sprite; - /** - * Reference to the global renderer for this VM, if one exists. - * @type {?RenderWebGL} - */ this.renderer = null; - if (this.runtime) { - this.renderer = this.runtime.renderer; - } - /** - * ID of the drawable for this rendered target, - * returned by the renderer, if rendered. - * @type {?number} - */ - this.drawableID = null; - - /** - * Drag state of this rendered target. If true, x/y position can't be - * changed by blocks. - * @type {boolean} - */ - this.dragging = false; - - /** - * Map of current graphic effect values. - * @type {!Record} - */ - this.effects = { - color: 0, - fisheye: 0, - whirl: 0, - pixelate: 0, - mosaic: 0, - brightness: 0, - ghost: 0 - }; - - /** - * Whether this represents an "original" non-clone rendered-target for a sprite, - * i.e., created by the editor and not clone blocks. - * @type {boolean} - */ - this.isOriginal = true; - - /** - * Whether this rendered target represents the Scratch stage. - * @type {boolean} - */ - this.isStage = false; - - /** - * Scratch X coordinate. Currently should range from -240 to 240. - * @type {number} - */ - this.x = 0; - - /** - * Scratch Y coordinate. Currently should range from -180 to 180. - * @type {number} - */ - this.y = 0; - - /** - * Scratch direction. Currently should range from -179 to 180. - * @type {number} - */ - this.direction = 90; - - /** - * Whether the rendered target is draggable on the stage - * @type {boolean} - */ - this.draggable = false; - - /** - * Whether the rendered target is currently visible. - * @type {boolean} - */ - this.visible = true; - - /** - * Size of rendered target as a percent of costume size. - * @type {number} - */ - this.size = 100; - - /** - * Currently selected costume index. - * @type {number} - */ - this.currentCostume = 0; - - /** - * Current rotation style. - * @type {!string} - */ - this.rotationStyle = RenderedTarget.ROTATION_STYLE_ALL_AROUND; - - /** - * Loudness for sound playback for this target, as a percentage. - * @type {number} - */ - this.volume = 100; - - /** - * Current tempo (used by the music extension). - * This property is global to the project and stored in the stage. - * @type {number} - */ - this.tempo = 60; - - /** - * The transparency of the video (used by extensions with camera input). - * This property is global to the project and stored in the stage. - * @type {number} - */ - this.videoTransparency = 50; - - /** - * The state of the video input (used by extensions with camera input). - * This property is global to the project and stored in the stage. - * - * Defaults to ON. This setting does not turn the video by itself. A - * video extension once loaded will set the video device to this - * setting. Set to ON when a video extension is added in the editor the - * video will start ON. If the extension is loaded as part of loading a - * saved project the extension will see the value set when the stage - * was loaded from the saved values including the video state. - * - * @type {string} - */ - this.videoState = RenderedTarget.VIDEO_STATE.ON; - - /** - * The language to use for speech synthesis, in the text2speech extension. - * It is initialized to null so that on extension load, we can check for - * this and try setting it using the editor locale. - * @type {string} - */ - this.textToSpeechLanguage = null; + this.renderer = this.runtime?.renderer ?? null; } /** * Create a drawable with the this.renderer. - * @param {string} layerGroup The layer group this drawable should be added to + * @param layerGroup The layer group this drawable should be added to */ - initDrawable (layerGroup) { + initDrawable (layerGroup: StageLayer) { if (this.renderer) { this.drawableID = this.renderer.createDrawable(layerGroup); } @@ -194,7 +190,7 @@ class RenderedTarget extends Target { const bank = this.sprite.soundBank; const audioPlayerProxy = { - playSound: soundId => bank.play(this, soundId) + playSound: (soundId: string) => bank?.playSound(this, soundId) }; Object.defineProperty(this, 'audioPlayer', { @@ -215,42 +211,37 @@ class RenderedTarget extends Target { /** * Event which fires when a target moves. - * @type {string} */ static get EVENT_TARGET_MOVED () { - return 'TARGET_MOVED'; + return 'TARGET_MOVED' as const; } /** * Event which fires when a target changes visually, for updating say bubbles. - * @type {string} */ static get EVENT_TARGET_VISUAL_CHANGE () { - return 'EVENT_TARGET_VISUAL_CHANGE'; + return 'EVENT_TARGET_VISUAL_CHANGE' as const; } /** * Rotation style for "all around"/spinning. - * @type {string} */ static get ROTATION_STYLE_ALL_AROUND () { - return 'all around'; + return 'all around' as const; } /** * Rotation style for "left-right"/flipping. - * @type {string} */ static get ROTATION_STYLE_LEFT_RIGHT () { - return 'left-right'; + return 'left-right' as const; } /** * Rotation style for "no rotation." - * @type {string} */ static get ROTATION_STYLE_NONE () { - return "don't rotate"; + return "don't rotate" as const; } /** @@ -262,27 +253,27 @@ class RenderedTarget extends Target { OFF: 'off', ON: 'on', ON_FLIPPED: 'on-flipped' - }; + } as const; } /** * Set the X and Y coordinates. - * @param {!number} x New X coordinate, in Scratch coordinates. - * @param {!number} y New Y coordinate, in Scratch coordinates. - * @param {?boolean} [force] Force setting X/Y, in case of dragging + * @param x New X coordinate, in Scratch coordinates. + * @param y New Y coordinate, in Scratch coordinates. + * @param force Force setting X/Y, in case of dragging */ - setXY (x, y, force) { + setXY (x: number, y: number, force?: boolean) { if (this.isStage) return; if (this.dragging && !force) return; const oldX = this.x; const oldY = this.y; if (this.renderer) { - const position = this.runtime.limitOptions.edgelessStage ? - [x, y] : this.renderer.getFencedPositionOfDrawable(this.drawableID, [x, y]); + const position: [number, number] = this.runtime.limitOptions.edgelessStage ? + [x, y] : this.renderer.getFencedPositionOfDrawable(this.drawableID!, [x, y]); this.x = position[0]; this.y = position[1]; - this.renderer.updateDrawablePosition(this.drawableID, position); + this.renderer.updateDrawablePosition(this.drawableID!, position); if (this.visible) { this.emit(RenderedTarget.EVENT_TARGET_VISUAL_CHANGE, this); this.runtime.requestRedraw(); @@ -297,12 +288,12 @@ class RenderedTarget extends Target { /** * Get the rendered direction and scale, after applying rotation style. - * @returns {Record} Direction and scale to render. + * @returns Direction and scale to render. */ _getRenderedDirectionAndScale () { // Default: no changes to `this.direction` or `this.scale`. let finalDirection = this.direction; - let finalScale = [this.size, this.size]; + let finalScale: number[] = [this.size, this.size]; if (this.rotationStyle === RenderedTarget.ROTATION_STYLE_NONE) { // Force rendered direction to be 90. finalDirection = 90; @@ -317,9 +308,9 @@ class RenderedTarget extends Target { /** * Set the direction. - * @param {!number} direction New direction. + * @param direction New direction. */ - setDirection (direction) { + setDirection (direction: number) { if (this.isStage) { return; } @@ -330,7 +321,7 @@ class RenderedTarget extends Target { this.direction = MathUtil.wrapClamp(direction, -179, 180); if (this.renderer) { const {direction: renderedDirection, scale} = this._getRenderedDirectionAndScale(); - this.renderer.updateDrawableDirectionScale(this.drawableID, renderedDirection, scale); + this.renderer.updateDrawableDirectionScale(this.drawableID!, renderedDirection, scale); if (this.visible) { this.emit(RenderedTarget.EVENT_TARGET_VISUAL_CHANGE, this); this.runtime.requestRedraw(); @@ -341,9 +332,9 @@ class RenderedTarget extends Target { /** * Set draggability; i.e., whether it's able to be dragged in the player - * @param {!boolean} draggable True if should be draggable. + * @param draggable True if should be draggable. */ - setDraggable (draggable) { + setDraggable (draggable: boolean) { if (this.isStage) return; this.draggable = !!draggable; this.runtime.requestTargetsUpdate(this); @@ -351,15 +342,15 @@ class RenderedTarget extends Target { /** * Set visibility; i.e., whether it's shown or hidden. - * @param {!boolean} visible True if should be shown. + * @param visible True if should be shown. */ - setVisible (visible) { + setVisible (visible: boolean) { if (this.isStage) { return; } this.visible = !!visible; if (this.renderer) { - this.renderer.updateDrawableVisible(this.drawableID, this.visible); + this.renderer.updateDrawableVisible(this.drawableID!, this.visible); if (this.visible) { this.emit(RenderedTarget.EVENT_TARGET_VISUAL_CHANGE, this); this.runtime.requestRedraw(); @@ -370,16 +361,16 @@ class RenderedTarget extends Target { /** * Set size, as a percentage of the costume size. - * @param {!number} size Size of rendered target, as % of costume size. + * @param size Size of rendered target, as % of costume size. */ - setSize (size) { + setSize (size: number) { if (this.isStage) { return; } if (this.renderer) { // Clamp to scales relative to costume and stage size. // See original ScratchSprite.as:setSize. - const costumeSize = this.renderer.getCurrentSkinSize(this.drawableID); + const costumeSize = this.renderer.getCurrentSkinSize(this.drawableID!); const origW = costumeSize[0]; const origH = costumeSize[1]; const minScale = this.runtime.limitOptions.edgelessStage ? @@ -391,7 +382,7 @@ class RenderedTarget extends Target { ); this.size = MathUtil.clamp(size / 100, minScale, maxScale) * 100; const {direction, scale} = this._getRenderedDirectionAndScale(); - this.renderer.updateDrawableDirectionScale(this.drawableID, direction, scale); + this.renderer.updateDrawableDirectionScale(this.drawableID!, direction, scale); if (this.visible) { this.emit(RenderedTarget.EVENT_TARGET_VISUAL_CHANGE, this); this.runtime.requestRedraw(); @@ -402,14 +393,14 @@ class RenderedTarget extends Target { /** * Set a particular graphic effect value. - * @param {!string} effectName Name of effect (see `RenderedTarget.prototype.effects`). - * @param {!number} value Numerical magnitude of effect. + * @param effectName Name of effect (see {@link RenderedTarget.effects}). + * @param value Numerical magnitude of effect. */ - setEffect (effectName, value) { + setEffect (effectName: string, value: number) { if (!Object.prototype.hasOwnProperty.call(this.effects, effectName)) return; this.effects[effectName] = value; if (this.renderer) { - this.renderer.updateDrawableEffect(this.drawableID, effectName, value); + this.renderer.updateDrawableEffect(this.drawableID!, effectName, value); if (this.visible) { this.emit(RenderedTarget.EVENT_TARGET_VISUAL_CHANGE, this); this.runtime.requestRedraw(); @@ -428,7 +419,7 @@ class RenderedTarget extends Target { if (this.renderer) { for (const effectName in this.effects) { if (!Object.prototype.hasOwnProperty.call(this.effects, effectName)) continue; - this.renderer.updateDrawableEffect(this.drawableID, effectName, 0); + this.renderer.updateDrawableEffect(this.drawableID!, effectName, 0); } if (this.visible) { this.emit(RenderedTarget.EVENT_TARGET_VISUAL_CHANGE, this); @@ -439,9 +430,9 @@ class RenderedTarget extends Target { /** * Set the current costume. - * @param {number} index New index of costume. + * @param index New index of costume. */ - setCostume (index) { + setCostume (index: number) { // Keep the costume index within possible values. index = Math.round(index); if ([Infinity, -Infinity, NaN].includes(index)) index = 0; @@ -451,7 +442,7 @@ class RenderedTarget extends Target { ); if (this.renderer) { const costume = this.getCostumes()[this.currentCostume]; - this.renderer.updateDrawableSkinId(this.drawableID, costume.skinId); + this.renderer.updateDrawableSkinId(this.drawableID!, costume.skinId); if (this.visible) { this.emit(RenderedTarget.EVENT_TARGET_VISUAL_CHANGE, this); @@ -463,10 +454,10 @@ class RenderedTarget extends Target { /** * Add a costume, taking care to avoid duplicate names. - * @param {!object} costumeObject Object representing the costume. - * @param {?int} index Index at which to add costume + * @param costumeObject Object representing the costume. + * @param index Index at which to add costume */ - addCostume (costumeObject, index) { + addCostume (costumeObject: Costume, index?: number) { if (typeof index === 'number' && !isNaN(index)) { this.sprite.addCostumeAt(costumeObject, index); } else { @@ -476,10 +467,10 @@ class RenderedTarget extends Target { /** * Rename a costume, taking care to avoid duplicate names. - * @param {int} costumeIndex - the index of the costume to be renamed. - * @param {string} newName - the desired new name of the costume (will be modified if already in use). + * @param costumeIndex - the index of the costume to be renamed. + * @param newName - the desired new name of the costume (will be modified if already in use). */ - renameCostume (costumeIndex, newName) { + renameCostume (costumeIndex: number, newName: string) { const usedNames = this.sprite.costumes .filter((costume, index) => costumeIndex !== index) .map(costume => costume.name); @@ -503,12 +494,12 @@ class RenderedTarget extends Target { /** * Delete a costume by index. - * @param {number} index Costume index to be deleted - * @returns {?object} The costume that was deleted or null + * @param index Costume index to be deleted + * @returns The costume that was deleted or null * if the index was out of bounds of the costumes list or * this target only has one costume. */ - deleteCostume (index) { + deleteCostume (index: number): Costume | null { const originalCostumeCount = this.sprite.costumes.length; if (originalCostumeCount === 1) return null; @@ -532,10 +523,10 @@ class RenderedTarget extends Target { /** * Add a sound, taking care to avoid duplicate names. - * @param {!object} soundObject Object representing the sound. - * @param {?int} index Index at which to add costume + * @param soundObject Object representing the sound. + * @param index Index at which to add costume */ - addSound (soundObject, index) { + addSound (soundObject: Sound, index?: number) { const usedNames = this.sprite.sounds.map(sound => sound.name); soundObject.name = StringUtil.unusedName(soundObject.name, usedNames); if (typeof index === 'number' && !isNaN(index)) { @@ -547,10 +538,10 @@ class RenderedTarget extends Target { /** * Rename a sound, taking care to avoid duplicate names. - * @param {int} soundIndex - the index of the sound to be renamed. - * @param {string} newName - the desired new name of the sound (will be modified if already in use). + * @param soundIndex - the index of the sound to be renamed. + * @param newName - the desired new name of the sound (will be modified if already in use). */ - renameSound (soundIndex, newName) { + renameSound (soundIndex: number, newName: string) { const usedNames = this.sprite.sounds .filter((sound, index) => soundIndex !== index) .map(sound => sound.name); @@ -562,10 +553,10 @@ class RenderedTarget extends Target { /** * Delete a sound by index. - * @param {number} index Sound index to be deleted - * @returns {object} The deleted sound object, or null if no sound was deleted. + * @param index Sound index to be deleted + * @returns The deleted sound object, or null if no sound was deleted. */ - deleteSound (index) { + deleteSound (index: number): Sound | null { // Make sure the sound index is not out of bounds if (index < 0 || index >= this.sprite.sounds.length) { return null; @@ -578,9 +569,9 @@ class RenderedTarget extends Target { /** * Update the rotation style. - * @param {!string} rotationStyle New rotation style. + * @param rotationStyle New rotation style. */ - setRotationStyle (rotationStyle) { + setRotationStyle (rotationStyle: string) { if (rotationStyle === RenderedTarget.ROTATION_STYLE_NONE) { this.rotationStyle = RenderedTarget.ROTATION_STYLE_NONE; } else if (rotationStyle === RenderedTarget.ROTATION_STYLE_ALL_AROUND) { @@ -590,7 +581,7 @@ class RenderedTarget extends Target { } if (this.renderer) { const {direction, scale} = this._getRenderedDirectionAndScale(); - this.renderer.updateDrawableDirectionScale(this.drawableID, direction, scale); + this.renderer.updateDrawableDirectionScale(this.drawableID!, direction, scale); if (this.visible) { this.emit(RenderedTarget.EVENT_TARGET_VISUAL_CHANGE, this); this.runtime.requestRedraw(); @@ -601,10 +592,10 @@ class RenderedTarget extends Target { /** * Get a costume index of this rendered target, by name of the costume. - * @param {?string} costumeName Name of a costume. - * @returns {number} Index of the named costume, or -1 if not present. + * @param costumeName Name of a costume. + * @returns Index of the named costume, or -1 if not present. */ - getCostumeIndexByName (costumeName) { + getCostumeIndexByName (costumeName: string): number { for (let i = 0; i < this.sprite.costumes.length; i++) { if (this.getCostumes()[i].name === costumeName) { return i; @@ -615,9 +606,9 @@ class RenderedTarget extends Target { /** * Get a costume of this rendered target by id. - * @returns {object} current costume + * @returns current costume */ - getCurrentCostume () { + getCurrentCostume (): Costume { return this.getCostumes()[this.currentCostume]; } @@ -625,17 +616,17 @@ class RenderedTarget extends Target { * Get full costume list * @returns list of costumes */ - getCostumes () { + getCostumes (): Costume[] { return this.sprite.costumes; } /** * Reorder costume list by moving costume at costumeIndex to newIndex. - * @param {!number} costumeIndex Index of the costume to move. - * @param {!number} newIndex New index for that costume. - * @returns {boolean} If a change occurred (i.e. if the indices do not match) + * @param costumeIndex Index of the costume to move. + * @param newIndex New index for that costume. + * @returns If a change occurred (i.e. if the indices do not match) */ - reorderCostume (costumeIndex, newIndex) { + reorderCostume (costumeIndex: number, newIndex: number): boolean { newIndex = MathUtil.clamp(newIndex, 0, this.sprite.costumes.length - 1); costumeIndex = MathUtil.clamp(costumeIndex, 0, this.sprite.costumes.length - 1); @@ -654,11 +645,11 @@ class RenderedTarget extends Target { /** * Reorder sound list by moving sound at soundIndex to newIndex. - * @param {!number} soundIndex Index of the sound to move. - * @param {!number} newIndex New index for that sound. - * @returns {boolean} If a change occurred (i.e. if the indices do not match) + * @param soundIndex Index of the sound to move. + * @param newIndex New index for that sound. + * @returns If a change occurred (i.e. if the indices do not match) */ - reorderSound (soundIndex, newIndex) { + reorderSound (soundIndex: number, newIndex: number): boolean { newIndex = MathUtil.clamp(newIndex, 0, this.sprite.sounds.length - 1); soundIndex = MathUtil.clamp(soundIndex, 0, this.sprite.sounds.length - 1); @@ -672,9 +663,9 @@ class RenderedTarget extends Target { /** * Get full sound list - * @returns {object[]} list of sounds + * @returns list of sounds */ - getSounds () { + getSounds (): Sound[] { return this.sprite.sounds; } @@ -685,16 +676,16 @@ class RenderedTarget extends Target { updateAllDrawableProperties () { if (this.renderer) { const {direction, scale} = this._getRenderedDirectionAndScale(); - this.renderer.updateDrawablePosition(this.drawableID, [this.x, this.y]); - this.renderer.updateDrawableDirectionScale(this.drawableID, direction, scale); - this.renderer.updateDrawableVisible(this.drawableID, this.visible); + this.renderer.updateDrawablePosition(this.drawableID!, [this.x, this.y]); + this.renderer.updateDrawableDirectionScale(this.drawableID!, direction, scale); + this.renderer.updateDrawableVisible(this.drawableID!, this.visible); const costume = this.getCostumes()[this.currentCostume]; - this.renderer.updateDrawableSkinId(this.drawableID, costume.skinId); + this.renderer.updateDrawableSkinId(this.drawableID!, costume.skinId); for (const effectName in this.effects) { if (!Object.prototype.hasOwnProperty.call(this.effects, effectName)) continue; - this.renderer.updateDrawableEffect(this.drawableID, effectName, this.effects[effectName]); + this.renderer.updateDrawableEffect(this.drawableID!, effectName, this.effects[effectName]); } if (this.visible) { @@ -708,17 +699,17 @@ class RenderedTarget extends Target { /** * Return the human-readable name for this rendered target, e.g., the sprite's name. * @override - * @returns {string} Human-readable name. + * @returns Human-readable name. */ - getName () { + getName (): string { return this.sprite.name; } /** * Return whether this rendered target is a sprite (not a clone, not the stage). - * @returns {boolean} True if not a clone and not the stage. + * @returns True if not a clone and not the stage. */ - isSprite () { + isSprite (): boolean { return !this.isStage && this.isOriginal; } @@ -727,9 +718,9 @@ class RenderedTarget extends Target { * Includes top, left, bottom, right attributes in Scratch coordinates. * @returns Tight bounding box, or null. */ - getBounds () { + getBounds (): Rectangle | null { if (this.renderer) { - return this.runtime.renderer.getBounds(this.drawableID); + return this.runtime.renderer!.getBounds(this.drawableID!); } return null; } @@ -737,21 +728,21 @@ class RenderedTarget extends Target { /** * Return the bounding box around a slice of the top 8px of the rendered target. * Includes top, left, bottom, right attributes in Scratch coordinates. - * @returns {?object} Tight bounding box, or null. + * @returns Tight bounding box, or null. */ - getBoundsForBubble () { + getBoundsForBubble (): object | null { if (this.renderer) { - return this.runtime.renderer.getBoundsForBubble(this.drawableID); + return this.runtime.renderer!.getBoundsForBubble(this.drawableID!); } return null; } /** * Return whether this target is touching the mouse, an edge, or a sprite. - * @param {string} requestedObject an id for mouse or edge, or a sprite name. - * @returns {boolean} True if the sprite is touching the object. + * @param requestedObject an id for mouse or edge, or a sprite name. + * @returns True if the sprite is touching the object. */ - isTouchingObject (requestedObject) { + isTouchingObject (requestedObject: string): boolean { if (requestedObject === '_mouse_') { if (!this.runtime.ioDevices.mouse) return false; const mouseX = this.runtime.ioDevices.mouse.getClientX(); @@ -765,26 +756,27 @@ class RenderedTarget extends Target { /** * Return whether touching a point. - * @param {number} x X coordinate of test point. - * @param {number} y Y coordinate of test point. - * @returns {boolean} True iff the rendered target is touching the point. + * @param x X coordinate of test point. + * @param y Y coordinate of test point. + * @returns True iff the rendered target is touching the point. */ - isTouchingPoint (x, y) { + isTouchingPoint (x: number, y: number): boolean { if (this.renderer) { - return this.renderer.drawableTouching(this.drawableID, x, y); + return this.renderer.drawableTouching(this.drawableID!, x, y); } return false; } /** * Return whether touching a stage edge. - * @returns {boolean} True iff the rendered target is touching the stage edge. + * @returns True iff the rendered target is touching the stage edge. */ - isTouchingEdge () { + isTouchingEdge (): boolean { if (this.renderer) { const stageWidth = this.runtime.stageWidth; const stageHeight = this.runtime.stageHeight; const bounds = this.getBounds(); + if (!bounds) return false; if (bounds.left < -stageWidth / 2 || bounds.right > stageWidth / 2 || bounds.top > stageHeight / 2 || @@ -797,10 +789,10 @@ class RenderedTarget extends Target { /** * Return whether touching any of a named sprite's clones. - * @param {string} spriteName Name of the sprite. - * @returns {boolean} True iff touching a clone of the sprite. + * @param spriteName Name of the sprite. + * @returns True iff touching a clone of the sprite. */ - isTouchingSprite (spriteName) { + isTouchingSprite (spriteName: string): boolean { spriteName = Cast.toString(spriteName); const firstClone = this.runtime.getSpriteTargetByName(spriteName); if (!firstClone || !this.renderer) { @@ -810,33 +802,33 @@ class RenderedTarget extends Target { // can detect other sprites using touching , but cannot be detected // by other sprites while it is being dragged. This matches Scratch 2.0 behavior. const drawableCandidates = firstClone.sprite.clones.filter(clone => !clone.dragging) - .map(clone => clone.drawableID); + .map(clone => clone.drawableID!); return this.renderer.isTouchingDrawables( - this.drawableID, drawableCandidates); + this.drawableID!, drawableCandidates); } /** * Return whether touching a color. - * @param {Array.} rgb [r,g,b], values between 0-255. - * @returns {Promise.} True iff the rendered target is touching the color. + * @param rgb [r,g,b], values between 0-255. + * @returns True iff the rendered target is touching the color. */ - isTouchingColor (rgb) { + isTouchingColor (rgb: number[]): boolean { if (this.renderer) { - return this.renderer.isTouchingColor(this.drawableID, rgb); + return this.renderer.isTouchingColor(this.drawableID!, rgb); } return false; } /** * Return whether rendered target's color is touching a color. - * @param {object} targetRgb {Array.} [r,g,b], values between 0-255. - * @param {object} maskRgb {Array.} [r,g,b], values between 0-255. - * @returns {Promise.} True iff the color is touching the color. + * @param targetRgb [r,g,b], values between 0-255. + * @param maskRgb [r,g,b], values between 0-255. + * @returns True iff the color is touching the color. */ - colorIsTouchingColor (targetRgb, maskRgb) { + colorIsTouchingColor (targetRgb: number[], maskRgb: number[]): boolean { if (this.renderer) { return this.renderer.isTouchingColor( - this.drawableID, + this.drawableID!, targetRgb, maskRgb ); @@ -844,9 +836,9 @@ class RenderedTarget extends Target { return false; } - getLayerOrder () { + getLayerOrder (): number | null { if (this.renderer) { - return this.renderer.getDrawableOrder(this.drawableID); + return this.renderer.getDrawableOrder(this.drawableID!); } return null; } @@ -858,7 +850,7 @@ class RenderedTarget extends Target { if (this.renderer) { // Let the renderer re-order the sprite based on its knowledge // of what layers are present - this.renderer.setDrawableOrder(this.drawableID, Infinity, StageLayering.SPRITE_LAYER); + this.renderer.setDrawableOrder(this.drawableID!, Infinity, StageLayering.SPRITE_LAYER); } this.runtime.setExecutablePosition(this, Infinity); @@ -871,7 +863,7 @@ class RenderedTarget extends Target { if (this.renderer) { // Let the renderer re-order the sprite based on its knowledge // of what layers are present - this.renderer.setDrawableOrder(this.drawableID, -Infinity, StageLayering.SPRITE_LAYER, false); + this.renderer.setDrawableOrder(this.drawableID!, -Infinity, StageLayering.SPRITE_LAYER, false); } this.runtime.setExecutablePosition(this, -Infinity); @@ -879,11 +871,11 @@ class RenderedTarget extends Target { /** * Move forward a number of layers. - * @param {number} nLayers How many layers to go forward. + * @param nLayers How many layers to go forward. */ - goForwardLayers (nLayers) { + goForwardLayers (nLayers: number) { if (this.renderer) { - this.renderer.setDrawableOrder(this.drawableID, nLayers, StageLayering.SPRITE_LAYER, true); + this.renderer.setDrawableOrder(this.drawableID!, nLayers, StageLayering.SPRITE_LAYER, true); } this.runtime.moveExecutable(this, nLayers); @@ -891,11 +883,11 @@ class RenderedTarget extends Target { /** * Move backward a number of layers. - * @param {number} nLayers How many layers to go backward. + * @param nLayers How many layers to go backward. */ - goBackwardLayers (nLayers) { + goBackwardLayers (nLayers: number) { if (this.renderer) { - this.renderer.setDrawableOrder(this.drawableID, -nLayers, StageLayering.SPRITE_LAYER, true); + this.renderer.setDrawableOrder(this.drawableID!, -nLayers, StageLayering.SPRITE_LAYER, true); } this.runtime.moveExecutable(this, -nLayers); @@ -903,13 +895,13 @@ class RenderedTarget extends Target { /** * Move behind some other rendered target. - * @param {!RenderedTarget} other Other rendered target to move behind. + * @param other Other rendered target to move behind. */ - goBehindOther (other) { + goBehindOther (other: RenderedTarget) { if (this.renderer) { const otherLayer = this.renderer.setDrawableOrder( - other.drawableID, 0, StageLayering.SPRITE_LAYER, true); - this.renderer.setDrawableOrder(this.drawableID, otherLayer, StageLayering.SPRITE_LAYER); + other.drawableID!, 0, StageLayering.SPRITE_LAYER, true); + this.renderer.setDrawableOrder(this.drawableID!, otherLayer!, StageLayering.SPRITE_LAYER); } const executionPosition = this.runtime.executableTargets.indexOf(other); @@ -918,12 +910,16 @@ class RenderedTarget extends Target { /** * Keep a desired position within a fence. - * @param {number} newX New desired X position. - * @param {number} newY New desired Y position. - * @param {object=} optFence Optional fence with left, right, top bottom. - * @returns {Array.} Fenced X and Y coordinates. + * @param newX New desired X position. + * @param newY New desired Y position. + * @param optFence Optional fence with left, right, top bottom. + * @returns Fenced X and Y coordinates. */ - keepInFence (newX, newY, optFence) { + keepInFence ( + newX: number, + newY: number, + optFence?: Fence + ): [number, number] | undefined { let fence = optFence; if (!fence) { fence = { @@ -961,9 +957,9 @@ class RenderedTarget extends Target { /** * Make a clone, copying any run-time properties. * If we've hit the global clone limit, returns null. - * @returns {RenderedTarget} New clone. + * @returns New clone. */ - makeClone () { + makeClone (): RenderedTarget | null { if (!this.runtime.clonesAvailable() || this.isStage) { return null; // Hit max clone limit, or this is the stage. } @@ -988,9 +984,9 @@ class RenderedTarget extends Target { /** * Make a duplicate using a duplicate sprite. - * @returns {RenderedTarget} New clone. + * @returns New clone. */ - duplicate () { + duplicate (): Promise { return this.sprite.duplicate().then(newSprite => { const newTarget = newSprite.createClone(); // Copy all properties. @@ -1028,29 +1024,29 @@ class RenderedTarget extends Target { /** * Post/edit sprite info. - * @param {object} data An object with sprite info data to set. + * @param data An object with sprite info data to set. */ - postSpriteInfo (data) { - const force = Object.prototype.hasOwnProperty.call(data, 'force') ? data.force : null; + postSpriteInfo (data: Partial) { + const force = Object.prototype.hasOwnProperty.call(data, 'force') ? data.force : undefined; const isXChanged = Object.prototype.hasOwnProperty.call(data, 'x'); const isYChanged = Object.prototype.hasOwnProperty.call(data, 'y'); if (isXChanged || isYChanged) { - this.setXY(isXChanged ? data.x : this.x, isYChanged ? data.y : this.y, force); + this.setXY(isXChanged ? data.x! : this.x, isYChanged ? data.y! : this.y, force); } if (Object.prototype.hasOwnProperty.call(data, 'direction')) { - this.setDirection(data.direction); + this.setDirection(data.direction!); } if (Object.prototype.hasOwnProperty.call(data, 'draggable')) { - this.setDraggable(data.draggable); + this.setDraggable(data.draggable!); } if (Object.prototype.hasOwnProperty.call(data, 'rotationStyle')) { - this.setRotationStyle(data.rotationStyle); + this.setRotationStyle(data.rotationStyle!); } if (Object.prototype.hasOwnProperty.call(data, 'visible')) { - this.setVisible(data.visible); + this.setVisible(data.visible!); } if (Object.prototype.hasOwnProperty.call(data, 'size')) { - this.setSize(data.size); + this.setSize(data.size!); } } @@ -1068,10 +1064,9 @@ class RenderedTarget extends Target { this.dragging = false; } - /** * Serialize sprite info, used when emitting events about the sprite - * @returns {object} Sprite data as a simple object + * @returns Sprite data as a simple object */ toJSON () { const costumes = this.getCostumes(); @@ -1111,7 +1106,7 @@ class RenderedTarget extends Target { this.runtime.stopForTarget(this); this.runtime.removeExecutable(this); this.sprite.removeClone(this); - if (this.renderer && this.drawableID !== null) { + if (this.renderer && typeof this.drawableID === 'number') { this.renderer.destroyDrawable(this.drawableID, this.isStage ? StageLayering.BACKGROUND_LAYER : StageLayering.SPRITE_LAYER); @@ -1125,10 +1120,10 @@ class RenderedTarget extends Target { /** * Whether a given target is a RenderedTarget, i.e., has drawable properties and can be rendered. - * @param {Target} target Target to check. - * @returns {target is RenderedTarget} True if the target is a RenderedTarget. + * @param target Target to check. + * @returns True if the target is a RenderedTarget. */ -export const isRenderedTarget = function (target) { +export const isRenderedTarget = function (target: Target): target is RenderedTarget { return 'drawableID' in target; }; diff --git a/packages/vm/src/sprites/sprite.ts b/packages/vm/src/sprites/sprite.ts index eac301fd8..418ee35ca 100644 --- a/packages/vm/src/sprites/sprite.ts +++ b/packages/vm/src/sprites/sprite.ts @@ -1,4 +1,4 @@ -import RenderedTarget from './rendered-target.js'; +import RenderedTarget from './rendered-target'; import Blocks from '../engine/blocks'; import {loadSoundFromAsset} from '../import/load-sound.js'; import {loadCostumeFromAsset} from '../import/load-costume.js'; @@ -18,13 +18,21 @@ export interface Costume { bitmapResolution: number; rotationCenterX: number; rotationCenterY: number; + asset: Asset; + broken?: { + asset: Asset; + }; } export interface Sound { + name: string; soundId: string; rate: number; sampleCount: number; asset: Asset; + broken?: { + asset: Asset; + }; md5: string; } @@ -123,7 +131,7 @@ class Sprite { * Defaults to the sprite layer group * @returns Newly created clone. */ - createClone (optLayerGroup: StageLayer) { + createClone (optLayerGroup?: StageLayer) { const newClone = new RenderedTarget(this, this.runtime); newClone.isOriginal = this.clones.length === 0; this.clones.push(newClone); diff --git a/packages/vm/test/integration/addSprite.js b/packages/vm/test/integration/addSprite.js index c7f8df88b..cc6c5dd24 100644 --- a/packages/vm/test/integration/addSprite.js +++ b/packages/vm/test/integration/addSprite.js @@ -3,7 +3,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {readFileToBuffer} from '../fixtures/readProjectFile.js'; import VirtualMachine from '../../src/virtual-machine.js'; -import RenderedTarget from '../../src/sprites/rendered-target.js'; +import RenderedTarget from '../../src/sprites/rendered-target'; const projectUri = path.resolve(__dirname, '../fixtures/default.sb2'); const project = readFileToBuffer(projectUri); diff --git a/packages/vm/test/integration/import_nested_sb2.js b/packages/vm/test/integration/import_nested_sb2.js index 24521ccba..fedf6a20d 100644 --- a/packages/vm/test/integration/import_nested_sb2.js +++ b/packages/vm/test/integration/import_nested_sb2.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {extractProjectJson} from '../fixtures/readProjectFile.js'; -import renderedTarget from '../../src/sprites/rendered-target.js'; +import renderedTarget from '../../src/sprites/rendered-target'; import runtime from '../../src/engine/runtime.js'; import {deserialize} from '../../src/serialization/sb2.js'; diff --git a/packages/vm/test/integration/import_sb2.js b/packages/vm/test/integration/import_sb2.js index 7ef6b78cd..5aa0a0d90 100644 --- a/packages/vm/test/integration/import_sb2.js +++ b/packages/vm/test/integration/import_sb2.js @@ -2,7 +2,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import makeTestStorage from '../fixtures/make-test-storage.js'; import {extractProjectJson} from '../fixtures/readProjectFile.js'; -import renderedTarget from '../../src/sprites/rendered-target.js'; +import renderedTarget from '../../src/sprites/rendered-target'; import runtime from '../../src/engine/runtime.js'; import {deserialize} from '../../src/serialization/sb2.js'; diff --git a/packages/vm/test/integration/internal-extension.js b/packages/vm/test/integration/internal-extension.js index a1fda9f45..c84117269 100644 --- a/packages/vm/test/integration/internal-extension.js +++ b/packages/vm/test/integration/internal-extension.js @@ -4,7 +4,7 @@ import BlockType from '../../src/extension-support/block-type'; import dispatch from '../../src/dispatch/central-dispatch'; import VirtualMachine from '../../src/virtual-machine.js'; import Sprite from '../../src/sprites/sprite'; -import RenderedTarget from '../../src/sprites/rendered-target.js'; +import RenderedTarget from '../../src/sprites/rendered-target'; // By default Central Dispatch works with the Worker class built into the browser. Tell it to use TinyWorker instead. dispatch.workerClass = Worker; diff --git a/packages/vm/test/unit/blocks_looks.js b/packages/vm/test/unit/blocks_looks.js index 2da32b9d0..63f1df94c 100644 --- a/packages/vm/test/unit/blocks_looks.js +++ b/packages/vm/test/unit/blocks_looks.js @@ -2,7 +2,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Looks from '../../src/blocks/scratch3_looks'; import Runtime from '../../src/engine/runtime.js'; import Sprite from '../../src/sprites/sprite'; -import RenderedTarget from '../../src/sprites/rendered-target.js'; +import RenderedTarget from '../../src/sprites/rendered-target'; const util = { target: { currentCostume: 0, // Internally, current costume is 0 indexed diff --git a/packages/vm/test/unit/blocks_motion.js b/packages/vm/test/unit/blocks_motion.js index 7bc4fd5ee..e721015b9 100644 --- a/packages/vm/test/unit/blocks_motion.js +++ b/packages/vm/test/unit/blocks_motion.js @@ -2,7 +2,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Motion from '../../src/blocks/scratch3_motion'; import Runtime from '../../src/engine/runtime.js'; import Sprite from '../../src/sprites/sprite'; -import RenderedTarget from '../../src/sprites/rendered-target.js'; +import RenderedTarget from '../../src/sprites/rendered-target'; import VirtualMachine from '../../src/index'; test('getPrimitives', t => { diff --git a/packages/vm/test/unit/blocks_sensing.js b/packages/vm/test/unit/blocks_sensing.js index dfbc8d3db..31510c852 100644 --- a/packages/vm/test/unit/blocks_sensing.js +++ b/packages/vm/test/unit/blocks_sensing.js @@ -2,7 +2,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Sensing from '../../src/blocks/scratch3_sensing'; import Runtime from '../../src/engine/runtime.js'; import Sprite from '../../src/sprites/sprite'; -import RenderedTarget from '../../src/sprites/rendered-target.js'; +import RenderedTarget from '../../src/sprites/rendered-target'; import BlockUtility from '../../src/engine/block-utility'; test('getPrimitives', t => { diff --git a/packages/vm/test/unit/engine_sequencer.js b/packages/vm/test/unit/engine_sequencer.js index 7e340f710..36d2ec03d 100644 --- a/packages/vm/test/unit/engine_sequencer.js +++ b/packages/vm/test/unit/engine_sequencer.js @@ -2,7 +2,7 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Sequencer from '../../src/engine/sequencer'; import Runtime from '../../src/engine/runtime.js'; import Thread from '../../src/engine/thread'; -import RenderedTarget from '../../src/sprites/rendered-target.js'; +import RenderedTarget from '../../src/sprites/rendered-target'; import Sprite from '../../src/sprites/sprite'; test('spec', t => { diff --git a/packages/vm/test/unit/engine_thread.js b/packages/vm/test/unit/engine_thread.js index 8ae18e142..0f2f1ee45 100644 --- a/packages/vm/test/unit/engine_thread.js +++ b/packages/vm/test/unit/engine_thread.js @@ -1,6 +1,6 @@ import {test} from '../fixtures/jest-tap-bridge.js'; import Thread from '../../src/engine/thread'; -import RenderedTarget from '../../src/sprites/rendered-target.js'; +import RenderedTarget from '../../src/sprites/rendered-target'; import Sprite from '../../src/sprites/sprite'; import Runtime from '../../src/engine/runtime.js'; diff --git a/packages/vm/test/unit/serialization_sb2.js b/packages/vm/test/unit/serialization_sb2.js index 026eb0d93..5f7c23b4d 100644 --- a/packages/vm/test/unit/serialization_sb2.js +++ b/packages/vm/test/unit/serialization_sb2.js @@ -1,7 +1,7 @@ import path from 'path'; import {test} from '../fixtures/jest-tap-bridge.js'; import {extractProjectJson} from '../fixtures/readProjectFile.js'; -import RenderedTarget from '../../src/sprites/rendered-target.js'; +import RenderedTarget from '../../src/sprites/rendered-target'; import Runtime from '../../src/engine/runtime.js'; import {deserialize} from '../../src/serialization/sb2.js'; diff --git a/packages/vm/test/unit/sprites_rendered-target.js b/packages/vm/test/unit/sprites_rendered-target.js index e693c09d8..5f6e44202 100644 --- a/packages/vm/test/unit/sprites_rendered-target.js +++ b/packages/vm/test/unit/sprites_rendered-target.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import RenderedTarget from '../../src/sprites/rendered-target.js'; +import RenderedTarget from '../../src/sprites/rendered-target'; import Sprite from '../../src/sprites/sprite'; import Runtime from '../../src/engine/runtime.js'; import FakeRenderer from '../fixtures/fake-renderer.js'; diff --git a/packages/vm/test/unit/virtual-machine.js b/packages/vm/test/unit/virtual-machine.js index 92d1c54a9..24da210ea 100644 --- a/packages/vm/test/unit/virtual-machine.js +++ b/packages/vm/test/unit/virtual-machine.js @@ -6,7 +6,7 @@ import adapter from '../../src/engine/adapter'; import events from '../fixtures/events.json'; import Renderer from '../fixtures/fake-renderer.js'; import Runtime from '../../src/engine/runtime.js'; -import RenderedTarget from '../../src/sprites/rendered-target.js'; +import RenderedTarget from '../../src/sprites/rendered-target'; test('deleteSound returns function after deleting or null if nothing was deleted', t => { const vm = new VirtualMachine(); diff --git a/packages/vm/test/unit/vm_collectAssets.js b/packages/vm/test/unit/vm_collectAssets.js index fe88e10d5..fd1140d29 100644 --- a/packages/vm/test/unit/vm_collectAssets.js +++ b/packages/vm/test/unit/vm_collectAssets.js @@ -1,5 +1,5 @@ import {test} from '../fixtures/jest-tap-bridge.js'; -import RenderedTarget from '../../src/sprites/rendered-target.js'; +import RenderedTarget from '../../src/sprites/rendered-target'; import Sprite from '../../src/sprites/sprite'; import VirtualMachine from '../../src/virtual-machine.js'; From 7a9f4a0ccd09a9346010cb5d7f24e3c60aab2f31 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Tue, 12 May 2026 19:23:58 +0800 Subject: [PATCH 07/40] :wrench: chore(vm): more neat ts usage Signed-off-by: SimonShiki --- packages/vm/src/engine/blocks-runtime-cache.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) 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(); } } } From 5a1d40bc8bab8c14a1124445b697a96116981434 Mon Sep 17 00:00:00 2001 From: SimonShiki Date: Tue, 12 May 2026 20:24:03 +0800 Subject: [PATCH 08/40] :truck: chore(vm,render,storage): migrate left io/*.js and load-costume.js Signed-off-by: SimonShiki --- packages/render/src/RenderWebGL.js | 8 +- packages/render/src/Skin.js | 4 +- packages/storage/src/Asset.ts | 2 +- packages/storage/src/ScratchStorage.ts | 4 +- packages/vm/src/engine/runtime.js | 9 +- .../vm/src/extensions/scratch3_boost/index.js | 2 +- .../vm/src/extensions/scratch3_ev3/index.js | 2 +- .../src/extensions/scratch3_gdx_for/index.js | 2 +- .../src/extensions/scratch3_microbit/index.js | 2 +- .../scratch3_video_sensing/index.js | 2 +- .../vm/src/extensions/scratch3_wedo2/index.js | 2 +- .../{load-costume.js => load-costume.ts} | 133 +++++++------- packages/vm/src/io/{ble.js => ble.ts} | 156 ++++++++++------- packages/vm/src/io/{bt.js => bt.ts} | 97 +++++++---- packages/vm/src/io/{cloud.js => cloud.ts} | 112 ++++++------ packages/vm/src/io/{mouse.js => mouse.ts} | 92 ++++++---- packages/vm/src/io/{video.js => video.ts} | 162 ++++++++++-------- packages/vm/src/playground/benchmark.js | 2 +- packages/vm/src/serialization/sb2.js | 2 +- packages/vm/src/serialization/sb3.js | 2 +- packages/vm/src/sprites/sprite.ts | 31 +++- packages/vm/src/virtual-machine.js | 2 +- packages/vm/test/integration/sb3-roundtrip.js | 2 +- packages/vm/test/unit/io_cloud.js | 2 +- packages/vm/test/unit/io_mouse.js | 2 +- 25 files changed, 479 insertions(+), 357 deletions(-) rename packages/vm/src/import/{load-costume.js => load-costume.ts} (77%) rename packages/vm/src/io/{ble.js => ble.ts} (54%) rename packages/vm/src/io/{bt.js => bt.ts} (60%) rename packages/vm/src/io/{cloud.js => cloud.ts} (51%) rename packages/vm/src/io/{mouse.js => mouse.ts} (61%) rename packages/vm/src/io/{video.js => video.ts} (57%) diff --git a/packages/render/src/RenderWebGL.js b/packages/render/src/RenderWebGL.js index 120a8b403..6d1a305ab 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) { @@ -800,7 +800,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 +810,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..18b7a5d88 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,7 +134,7 @@ export class ScratchStorage { assetType: IAssetType, dataFormat: DataFormat, data: AssetData, - id: AssetId, + id: AssetId | null, generateId: boolean ): Asset { if (!dataFormat) throw new Error('Tried to create asset without a dataFormat'); diff --git a/packages/vm/src/engine/runtime.js b/packages/vm/src/engine/runtime.js index 2482dd6ba..3bb8eb8c2 100644 --- a/packages/vm/src/engine/runtime.js +++ b/packages/vm/src/engine/runtime.js @@ -19,12 +19,12 @@ 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'; @@ -59,6 +59,7 @@ const defaultExtensionColors = ['#0FBD8C', '#0DA57A', '#0B8E69']; * @typedef {import('clipcc-audio')} AudioEngine * @typedef {import('clipcc-render')} RenderWebGL * @typedef {import('clipcc-storage').ScratchStorage} ScratchStorage + * @typedef {import('clipcc-svg-renderer').BitmapAdapter} BitmapAdapter */ /** @@ -1907,7 +1908,7 @@ 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} bitmapAdapter The adapter to attach. */ attachV2BitmapAdapter (bitmapAdapter) { this.v2BitmapAdapter = bitmapAdapter; diff --git a/packages/vm/src/extensions/scratch3_boost/index.js b/packages/vm/src/extensions/scratch3_boost/index.js index 18bbd8fc7..80364a73f 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'; diff --git a/packages/vm/src/extensions/scratch3_ev3/index.js b/packages/vm/src/extensions/scratch3_ev3/index.js index 91b0e5c58..f2bbab68c 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'; diff --git a/packages/vm/src/extensions/scratch3_gdx_for/index.js b/packages/vm/src/extensions/scratch3_gdx_for/index.js index 672439678..5d2a00b49 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'; diff --git a/packages/vm/src/extensions/scratch3_microbit/index.js b/packages/vm/src/extensions/scratch3_microbit/index.js index 42be2f6aa..dd58ffe86 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'; /** diff --git a/packages/vm/src/extensions/scratch3_video_sensing/index.js b/packages/vm/src/extensions/scratch3_video_sensing/index.js index b43091a5a..b7d54f47a 100644 --- a/packages/vm/src/extensions/scratch3_video_sensing/index.js +++ b/packages/vm/src/extensions/scratch3_video_sensing/index.js @@ -4,7 +4,7 @@ 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'; /** diff --git a/packages/vm/src/extensions/scratch3_wedo2/index.js b/packages/vm/src/extensions/scratch3_wedo2/index.js index 2557cfc2d..5b46d0b35 100644 --- a/packages/vm/src/extensions/scratch3_wedo2/index.js +++ b/packages/vm/src/extensions/scratch3_wedo2/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'; 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..1a5cabbc1 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/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..997610ede 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: unknown) => 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, + characteristicId: number, + onCharacteristicChanged: ((message: unknown) => 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, + characteristicId: number, + 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, + characteristicId: number, + 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); @@ -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/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..0ec439c08 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; +} + +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/mouse.js b/packages/vm/src/io/mouse.ts similarity index 61% rename from packages/vm/src/io/mouse.js rename to packages/vm/src/io/mouse.ts index 91f3886b5..55897728a 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,9 +62,25 @@ 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) { @@ -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/video.js b/packages/vm/src/io/video.ts similarity index 57% rename from packages/vm/src/io/video.js rename to packages/vm/src/io/video.ts index 983e060e5..803cc2c40 100644 --- a/packages/vm/src/io/video.js +++ b/packages/vm/src/io/video.ts @@ -1,78 +1,90 @@ import StageLayering from '../engine/stage-layering'; +import type Runtime from '../engine/runtime'; + +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; +} 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 +93,61 @@ class Video { * * ioDevices.video.requestVideo() * - * @returns {Promise.