From 461dfac05ac265b8e43b0d01e1987dbcd94f11ea Mon Sep 17 00:00:00 2001 From: Beka Westberg Date: Tue, 12 Dec 2023 16:33:36 +0000 Subject: [PATCH] feat: connection highlighting in geras and thrasos (#7698) * chore: move connection highlighting into the geras renderer * chore: remove IConnectionHighlighter interface * chore: format * chore: fixup * chore: format * fix: PR comments --- core/interfaces/i_connection_highlighter.ts | 21 ------ core/rendered_connection.ts | 61 +----------------- core/renderers/common/constants.ts | 20 +++++- core/renderers/common/drawer.ts | 71 ++++++++++++++++++++- core/renderers/common/i_path_object.ts | 15 +++++ core/renderers/common/info.ts | 21 +++++- core/renderers/common/path_object.ts | 40 ++++++++++++ core/renderers/common/renderer.ts | 19 ++++++ 8 files changed, 183 insertions(+), 85 deletions(-) delete mode 100644 core/interfaces/i_connection_highlighter.ts diff --git a/core/interfaces/i_connection_highlighter.ts b/core/interfaces/i_connection_highlighter.ts deleted file mode 100644 index 35fcf6776..000000000 --- a/core/interfaces/i_connection_highlighter.ts +++ /dev/null @@ -1,21 +0,0 @@ -/** - * @license - * Copyright 2023 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type {RenderedConnection} from '../rendered_connection'; - -/** - * Visually highlights connections, usually to preview where a block will be - * connected if it is dropped. - * - * Often implemented by IPathObject classes. - */ -export interface IConnectionHighlighter { - /** Visually highlights the given connection. */ - highlightConnection(conn: RenderedConnection): void; - - /** Visually unhighlights the given connnection (if it was highlighted). */ - unhighlightConnection(conn: RenderedConnection): void; -} diff --git a/core/rendered_connection.ts b/core/rendered_connection.ts index 08c0471a2..80fa035e6 100644 --- a/core/rendered_connection.ts +++ b/core/rendered_connection.ts @@ -23,18 +23,6 @@ import {hasBubble} from './interfaces/i_has_bubble.js'; import * as internalConstants from './internal_constants.js'; import {Coordinate} from './utils/coordinate.js'; import * as dom from './utils/dom.js'; -import {Svg} from './utils/svg.js'; -import * as svgPaths from './utils/svg_paths.js'; - -/** A shape that has a pathDown property. */ -interface PathDownShape { - pathDown: string; -} - -/** A shape that has a pathLeft property. */ -interface PathLeftShape { - pathLeft: string; -} /** Maximum randomness in workspace units for bumping a block. */ const BUMP_RANDOMNESS = 10; @@ -305,57 +293,12 @@ export class RenderedConnection extends Connection { /** Add highlighting around this connection. */ highlight() { - if (this.highlightPath) { - // This connection is already highlighted - return; - } - let steps; - const sourceBlockSvg = this.sourceBlock_; - const renderConstants = sourceBlockSvg.workspace - .getRenderer() - .getConstants(); - const shape = renderConstants.shapeFor(this); - if ( - this.type === ConnectionType.INPUT_VALUE || - this.type === ConnectionType.OUTPUT_VALUE - ) { - // Vertical line, puzzle tab, vertical line. - const yLen = renderConstants.TAB_OFFSET_FROM_TOP; - steps = - svgPaths.moveBy(0, -yLen) + - svgPaths.lineOnAxis('v', yLen) + - (shape as unknown as PathDownShape).pathDown + - svgPaths.lineOnAxis('v', yLen); - } else { - const xLen = - renderConstants.NOTCH_OFFSET_LEFT - renderConstants.CORNER_RADIUS; - // Horizontal line, notch, horizontal line. - steps = - svgPaths.moveBy(-xLen, 0) + - svgPaths.lineOnAxis('h', xLen) + - (shape as unknown as PathLeftShape).pathLeft + - svgPaths.lineOnAxis('h', xLen); - } - const offset = this.offsetInBlock; - this.highlightPath = dom.createSvgElement( - Svg.PATH, - { - 'class': 'blocklyHighlightedConnectionPath', - 'd': steps, - 'transform': - `translate(${offset.x}, ${offset.y})` + - (this.sourceBlock_.RTL ? ' scale(-1 1)' : ''), - }, - this.sourceBlock_.getSvgRoot(), - ); + this.getSourceBlock().workspace.getRenderer().highlightConnection(this); } /** Remove the highlighting around this connection. */ unhighlight() { - if (this.highlightPath) { - dom.removeNode(this.highlightPath); - this.highlightPath = null; - } + this.getSourceBlock().workspace.getRenderer().unhighlightConnection(this); } /** diff --git a/core/renderers/common/constants.ts b/core/renderers/common/constants.ts index f0acc39aa..1983793f0 100644 --- a/core/renderers/common/constants.ts +++ b/core/renderers/common/constants.ts @@ -53,8 +53,8 @@ export interface PuzzleTab { type: number; width: number; height: number; - pathDown: string | ((p1: number) => string); - pathUp: string | ((p1: number) => string); + pathDown: string; + pathUp: string; } /** @@ -100,6 +100,22 @@ export function isDynamicShape(shape: Shape): shape is DynamicShape { return (shape as DynamicShape).isDynamic; } +/** Returns whether the shape is a puzzle tab or not. */ +export function isPuzzleTab(shape: Shape): shape is PuzzleTab { + return ( + (shape as PuzzleTab).pathDown !== undefined && + (shape as PuzzleTab).pathUp !== undefined + ); +} + +/** Returns whether the shape is a notch or not. */ +export function isNotch(shape: Shape): shape is Notch { + return ( + (shape as Notch).pathLeft !== undefined && + (shape as Notch).pathRight !== undefined + ); +} + /** * An object that provides constants for rendering blocks. */ diff --git a/core/renderers/common/drawer.ts b/core/renderers/common/drawer.ts index e361a606d..bda4303af 100644 --- a/core/renderers/common/drawer.ts +++ b/core/renderers/common/drawer.ts @@ -18,10 +18,12 @@ import type {PreviousConnection} from '../measurables/previous_connection.js'; import type {Row} from '../measurables/row.js'; import {Types} from '../measurables/types.js'; -import {isDynamicShape} from './constants.js'; +import {isDynamicShape, isNotch, isPuzzleTab} from './constants.js'; import type {ConstantProvider, Notch, PuzzleTab} from './constants.js'; import type {RenderInfo} from './info.js'; import * as deprecation from '../../utils/deprecation.js'; +import type {RenderedConnection} from '../../rendered_connection.js'; +import {ConnectionType} from '../../connection_type.js'; /** * An object that draws a block based on the given rendering information. @@ -440,4 +442,71 @@ export class Drawer { ); } } + + /** Returns a path to highlight the given connection. */ + drawConnectionHighlightPath(conn: RenderedConnection) { + const measurable = this.info_.getMeasureableForConnection(conn); + if (!measurable) { + throw new Error('Could not find measurable for connection'); + } + + let path = ''; + if ( + conn.type === ConnectionType.INPUT_VALUE || + conn.type === ConnectionType.OUTPUT_VALUE + ) { + path = this.getExpressionConnectionHighlightPath(measurable); + } else { + path = this.getStatementConnectionHighlightPath(measurable); + } + const block = conn.getSourceBlock(); + block.pathObject.addConnectionHighlight?.( + conn, + path, + conn.getOffsetInBlock(), + block.RTL, + ); + } + + /** + * Returns a path to highlight the given conneciton, assuming it is an + * input or output connection. + */ + private getExpressionConnectionHighlightPath(connection: Connection): string { + let connPath = ''; + if (isDynamicShape(connection.shape)) { + connPath = connection.shape.pathDown(connection.height); + } else if (isPuzzleTab(connection.shape)) { + connPath = connection.shape.pathDown; + } + + // We are assuming that there is room for the tab offset above and below + // the tab. + const yLen = this.constants_.TAB_OFFSET_FROM_TOP; + return ( + svgPaths.moveBy(0, -yLen) + + svgPaths.lineOnAxis('v', yLen) + + connPath + + svgPaths.lineOnAxis('v', yLen) + ); + } + + /** + * Returns a path to highlight the given conneciton, assuming it is a + * next or previous connection. + */ + private getStatementConnectionHighlightPath(connection: Connection): string { + if (!isNotch(connection.shape)) { + throw new Error('Statement connections should have notch shapes'); + } + + const xLen = + this.constants_.NOTCH_OFFSET_LEFT - this.constants_.CORNER_RADIUS; + return ( + svgPaths.moveBy(-xLen, 0) + + svgPaths.lineOnAxis('h', xLen) + + connection.shape.pathLeft + + svgPaths.lineOnAxis('h', xLen) + ); + } } diff --git a/core/renderers/common/i_path_object.ts b/core/renderers/common/i_path_object.ts index 8bf57ddaa..30033f18e 100644 --- a/core/renderers/common/i_path_object.ts +++ b/core/renderers/common/i_path_object.ts @@ -9,6 +9,8 @@ import type {BlockStyle} from '../../theme.js'; import type {BlockSvg} from '../../block_svg.js'; import type {ConstantProvider} from './constants.js'; +import type {RenderedConnection} from '../../rendered_connection.js'; +import type {Coordinate} from '../../utils/coordinate.js'; /** * An interface for a block's path object. @@ -119,4 +121,17 @@ export interface IPathObject { * @param enable True if the block is movable, false otherwise. */ updateMovable(enabled: boolean): void; + + /** Adds the given path as a connection highlight for the given connection. */ + addConnectionHighlight?( + connection: RenderedConnection, + connectionPath: string, + offset: Coordinate, + rtl: boolean, + ): void; + + /** + * Removes any highlight associated with the given connection, if it exists. + */ + removeConnectionHighlight?(connection: RenderedConnection): void; } diff --git a/core/renderers/common/info.ts b/core/renderers/common/info.ts index fa44b7743..995124c1b 100644 --- a/core/renderers/common/info.ts +++ b/core/renderers/common/info.ts @@ -37,6 +37,7 @@ import {ValueInput} from '../../inputs/value_input.js'; import type {ConstantProvider} from './constants.js'; import type {Renderer} from './renderer.js'; +import {Connection} from '../measurables/connection.js'; /** * An object containing all sizing information needed to draw this block. @@ -143,8 +144,7 @@ export class RenderInfo { } /** - * Populate and return an object containing all sizing information needed to - * draw this block. + * Populate this object with all sizing information needed to draw the block. * * This measure pass does not propagate changes to the block (although fields * may choose to rerender when getSize() is called). However, calling it @@ -748,4 +748,21 @@ export class RenderInfo { this.startY = this.topRow.capline; this.bottomRow.baseline = yCursor - this.bottomRow.descenderHeight; } + + /** Returns the connection measurable associated with the given connection. */ + getMeasureableForConnection(conn: RenderedConnection): Connection | null { + if (this.outputConnection?.connectionModel === conn) { + return this.outputConnection; + } + + for (const row of this.rows) { + for (const elem of row.elements) { + if (elem instanceof Connection && elem.connectionModel === conn) { + return elem; + } + } + } + + return null; + } } diff --git a/core/renderers/common/path_object.ts b/core/renderers/common/path_object.ts index f5987f86e..35e1f83c5 100644 --- a/core/renderers/common/path_object.ts +++ b/core/renderers/common/path_object.ts @@ -8,7 +8,9 @@ import type {BlockSvg} from '../../block_svg.js'; import type {Connection} from '../../connection.js'; +import {RenderedConnection} from '../../rendered_connection.js'; import type {BlockStyle} from '../../theme.js'; +import {Coordinate} from '../../utils/coordinate.js'; import * as dom from '../../utils/dom.js'; import {Svg} from '../../utils/svg.js'; @@ -38,6 +40,13 @@ export class PathObject implements IPathObject { constants: ConstantProvider; style: BlockStyle; + /** + * Highlight paths associated with connections. + * + * @protected + */ + connectionHighlights = new WeakMap(); + /** * @param root The root SVG element. * @param style The style object to use for colouring. @@ -256,4 +265,35 @@ export class PathObject implements IPathObject { updateShapeForInputHighlight(_conn: Connection, _enable: boolean) { // NOOP } + + /** Adds the given path as a connection highlight for the given connection. */ + addConnectionHighlight( + connection: RenderedConnection, + connectionPath: string, + offset: Coordinate, + rtl: boolean, + ) { + if (this.connectionHighlights.has(connection)) return; + const highlight = dom.createSvgElement( + Svg.PATH, + { + 'class': 'blocklyHighlightedConnectionPath', + 'd': connectionPath, + 'transform': + `translate(${offset.x}, ${offset.y})` + (rtl ? ' scale(-1 1)' : ''), + }, + this.svgRoot, + ); + this.connectionHighlights.set(connection, highlight); + } + + /** + * Removes any highlight associated with the given connection, if it exists. + */ + removeConnectionHighlight(connection: RenderedConnection) { + const highlight = this.connectionHighlights.get(connection); + if (!highlight) return; + dom.removeNode(highlight); + this.connectionHighlights.delete(connection); + } } diff --git a/core/renderers/common/renderer.ts b/core/renderers/common/renderer.ts index 3a53322dd..d171f8b09 100644 --- a/core/renderers/common/renderer.ts +++ b/core/renderers/common/renderer.ts @@ -257,6 +257,25 @@ export class Renderer implements IRegistrable { return InsertionMarkerManager.PREVIEW_TYPE.INSERTION_MARKER; } + /** + * Visually highlights the given connection with a border, if it is not + * already highlighted. + */ + highlightConnection(conn: RenderedConnection): void { + const block = conn.getSourceBlock(); + + const info = this.makeRenderInfo_(block); + info.measure(); + const drawer = this.makeDrawer_(block, info); + + drawer.drawConnectionHighlightPath(conn); + } + + /** Visually unhighlights the given connection, if it is highlighted. */ + unhighlightConnection(conn: RenderedConnection): void { + conn.getSourceBlock().pathObject.removeConnectionHighlight?.(conn); + } + /** * Render the block. *