From bb6124a7c301f08a03af79e3c6f51712bd6797e4 Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Wed, 1 Mar 2023 15:00:05 -0800 Subject: [PATCH] fix: updating connections in the db multiple times (#6859) * fix: updating connections in the DB recursively * chore: cleanup --- core/block_svg.ts | 49 ++++++++++++++++++++++++++++++++++++- core/render_management.ts | 45 ++++++++++++++++++++++++++++------ core/rendered_connection.ts | 40 ++++++++++++++++++++++++------ 3 files changed, 118 insertions(+), 16 deletions(-) diff --git a/core/block_svg.ts b/core/block_svg.ts index 151fb4a00..d433e11ff 100644 --- a/core/block_svg.ts +++ b/core/block_svg.ts @@ -143,6 +143,15 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg, private translation = ''; + /** + * The location of the top left of this block (in workspace coordinates) + * relative to either its parent block, or the workspace origin if it has no + * parent. + * + * @internal + */ + relativeCoords = new Coordinate(0, 0); + /** * @param workspace The block's workspace. * @param prototypeName Name of the language object containing type-specific @@ -382,6 +391,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg, */ translate(x: number, y: number) { this.translation = `translate(${x}, ${y})`; + this.relativeCoords = new Coordinate(x, y); this.getSvgRoot().setAttribute('transform', this.getTranslation()); } @@ -1339,7 +1349,7 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg, * * @param all If true, return all connections even hidden ones. * Otherwise, for a non-rendered block return an empty list, and for a - * collapsed block don't return inputs connections. + * collapsed block don't return inputs connections. * @returns Array of connections. * @internal */ @@ -1586,6 +1596,43 @@ export class BlockSvg extends Block implements IASTNodeLocationSvg, } } + /** + * Renders this block in a way that's compatible with the more efficient + * render management system. + * + * @internal + */ + renderEfficiently() { + this.rendered = true; + dom.startTextWidthCache(); + + if (this.isCollapsed()) { + this.updateCollapsed_(); + } + this.workspace.getRenderer().render(this); + this.tightenChildrenEfficiently(); + + dom.stopTextWidthCache(); + this.updateMarkers_(); + } + + /** + * Tightens all children of this block so they are snuggly rendered against + * their parent connections. + * + * Does not update connection locations, so that they can be updated more + * efficiently by the render management system. + * + * @internal + */ + tightenChildrenEfficiently() { + for (const input of this.inputList) { + const conn = input.connection as RenderedConnection; + if (conn) conn.tightenEfficiently(); + } + if (this.nextConnection) this.nextConnection.tightenEfficiently(); + } + /** Redraw any attached marker or cursor svgs if needed. */ protected updateMarkers_() { if (this.workspace.keyboardAccessibilityMode && this.pathObject.cursorSvg) { diff --git a/core/render_management.ts b/core/render_management.ts index 203dd4c72..a6a79f118 100644 --- a/core/render_management.ts +++ b/core/render_management.ts @@ -4,11 +4,12 @@ * SPDX-License-Identifier: Apache-2.0 */ -import {BlockSvg} from './block_svg'; +import {BlockSvg} from './block_svg.js'; +import {Coordinate} from './utils/coordinate.js'; const rootBlocks = new Set(); -const dirtyBlocks = new WeakSet(); +let dirtyBlocks = new WeakSet(); let pid = 0; /** @@ -43,11 +44,24 @@ function queueBlock(block: BlockSvg) { * Rerenders all of the blocks in the queue. */ function doRenders() { + const workspaces = new Set([...rootBlocks].map((block) => block.workspace)); for (const block of rootBlocks) { + // No need to render a dead block. if (block.isDisposed()) continue; + // A render for this block may have been queued, and then the block was + // connected to a parent, so it is no longer a root block. + // Rendering will be triggered through the real root block. + if (block.getParent()) continue; + renderBlock(block); + updateConnectionLocations(block, block.getRelativeToSurfaceXY()); + } + for (const workspace of workspaces) { + workspace.resizeContents(); } + rootBlocks.clear(); + dirtyBlocks = new Set(); pid = 0; } @@ -58,14 +72,29 @@ function doRenders() { * @param block The block to rerender. */ function renderBlock(block: BlockSvg) { + if (!dirtyBlocks.has(block)) return; for (const child of block.getChildren(false)) { renderBlock(child); } - if (dirtyBlocks.has(block)) { - dirtyBlocks.delete(block); - rootBlocks.delete(block); - block.render(false); - } else { - block.updateConnectionLocations(); + block.renderEfficiently(); +} + +/** + * Updates the connection database with the new locations of all of the + * connections that are children of the given block. + * + * @param block The block to update the connection locations of. + * @param blockOrigin The top left of the given block in workspace coordinates. + */ +function updateConnectionLocations(block: BlockSvg, blockOrigin: Coordinate) { + for (const conn of block.getConnections_(false)) { + const moved = conn.moveToOffset(blockOrigin); + const target = conn.targetBlock(); + if (!conn.isSuperior()) continue; + if (!target) continue; + if (moved || dirtyBlocks.has(target)) { + updateConnectionLocations( + target, Coordinate.sum(blockOrigin, target.relativeCoords)); + } } } diff --git a/core/rendered_connection.ts b/core/rendered_connection.ts index 0cc20483d..dbe729dd3 100644 --- a/core/rendered_connection.ts +++ b/core/rendered_connection.ts @@ -183,23 +183,31 @@ export class RenderedConnection extends Connection { * * @param x New absolute x coordinate, in workspace coordinates. * @param y New absolute y coordinate, in workspace coordinates. + * @return True if the position of the connection in the connection db + * was updated. */ - moveTo(x: number, y: number) { + moveTo(x: number, y: number): boolean { const dx = this.x - x; const dy = this.y - y; + let moved = false; + if (this.trackedState_ === RenderedConnection.TrackedState.WILL_TRACK) { this.db_.addConnection(this, y); this.trackedState_ = RenderedConnection.TrackedState.TRACKED; + moved = true; } else if ( this.trackedState_ === RenderedConnection.TrackedState.TRACKED && (dx !== 0 || dy !== 0)) { this.db_.removeConnection(this, this.y); this.db_.addConnection(this, y); + moved = true; } this.x = x; this.y = y; + + return moved; } /** @@ -207,9 +215,11 @@ export class RenderedConnection extends Connection { * * @param dx Change to x coordinate, in workspace units. * @param dy Change to y coordinate, in workspace units. + * @return True if the position of the connection in the connection db + * was updated. */ - moveBy(dx: number, dy: number) { - this.moveTo(this.x + dx, this.y + dy); + moveBy(dx: number, dy: number): boolean { + return this.moveTo(this.x + dx, this.y + dy); } /** @@ -218,9 +228,11 @@ export class RenderedConnection extends Connection { * * @param blockTL The location of the top left corner of the block, in * workspace coordinates. + * @return True if the position of the connection in the connection db + * was updated. */ - moveToOffset(blockTL: Coordinate) { - this.moveTo( + moveToOffset(blockTL: Coordinate): boolean { + return this.moveTo( blockTL.x + this.offsetInBlock_.x, blockTL.y + this.offsetInBlock_.y); } @@ -261,12 +273,26 @@ export class RenderedConnection extends Connection { } // Workspace coordinates. const xy = svgMath.getRelativeXY(svgRoot); - block!.getSvgRoot().setAttribute( - 'transform', 'translate(' + (xy.x - dx) + ',' + (xy.y - dy) + ')'); + block!.translate(xy.x - dx, xy.y - dy); block!.moveConnections(-dx, -dy); } } + /** + * Moves the blocks on either side of this connection right next to + * each other, based on their local offsets, not global positions. + * + * @internal + */ + tightenEfficiently() { + const target = this.targetConnection; + const block = this.targetBlock(); + if (!target || !block) return; + const offset = + Coordinate.difference(this.offsetInBlock_, target.offsetInBlock_); + block.translate(offset.x, offset.y); + } + /** * Find the closest compatible connection to this connection. * All parameters are in workspace units.