chrome-devtools-frontend
Version:
Chrome DevTools UI
261 lines (224 loc) • 8.48 kB
text/typescript
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import * as UI from '../../../ui/legacy/legacy.js';
import {
BottomPaddingWithoutParam,
BottomPaddingWithParam,
LeftMarginOfText,
LeftSideTopPadding,
NodeLabelFontStyle,
ParamLabelFontStyle,
PortTypes,
RightMarginOfText,
TotalInputPortHeight,
TotalOutputPortHeight,
TotalParamPortHeight,
type NodeCreationData,
type NodeLayout,
type Port,
} from './GraphStyle.js';
import {calculateInputPortXY, calculateOutputPortXY, calculateParamPortXY} from './NodeRendererUtility.js';
// A class that represents a node of a graph, consisting of the information needed to layout the
// node and display the node. Each node has zero or more ports, including input, output, and param ports.
export class NodeView {
id: string;
type: string;
numberOfInputs: number;
numberOfOutputs: number;
label: string;
size: {
width: number,
height: number,
};
position: Object|null;
private layout: NodeLayout;
ports: Map<string, Port>;
constructor(data: NodeCreationData, label: string) {
this.id = data.nodeId;
this.type = data.nodeType;
this.numberOfInputs = data.numberOfInputs;
this.numberOfOutputs = data.numberOfOutputs;
this.label = label;
this.size = {width: 0, height: 0};
// Position of the center. If null, it means the graph layout has not been computed
// and this node should not be rendered. It will be set after layouting.
this.position = null;
this.layout = {
inputPortSectionHeight: 0,
outputPortSectionHeight: 0,
maxTextLength: 0,
totalHeight: 0,
};
this.ports = new Map();
this.initialize(data);
}
private initialize(data: NodeCreationData): void {
this.updateNodeLayoutAfterAddingNode(data);
this.setupInputPorts();
this.setupOutputPorts();
}
/**
* Add an AudioParam to this node.
* Note for @method removeParamPort: removeParamPort is not necessary because it will only happen
* when the parent NodeView is destroyed. So there is no need to remove port individually
* when the whole NodeView will be gone.
*/
addParamPort(paramId: string, paramType: string): void {
const paramPorts = this.getPortsByType(PortTypes.Param);
const numberOfParams = paramPorts.length;
const {x, y} = calculateParamPortXY(numberOfParams, this.layout.inputPortSectionHeight);
this.addPort({
id: generateParamPortId(this.id, paramId),
type: PortTypes.Param,
label: paramType,
x,
y,
});
this.updateNodeLayoutAfterAddingParam(numberOfParams + 1, paramType);
// The position of output ports may be changed if adding a param increases the total height.
this.setupOutputPorts();
}
getPortsByType(type: PortTypes): Port[] {
const result: Port[] = [];
this.ports.forEach(port => {
if (port.type === type) {
result.push(port);
}
});
return result;
}
/**
* Use number of inputs and outputs to compute the layout
* for text and ports.
* Credit: This function is mostly borrowed from Audion/
* `audion.entryPoints.handleNodeCreated_()`.
* https://github.com/google/audion/blob/master/js/entry-points/panel.js
*/
private updateNodeLayoutAfterAddingNode(data: NodeCreationData): void {
// Even if there are no input ports, leave room for the node label.
const inputPortSectionHeight = TotalInputPortHeight * Math.max(1, data.numberOfInputs) + LeftSideTopPadding;
this.layout.inputPortSectionHeight = inputPortSectionHeight;
this.layout.outputPortSectionHeight = TotalOutputPortHeight * data.numberOfOutputs;
// Use the max of the left and right side heights as the total height.
// Include a little padding on the left.
this.layout.totalHeight =
Math.max(inputPortSectionHeight + BottomPaddingWithoutParam, this.layout.outputPortSectionHeight);
// Update max length with node label.
const nodeLabelLength = measureTextWidth(this.label, NodeLabelFontStyle);
this.layout.maxTextLength = Math.max(this.layout.maxTextLength, nodeLabelLength);
this.updateNodeSize();
}
/**
* After adding a param port, update the node layout based on the y value
* and label length.
*/
private updateNodeLayoutAfterAddingParam(numberOfParams: number, paramType: string): void {
// The height after adding param ports and input ports.
// Include a little padding on the left.
const leftSideMaxHeight =
this.layout.inputPortSectionHeight + numberOfParams * TotalParamPortHeight + BottomPaddingWithParam;
// Use the max of the left and right side heights as the total height.
this.layout.totalHeight = Math.max(leftSideMaxHeight, this.layout.outputPortSectionHeight);
// Update max length with param label.
const paramLabelLength = measureTextWidth(paramType, ParamLabelFontStyle);
this.layout.maxTextLength = Math.max(this.layout.maxTextLength, paramLabelLength);
this.updateNodeSize();
}
private updateNodeSize(): void {
this.size = {
width: Math.ceil(LeftMarginOfText + this.layout.maxTextLength + RightMarginOfText),
height: this.layout.totalHeight,
};
}
// Setup the properties of each input port.
private setupInputPorts(): void {
for (let i = 0; i < this.numberOfInputs; i++) {
const {x, y} = calculateInputPortXY(i);
this.addPort({id: generateInputPortId(this.id, i), type: PortTypes.In, x, y, label: undefined});
}
}
// Setup the properties of each output port.
private setupOutputPorts(): void {
for (let i = 0; i < this.numberOfOutputs; i++) {
const portId = generateOutputPortId(this.id, i);
const {x, y} = calculateOutputPortXY(i, this.size, this.numberOfOutputs);
if (this.ports.has(portId)) {
// Update y value of an existing output port.
const port = this.ports.get(portId);
if (!port) {
throw new Error(`Unable to find port with id ${portId}`);
}
port.x = x;
port.y = y;
} else {
this.addPort({id: portId, type: PortTypes.Out, x, y, label: undefined});
}
}
}
private addPort(port: Port): void {
this.ports.set(port.id, port);
}
}
/**
* Generates the port id for the input of node.
*/
export const generateInputPortId = (nodeId: string, inputIndex: number|undefined): string => {
return `${nodeId}-input-${inputIndex || 0}`;
};
/**
* Generates the port id for the output of node.
*/
export const generateOutputPortId = (nodeId: string, outputIndex: number|undefined): string => {
return `${nodeId}-output-${outputIndex || 0}`;
};
/**
* Generates the port id for the param of node.
*/
export const generateParamPortId = (nodeId: string, paramId: string): string => {
return `${nodeId}-param-${paramId}`;
};
// A label generator to convert UUID of node to shorter label to display.
// Each graph should have its own generator since the node count starts from 0.
export class NodeLabelGenerator {
private totalNumberOfNodes: number;
constructor() {
this.totalNumberOfNodes = 0;
}
/**
* Generates the label for a node of a graph.
*/
generateLabel(nodeType: string): string {
// To make the label concise, remove the suffix "Node" from the nodeType.
if (nodeType.endsWith('Node')) {
nodeType = nodeType.slice(0, nodeType.length - 4);
}
// Also, use an integer to replace the long UUID.
this.totalNumberOfNodes += 1;
const label = `${nodeType} ${this.totalNumberOfNodes}`;
return label;
}
}
// TODO(crbug.com/1172300) Ignored during the jsdoc to ts migration
// eslint-disable-next-line @typescript-eslint/naming-convention
let _contextForFontTextMeasuring: CanvasRenderingContext2D;
/**
* Get the text width using given font style.
*/
export const measureTextWidth = (text: string, fontStyle: string|null): number => {
if (!_contextForFontTextMeasuring) {
const context = document.createElement('canvas').getContext('2d');
if (!context) {
throw new Error('Unable to create canvas context.');
}
_contextForFontTextMeasuring = context;
}
const context = _contextForFontTextMeasuring;
context.save();
if (fontStyle) {
context.font = fontStyle;
}
const width = UI.UIUtils.measureTextWidth(context, text);
context.restore();
return width;
};