mirror of
https://github.com/react/react-native-devtools-frontend.git
synced 2026-10-10 13:44:39 +08:00
Bug: chromium:1011811 Change-Id: Ie18a64b6cdf84e3d53e594cd88f7c4431d9c78f6 Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/2450294 Reviewed-by: Tim van der Lippe <tvanderlippe@chromium.org> Commit-Queue: Paul Lewis <aerotwist@chromium.org>
273 lines
8.5 KiB
JavaScript
273 lines
8.5 KiB
JavaScript
// 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/ui.js';
|
|
|
|
import {BottomPaddingWithoutParam, BottomPaddingWithParam, LeftMarginOfText, LeftSideTopPadding, NodeCreationData, NodeLabelFontStyle, NodeLayout, ParamLabelFontStyle, Port, PortTypes, RightMarginOfText, TotalInputPortHeight, TotalOutputPortHeight, TotalParamPortHeight} from './GraphStyle.js'; // eslint-disable-line no-unused-vars
|
|
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 {
|
|
/**
|
|
* @param {!NodeCreationData} data
|
|
* @param {string} label
|
|
*/
|
|
constructor(data, label) {
|
|
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.
|
|
/**
|
|
* @type {?Object}
|
|
*/
|
|
this.position = null;
|
|
|
|
/** @type {!NodeLayout} */
|
|
this._layout = {
|
|
inputPortSectionHeight: 0,
|
|
outputPortSectionHeight: 0,
|
|
maxTextLength: 0,
|
|
totalHeight: 0,
|
|
};
|
|
/** @type {!Map<string, !Port>} */
|
|
this.ports = new Map();
|
|
|
|
this._initialize(data);
|
|
}
|
|
|
|
/**
|
|
* @param {!NodeCreationData} data
|
|
*/
|
|
_initialize(data) {
|
|
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.
|
|
* @param {string} paramId
|
|
* @param {string} paramType
|
|
*/
|
|
addParamPort(paramId, paramType) {
|
|
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();
|
|
}
|
|
|
|
/**
|
|
* @param {!PortTypes} type
|
|
* @return {!Array<!Port>}
|
|
*/
|
|
getPortsByType(type) {
|
|
/**
|
|
* @type {!Array<!Port>}
|
|
*/
|
|
const result = [];
|
|
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
|
|
* @param {!NodeCreationData} data
|
|
*/
|
|
_updateNodeLayoutAfterAddingNode(data) {
|
|
// 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.
|
|
* @param {number} numberOfParams
|
|
* @param {string} paramType
|
|
*/
|
|
_updateNodeLayoutAfterAddingParam(numberOfParams, paramType) {
|
|
// 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();
|
|
}
|
|
|
|
_updateNodeSize() {
|
|
this.size = {
|
|
width: Math.ceil(LeftMarginOfText + this._layout.maxTextLength + RightMarginOfText),
|
|
height: this._layout.totalHeight,
|
|
};
|
|
}
|
|
|
|
// Setup the properties of each input port.
|
|
_setupInputPorts() {
|
|
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.
|
|
_setupOutputPorts() {
|
|
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});
|
|
}
|
|
}
|
|
}
|
|
|
|
/** @param {!Port} port */
|
|
_addPort(port) {
|
|
this.ports.set(port.id, port);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates the port id for the input of node.
|
|
* @param {string} nodeId
|
|
* @param {number | undefined} inputIndex
|
|
* @return {string}
|
|
*/
|
|
export const generateInputPortId = (nodeId, inputIndex) => {
|
|
return `${nodeId}-input-${inputIndex || 0}`;
|
|
};
|
|
|
|
/**
|
|
* Generates the port id for the output of node.
|
|
* @param {string} nodeId
|
|
* @param {number | undefined} outputIndex
|
|
* @return {string}
|
|
*/
|
|
export const generateOutputPortId = (nodeId, outputIndex) => {
|
|
return `${nodeId}-output-${outputIndex || 0}`;
|
|
};
|
|
|
|
/**
|
|
* Generates the port id for the param of node.
|
|
* @param {string} nodeId
|
|
* @param {string} paramId
|
|
* @return {string}
|
|
*/
|
|
export const generateParamPortId = (nodeId, paramId) => {
|
|
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 {
|
|
constructor() {
|
|
this._totalNumberOfNodes = 0;
|
|
}
|
|
|
|
/**
|
|
* Generates the label for a node of a graph.
|
|
* @param {string} nodeType
|
|
* @return {string}
|
|
*/
|
|
generateLabel(nodeType) {
|
|
// 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;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @type {!CanvasRenderingContext2D}
|
|
*/
|
|
let _contextForFontTextMeasuring;
|
|
|
|
/**
|
|
* Get the text width using given font style.
|
|
* @param {string} text
|
|
* @param {?string} fontStyle
|
|
* @return {number}
|
|
*/
|
|
export const measureTextWidth = (text, fontStyle) => {
|
|
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;
|
|
};
|