Skip to content

Commit

Permalink
feat: add a block inflater that supports recycling
Browse files Browse the repository at this point in the history
  • Loading branch information
gonfunko committed Oct 3, 2024
1 parent 37e0f10 commit 84c49ce
Show file tree
Hide file tree
Showing 2 changed files with 166 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { registerFieldTextInputRemovable } from "./fields/field_textinput_remova
import { registerFieldVariableGetter } from "./fields/field_variable_getter.js";
import { registerFieldVariable } from "./fields/field_variable.js";
import { registerFieldVerticalSeparator } from "./fields/field_vertical_separator.js";
import { registerRecyclableBlockFlyoutInflater } from "./recyclable_block_flyout_inflater.js";

export * from "blockly/core";
export * from "./block_reporting.js";
Expand All @@ -85,6 +86,7 @@ export function inject(container, options) {
registerFieldVariableGetter();
registerFieldVariable();
registerFieldVerticalSeparator();
registerRecyclableBlockFlyoutInflater();

Object.assign(options, {
renderer: "scratch",
Expand Down
164 changes: 164 additions & 0 deletions src/recyclable_block_flyout_inflater.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/**
* @license
* Copyright 2024 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import * as Blockly from "blockly/core";

/**
* A block inflater that caches and reuses blocks to improve performance.
*/
export class RecyclableBlockFlyoutInflater extends Blockly.BlockFlyoutInflater {
recyclingEnabled = true;
recycledBlocks = new Map();

/**
* Toggles whether or not recycling is enabled.
*
* @param {boolean} enabled True if recycling should be enabled.
*/
setRecyclingEnabled(enabled) {
this.recyclingEnabled = enabled;
}

/**
* Creates a new block from the given block definition.
*
* @param {Object} blockDefinition The definition to create a block from.
* @returns {!Blockly.BlockSvg} The newly created block.
*/
createBlock(blockDefinition) {
const blockType = this.getTypeFromDefinition(blockDefinition);
return (
this.getRecycledBlock(blockType) ??
super.createBlock(blockDefinition, this.flyoutWorkspace)
);
}

/**
* Returns the type of a block from an XML or JSON block definition.
*
* @param blockDefinition {Object} The block definition to parse.
* @returns {string} The block type.
*/
getTypeFromDefinition(blockDefinition) {
if (blockDefinition["blockxml"]) {
const xml =
typeof blockDefinition["blockxml"] === "string"
? Blockly.utils.xml.textToDom(blockDefinition["blockxml"])
: blockDefinition["blockxml"];
return xml.getAttribute("type");
} else {
return blockDefinition["type"];
}
}

/**
* Puts a previously created block into the recycle bin and moves it to the
* top of the workspace. Used during large workspace swaps to limit the number
* of new DOM elements we need to create.
*
* @param block The block to recycle.
*/
recycleBlock(block) {
const xy = block.getRelativeToSurfaceXY();
block.moveBy(-xy.x, -xy.y);
this.recycledBlocks.set(block.type, block);
}

/**
* Returns a block from the cache of recycled blocks with the given type, or
* undefined if one cannot be found.
*
* @param blockType The type of the block to try to recycle.
* @returns The recycled block, or undefined if
* one could not be recycled.
*/
getRecycledBlock(blockType) {
const block = this.recycledBlocks.get(blockType);
this.recycledBlocks.delete(blockType);
return block;
}

/**
* Returns whether the given block can be recycled or not.
*
* @param block The block to check for recyclability.
* @returns True if the block can be recycled. False otherwise.
*/
blockIsRecyclable(block) {
if (!this.recyclingEnabled) {
return false;
}

// If the block needs to parse mutations, never recycle.
if (block.mutationToDom && block.domToMutation) {
return false;
}

if (!block.isEnabled()) {
return false;
}

for (const input of block.inputList) {
for (const field of input.fieldRow) {
// No variables.
if (field.referencesVariables()) {
return false;
}
if (field instanceof Blockly.FieldDropdown) {
if (field.isOptionListDynamic()) {
return false;
}
}
}
// Check children.
if (input.connection) {
const targetBlock =
/** @type {Blockly.BlockSvg} */
(input.connection.targetBlock());
if (targetBlock && !this.blockIsRecyclable(targetBlock)) {
return false;
}
}
}
return true;
}

/**
* Disposes of the provided block.
*
* @param {!Blockly.BlockSvg} element The block to dispose of.
*/
disposeElement(element) {
if (this.blockIsRecyclable(element)) {
this.removeListeners(element.id);
this.recycleBlock(element);
} else {
super.disposeElement(element);
}
}

/**
* Clears the cache of recycled blocks.
*/
emptyRecycledBlocks() {
this.recycledBlocks
.values()
.forEach((block) => block.dispose(false, false));
this.recycledBlocks.clear();
}
}

/**
* Registers the recyclable block flyout inflater.
*/
export function registerRecyclableBlockFlyoutInflater() {
Blockly.registry.unregister(Blockly.registry.Type.FLYOUT_INFLATER, "block");
Blockly.registry.register(
Blockly.registry.Type.FLYOUT_INFLATER,
"block",
RecyclableBlockFlyoutInflater
);
}

0 comments on commit 84c49ce

Please sign in to comment.