Skip to content

✨ feat(block): virtualized workspace#119

Open
SimonShiki wants to merge 38 commits into
dev/3.2from
feat/modern-blockly/virtualized
Open

✨ feat(block): virtualized workspace#119
SimonShiki wants to merge 38 commits into
dev/3.2from
feat/modern-blockly/virtualized

Conversation

@SimonShiki
Copy link
Copy Markdown
Member

@SimonShiki SimonShiki commented Dec 1, 2025

Overview

Blockly, as a visual programming tool, exhibits concerning performance when handling large programs. A significant bottleneck arises because Blockly renders all blocks svg in the workspace onto the screen. Once the number of blocks reaches a certain threshold, the workspace becomes extremely unresponsive. To prevent performance regression in the migrated clipcc-block, this pull request proposed an improvement: hiding all top blocks that are off-screen to enhance performance. Basic performance tests have confirmed this approach effectively improves rendering performance.

Proposals

  • Implement virtualize in stack-level
  • Implement virtualize in block-level (using QuadTree to track positions)
  • Truncate too-long dragging block stack based on viewport size

Known Issues

  • The virtualization check get dalayed while we listening VIEWPORT_CHANGE
    • Solution: Hook the workspace's maybeFireViewportChangeEvent to listen that event immediately. We also provide a immediate param to control whether we should use hooks to listen event changes.
  • virtualization won't performed after workspace resized
    • VIEWPORT_CHANGE won't get fired when resized. You should use workspace.setResizeHandlerWrapper to replace default resize handler and call virtualize by manual. It won't happen if you use immediate mode mentioned above.
  • Some blocks still invisible though visible while dragging.
  • Block flickers when block creates

Signed-off-by: SimonShiki <sinangentoo@gmail.com>
@SimonShiki SimonShiki self-assigned this Dec 1, 2025
@SimonShiki SimonShiki added feature New feature or request block Things related to clipcc-block. labels Dec 1, 2025
@cloudflare-workers-and-pages
Copy link
Copy Markdown

cloudflare-workers-and-pages Bot commented Dec 1, 2025

Deploying clipcc-preview with  Cloudflare Pages  Cloudflare Pages

Latest commit: 1a740cc
Status: ✅  Deploy successful!
Preview URL: https://0f2975ed.clipcc-preview.pages.dev
Branch Preview URL: https://feat-modern-blockly-virtuali.clipcc-preview.pages.dev

View logs

Signed-off-by: SimonShiki <sinangentoo@gmail.com>
@SimonShiki SimonShiki requested a review from alexcui03 December 1, 2025 13:19
@SimonShiki SimonShiki marked this pull request as ready for review December 1, 2025 13:20
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
not necessary for current calculation scale, but necessary if we virtualize in block level

Signed-off-by: SimonShiki <sinangentoo@gmail.com>
@SimonShiki SimonShiki marked this pull request as draft December 8, 2025 01:25
@SimonShiki SimonShiki force-pushed the feat/modern-blockly/virtualized branch from d23919e to 1d0f40d Compare December 8, 2025 11:25
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
…Children

Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
@SimonShiki SimonShiki marked this pull request as ready for review December 10, 2025 10:38
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements workspace virtualization for Blockly to improve performance when rendering large programs by hiding off-screen blocks. The implementation uses a QuadTree spatial data structure to efficiently track block positions and determine which blocks should be visible based on the current viewport.

Key Changes

  • Implements a VirtualizedManager class that manages block visibility using viewport-based culling and a QuadTree for efficient spatial queries
  • Adds a QuadTree utility class for managing block positions and performing range queries
  • Enables virtualization by default in the injectWorkspace function with an opt-out configuration option

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
packages/block/tsconfig.json Updates TypeScript target to ES2022 to enable WeakRef support required for virtualization
packages/block/tests/helpers/playground.ts Disables virtualization in test environment to prevent interference with existing tests
packages/block/src/virtualized_manager.ts Core implementation of the virtualization manager with event handling and block visibility management
packages/block/src/utils/quad_tree.ts QuadTree spatial data structure for efficient position-based queries of blocks
packages/block/src/styles/blockly.css CSS rules for hiding virtualized blocks using display:none and content-visibility
packages/block/src/index.ts Integrates virtualization into the public API with virtualized option in ClipCCBlockOptions

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +404 to +406
If false, it uses workspace change listener to update visibility, which may be delayed. and you
need to call `virtualize()` manually when window resized.
Default to true.
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The documentation comment has formatting inconsistencies with extra leading whitespace on line 404. The lines starting with "If false" should align with the rest of the parameter description. This makes the documentation harder to read and may cause issues with documentation generation tools.

