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
This commit is contained in:
Beka Westberg
2023-12-12 16:33:36 +00:00
parent 2c95c4202c
commit 461dfac05a
8 changed files with 183 additions and 85 deletions

View File

@@ -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;
}

View File

@@ -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);
}
/**

View File

@@ -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.
*/

View File

@@ -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)
);
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -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<RenderedConnection, SVGElement>();
/**
* @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);
}
}

View File

@@ -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.
*