mirror of
https://github.com/google/blockly.git
synced 2026-01-11 02:47:09 +01:00
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:
@@ -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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user