Suggested change
If false, it uses workspace change listener to update visibility, which may be delayed. and you
need to call `virtualize()` manually when window resized.
Default to true.
* If false, it uses workspace change listener to update visibility, which may be delayed, and you
* need to call `virtualize()` manually when the window is resized.
* Default to true.

Copilot uses AI. Check for mistakes.
Comment thread packages/block/src/virtualized_manager.ts
Comment thread packages/block/src/virtualized_manager.ts
Comment thread packages/block/src/virtualized_manager.ts Outdated
Comment on lines +103 to +116
insert(item: T, rect: Blockly.utils.Rect): void {
if (this.level === 0) {
if (this.itemMap!.has(item)) {
this.remove(item);
}
this.itemMap!.set(item, rect);

while (!this.contains(this.bounds, rect)) {
this.grow(rect);
}
}

this.insertInternal(item, rect);
}
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The insert method can cause infinite growth of the QuadTree if the item's rectangle has extreme coordinates or the grow logic doesn't properly expand to contain the rectangle. The while loop at line 110-112 lacks a safety limit, which could lead to a stack overflow or excessive memory consumption. Add a maximum growth iterations check or validate that the rectangle has reasonable bounds before attempting to grow.

Copilot uses AI. Check for mistakes.
Comment thread packages/block/tsconfig.json Outdated

/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing the TypeScript target from ES2016 to ES2022 introduces a potential breaking change for environments that don't support ES2022 features. This change enables the use of WeakRef (used in virtualized_manager.ts) which requires ES2021+, but may break compatibility with older browsers or Node.js versions. Ensure this change is documented and that the minimum supported environment versions are updated accordingly. Consider whether the target should be ES2021 instead of ES2022 to match the actual features used.

Suggested change
"target": "es2022", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"target": "es2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */

Copilot uses AI. Check for mistakes.
Comment thread packages/block/src/virtualized_manager.ts
Comment on lines +72 to +73
protected get workspace(): Blockly.WorkspaceSvg {
return this.workspaceRef.deref()!;
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workspace getter assumes the WeakRef will always return a valid value by using the non-null assertion operator. However, the WeakRef can return undefined if the workspace has been garbage collected. While the comment suggests it "always exists since the manager is tied to the workspace's lifecycle," this isn't guaranteed since dispose() can be called independently and virtualizeInternal() already checks for workspace existence at line 204. Add a null check or handle the case where the workspace has been collected to prevent potential runtime errors.

Suggested change
protected get workspace(): Blockly.WorkspaceSvg {
return this.workspaceRef.deref()!;
/**
* Get the workspace from WeakRef.
* May return undefined if the workspace has been garbage collected.
*/
protected get workspace(): Blockly.WorkspaceSvg | undefined {
return this.workspaceRef.deref();

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +415
/**
* @license
* Copyright 2025 Clip Team
* SPDX-License-Identifier: Apache-2.0
*/

import * as Blockly from 'blockly/core';
import {QuadTree} from './utils/quad_tree';

export class VirtualizedManager {
/**
* Hold a weak reference to the workspace to avoid memory leaks.
*/
protected workspaceRef: WeakRef<Blockly.WorkspaceSvg>;
/**
* Blocks being observed for virtualization.
*/
protected observedBlocks = new Map<string, Blockly.BlockSvg>();
/**
* The quad tree used to manage block positions.
*/
protected quadTree: QuadTree<Blockly.BlockSvg>;
/**
* Set of blocks that are currently hidden.
*/
protected hiddenBlocks = new Set<Blockly.BlockSvg>();
/**
* Whether to update block visibility immediately on viewport changes.
* If true, workspace methods will be hooked to listen viewport changes immediately.
*/
protected immediate: boolean;
/**
* Whether a virtualization check has been requested.
* Any requested check will perform in next microtask.
*/
protected requestedCheck = false;

constructor(workspace: Blockly.WorkspaceSvg, immediate = true) {
this.workspaceRef = new WeakRef(workspace);
this.immediate = immediate;

const rect = this.getViewportRect();
const viewWidth = rect.right - rect.left;
const viewHeight = rect.bottom - rect.top;
const cx = (rect.left + rect.right) / 2;
const cy = (rect.top + rect.bottom) / 2;

const halfW = viewWidth * 2;
const halfH = viewHeight * 2;

this.quadTree = new QuadTree(
new Blockly.utils.Rect(cy - halfH, cy + halfH, cx - halfW, cx + halfW)
);

for (const block of workspace.getAllBlocks(false)) {
this.observe(block);
}

this.virtualize();

if (immediate) {
this.hookWorkspace();
}
workspace.addChangeListener(this.workspaceChangeListener);
}

/**
* Get the workspace from WeakRef.
* Always exists since the manager is tied to the workspace's lifecycle.
* @returns The workspace.
*/
protected get workspace(): Blockly.WorkspaceSvg {
return this.workspaceRef.deref()!;
}

/**
* Hook workspace methods to update block visibility immediately on viewport changes.
*/
protected hookWorkspace() {
const proto: Blockly.WorkspaceSvg = Object.getPrototypeOf(this.workspace);
// eslint-disable-next-line @typescript-eslint/no-this-alias
const manager = this;
const originalMaybeFireViewportChangeEvent = proto.maybeFireViewportChangeEvent;
this.workspace.maybeFireViewportChangeEvent = function() {
originalMaybeFireViewportChangeEvent.call(this);
manager.virtualize();
};

const originalResize = proto.resize;
this.workspace.resize = function() {
originalResize.call(this);
manager.virtualize();
};

const originalDispose = proto.dispose;
this.workspace.dispose = function() {
originalDispose.call(this);
manager.dispose();
};
}

protected workspaceChangeListener = (e: Blockly.Events.Abstract) => {
switch (e.type) {
case Blockly.Events.BLOCK_CREATE: {
const event = e as Blockly.Events.BlockCreate;
if (!event.ids) break;

for (const id of event.ids) {
const block = this.workspace.getBlockById(id);
if (block) this.observe(block);
}
this.virtualize();
break;
}
case Blockly.Events.BLOCK_DELETE: {
const event = e as Blockly.Events.BlockDelete;
if (!event.blockId) break;
this.unobserve(event.blockId);
this.virtualize();
break;
}
case Blockly.Events.BLOCK_MOVE:
case Blockly.Events.BLOCK_CHANGE: {
const event = e as (Blockly.Events.BlockMove | Blockly.Events.BlockChange);
if (!event.blockId) break;
if (event instanceof Blockly.Events.BlockChange) {
if (event.element === 'disabled' || event.element === 'comment') {
// No need to update position for these changes.
break;
}
}
const block = this.workspace.getBlockById(event.blockId);
if (block) {
const descendants = block.getDescendants(false);
for (const desc of descendants) {
if (this.observedBlocks.has(desc.id)) {
this.updateObserve(desc);
} else {
this.observe(desc);
}
}
}
break;
}
case Blockly.Events.BLOCK_DRAG: {
const event = e as Blockly.Events.BlockDrag;
if (!event.blockId) break;
const block = this.workspace.getBlockById(event.blockId);
if (!block) break;
this.addDraggingBuffer(block);
break;
}
case Blockly.Events.VIEWPORT_CHANGE: {
if (this.immediate) break;
this.virtualize();
break;
}
}
};

/**
* Check whether these blocks are offscreen, then update their visibility.
* This method performs in next microtask to batch multiple calls.
*/
protected virtualize(): void {
if (this.requestedCheck) return;
this.requestedCheck = true;

// Perform in next microtask.
Promise.resolve().then(() => {
this.requestedCheck = false;
this.virtualizeInternal();
});
}

/**
* Get the current viewport rectangle in workspace coordinates.
* @returns The viewport rectangle.
*/
protected getViewportRect(): Blockly.utils.Rect {
const scale = this.workspace.getScale();
const metrics = this.workspace.getMetrics();
// metrics.flyoutWidth always return 0 since it's not always open.
const flyoutWidth = this.workspace.getFlyout()?.getWidth() ?? 0;
const viewLeft = (metrics.viewLeft - flyoutWidth) / scale;
const viewTop = metrics.viewTop / scale;
const viewWidth = (metrics.viewWidth + flyoutWidth) / scale;
const viewHeight = metrics.viewHeight / scale;

return new Blockly.utils.Rect(
viewTop,
viewTop + viewHeight,
viewLeft,
viewLeft + viewWidth
);
}

/**
* Actual logic to check whether these blocks are offscreen,
* then update their visibility.
*/
protected virtualizeInternal(): void {
// Check workspace here since it may get called in public.
if (!this.workspace) {
this.dispose();
return;
}

const viewRect = this.getViewportRect();
const visibleBlocks = new Set(this.quadTree.query(viewRect));
// Found the last visible blocks in their stacks.
const lastVisibleBlocks = new Set<Blockly.BlockSvg>();
const processedIds = new Set<string>();
for (const block of visibleBlocks) {
let current: Blockly.BlockSvg | null = block;
while (current) {
// Avoid processing the same block again.
if (processedIds.has(current.id)) break;
processedIds.add(current.id);
const next = current.getNextBlock();
if (!next) { // Reached the end of the stack...
lastVisibleBlocks.add(current); // then it's the last visible block!
break;
}
if (!visibleBlocks.has(next)) { // Next block is not visible...
lastVisibleBlocks.add(current); // then it's the last visible block!
break;
}
current = next;
}
}

const blocksToHide = new Set<Blockly.BlockSvg>();
const blocksToShow = new Set<Blockly.BlockSvg>();

// Update top block's visibility
const topBlocks = this.workspace.getTopBlocks(false);
for (const block of topBlocks) {
if (!visibleBlocks.has(block)) {
blocksToHide.add(block);
}
}

// Hide the block after the last visible blocks if has.
for (const block of lastVisibleBlocks) {
const root = block.getRootBlock();
blocksToHide.delete(root);
const target = block.getNextBlock();
if (target) {
blocksToHide.add(target);
}

// Show previous hidden blocks
let prev: Blockly.BlockSvg | null = block;
while (prev) {
if (blocksToShow.has(prev)) break;
blocksToShow.add(prev);
blocksToHide.delete(prev);
prev = prev.getPreviousBlock();
}
}

// Finally hide proposed blocks
for (const block of blocksToHide) {
if (blocksToShow.has(block)) continue;
if (!this.hiddenBlocks.has(block)) {
this.setBlockVisibility(block, false);
}
}

for (const block of blocksToShow) {
if (this.hiddenBlocks.has(block)) {
this.setBlockVisibility(block, true);
}
}
}

/**
* Display possible-visible blocks during dragging.
* @param block The block to add extra buffer blocks.
*/
protected addDraggingBuffer(block: Blockly.BlockSvg): void {
const scale = this.workspace.getScale();
const metrics = this.workspace.getMetrics();
const viewHeight = metrics.viewHeight / scale;
const buffer = Math.ceil(viewHeight / block.height) * 2;
let current: Blockly.BlockSvg | null = block.getNextBlock();
for (let i = 0; i < buffer; ++i) {
if (!current) break;
if (this.hiddenBlocks.has(current)) {
this.setBlockVisibility(current, true);
}
current = current.getNextBlock();
}
if (current) {
this.setBlockVisibility(current, false);
}
}

/**
* Check whether a block is visible.
* @param block The block to check.
* @returns Whether the block is visible.
*/
protected isBlockVisible(block: Blockly.BlockSvg): boolean {
return !this.hiddenBlocks.has(block);
}

/**
* Set the visibility of a block.
* @param block The block to set visibility for.
* @param visible Whether the block visible.
*/
protected setBlockVisibility(block: Blockly.BlockSvg, visible: boolean): void {
const svgRoot = block.getSvgRoot();
if (visible) {
Blockly.utils.dom.removeClass(svgRoot, 'blocklyVirtualizedHidden');
this.hiddenBlocks.delete(block);
} else {
Blockly.utils.dom.addClass(svgRoot, 'blocklyVirtualizedHidden');
this.hiddenBlocks.add(block);
}
}

/**
* Start observing a block for virtualization.
* @param block The block to observe.
*/
protected observe(block: Blockly.BlockSvg): void {
if (this.observedBlocks.has(block.id)) return;
// Track statement blocks only.
if (block.outputConnection?.isConnected()) return;

const rect = this.getBlockBoundingRect(block);
this.observedBlocks.set(block.id, block);
this.quadTree.insert(block, rect);
}

/**
* Stop observing a block.
* @param blockId The block ID to stop observing.
*/
protected unobserve(blockId: string): void {
const block = this.observedBlocks.get(blockId);
if (!block) return;
this.observedBlocks.delete(blockId);
this.quadTree.remove(block);
// Make sure the block is visible when unobserved.
this.setBlockVisibility(block, true);
}

/**
* Update the status for observed block.
* @param block The block to update.
*/
protected updateObserve(block: Blockly.BlockSvg): void {
if (!this.observedBlocks.has(block.id)) return;
if (block.outputConnection?.isConnected()) {
this.unobserve(block.id);
return;
}

const rect = this.getBlockBoundingRect(block);
this.quadTree.insert(block, rect);
}

/**
* Get the bounding rectangle of a block relative to the workspace surface.
* Consider inputs but not next blocks.
* @param block The block to get bounding rectangle for.
* @returns The bounding rectangle.
*/
protected getBlockBoundingRect(block: Blockly.BlockSvg): Blockly.utils.Rect {
const blockXY = block.getRelativeToSurfaceXY();
let left;
let right;
if (block.RTL) {
left = blockXY.x - block.width;
right = blockXY.x;
} else {
left = blockXY.x;
right = blockXY.x + block.width;
}
return new Blockly.utils.Rect(blockXY.y, blockXY.y + block.height, left, right);
}

/**
* Dispose the manager.
*/
dispose(): void {
this.observedBlocks.clear();
this.quadTree.clear();
if (this.workspace) {
this.workspace.removeChangeListener(this.workspaceChangeListener);
}
}
}

/**
* Virtualize the given workspace. Make blocks offscreen invisible.
* @param workspace The workspace to virtualize.
* @param immediate Whether update block's visibility immediately.
* Immediate mode hijacks workspace methods to update visibility immediately on viewport changes.
If false, it uses workspace change listener to update visibility, which may be delayed. and you
need to call `virtualize()` manually when window resized.
Default to true.
* @returns The VirtualizedManager instance.
*/
export function virtualize(workspace: Blockly.WorkspaceSvg, immediate = true): VirtualizedManager {
if (workspace.getFlyout()) {
virtualize(workspace.getFlyout()!.getWorkspace(), immediate);
}

return new VirtualizedManager(workspace, immediate);
}
Copy link

Copilot AI Dec 11, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new VirtualizedManager and virtualize functionality lack test coverage. The tests directory shows no test files for virtualized_manager.ts or quad_tree.ts, while other similar features in the codebase have comprehensive unit tests. Given the complexity of the virtualization logic and its performance-critical nature, tests should cover: QuadTree operations (insert, remove, query, grow), block visibility toggling, viewport changes, block creation/deletion/movement events, and edge cases like workspace disposal and dragging.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hard to actual test since there's no real browser environment now

Comment thread packages/block/src/styles/blockly.css
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>

@supports not (content-visibility: hidden) {
.blocklyBlock .blocklyVirtualizedHidden {
visibility: hidden;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

content-visibility is Baseline 2024 now, so most people can benefit from this.
visibility optimizes only on Chrome-based browsers.


/* Language and Environment */
"target": "es2016", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
"target": "es2021", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The oldest version to use WeakRef

@SimonShiki SimonShiki marked this pull request as draft December 25, 2025 09:13
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
/**
* Height of each buffered block during dragging, in pixels.
*/
static readonly BUFFERED_BLOCK_HEIGHT = 40;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reporter's height. not sure why statement's height is wrong

protected observe(block: Blockly.BlockSvg): void {
if (this.observedBlocks.has(block.id)) return;
// Track root-level blocks only.
if (block.outputConnection?.isConnected()) return;
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not sure it's worth tracking all blocks. it theoretically benefits rendering performance when we reached the end of a block stack, but may slow down the intersect performance.

const state = ScratchBlocks.loadWorkspace(JSON.parse(textarea.value), mainWorkspace, true);
}

function spaghetti(n = 8) {
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

port from blockly's playground. used to test performance

Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
@SimonShiki SimonShiki marked this pull request as ready for review December 27, 2025 03:54
Signed-off-by: SimonShiki <sinangentoo@gmail.com>
Base automatically changed from feat/modern-blockly/main to dev/3.2 March 2, 2026 06:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

block Things related to clipcc-block. feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants