initial commit

This commit is contained in:
equippedcoding-master
2025-09-17 09:37:06 -05:00
parent 86108ca47e
commit e2c98790b2
55389 changed files with 6206730 additions and 0 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.0 KiB

View File

@@ -0,0 +1,60 @@
export const GRAPH_ACTIONS = {
ADD_NODE: 'EVENT_ADD_NODE',
DELETE_NODE: 'EVENT_DELETE_NODE',
SELECT_NODE: 'EVENT_SELECT_NODE',
UPDATE_NODE_POSITION: 'EVENT_UPDATE_NODE_POSITION',
UPDATE_NODE_ATTRIBUTE: 'EVENT_UPDATE_NODE_ATTRIBUTE',
ADD_EDGE: 'EVENT_ADD_EDGE',
DELETE_EDGE: 'EVENT_DELETE_EDGE',
SELECT_EDGE: 'EVENT_SELECT_EDGE',
DESELECT_ITEM: 'EVENT_DESELECT_ITEM',
UPDATE_TRANSLATE: 'EVENT_UPDATE_TRANSLATE',
UPDATE_SCALE: 'EVENT_UPDATE_SCALE'
};
export const DEFAULT_CONFIG = {
readOnly: false,
passiveUIEvents: false,
incrementNodeNames: false,
restrictTranslate: false,
edgeHoverEffect: true,
includeFonts: true,
useGlobalPCUI: false,
adjustVertices: false,
defaultStyles: {
initialScale: 1,
initialPosition: {
x: 0,
y: 0
},
background: {
color: '#20292B',
gridSize: 10
},
node: {
fill: '#2c393c',
fillSecondary: '#364346',
stroke: '#293538',
strokeSelected: '#F60',
strokeHover: 'rgba(255, 102, 0, 0.32)',
textColor: '#FFFFFF',
textColorSecondary: '#b1b8ba',
includeIcon: true,
icon: '',
iconColor: '#F60',
baseHeight: 28,
baseWidth: 226,
textAlignMiddle: false,
lineHeight: 12
},
edge: {
stroke: 'rgb(3, 121, 238)',
strokeSelected: '#F60',
strokeWidth: 2,
strokeWidthSelected: 2,
targetMarker: true,
connectionStyle: 'default'
}
}
};

View File

@@ -0,0 +1,177 @@
import { Menu } from '@playcanvas/pcui';
import * as joint from 'jointjs/dist/joint.min.js';
joint.connectors.smoothInOut = function (sourcePoint, targetPoint, vertices, args) {
const p1 = sourcePoint.clone();
p1.offset(30, 0);
const p2 = targetPoint.clone();
p2.offset(-30, 0);
const path = new joint.g.Path(joint.g.Path.createSegment('M', sourcePoint));
path.appendSegment(joint.g.Path.createSegment('C', p1, p2, targetPoint));
return path;
};
class GraphViewEdge {
constructor(graphView, paper, graph, graphSchema, edgeData, edgeSchema, onEdgeSelected) {
this._graphView = graphView;
this._config = graphView._config;
this._paper = paper;
this._graph = graph;
this._graphSchema = graphSchema;
this.edgeData = edgeData;
this._edgeSchema = edgeSchema;
this.state = GraphViewEdge.STATES.DEFAULT;
const link = GraphViewEdge.createLink(this._config.defaultStyles, edgeSchema, edgeData);
const sourceNode = this._graphView.getNode(edgeData.from);
if (edgeData && Number.isFinite(edgeData.outPort)) {
link.source({
id: sourceNode.model.id,
port: `out${edgeData.outPort}`
});
} else {
if (sourceNode.model) {
link.source(sourceNode.model);
}
}
const targetNode = this._graphView.getNode(edgeData.to);
if (edgeData && Number.isFinite(edgeData.inPort)) {
link.target({
id: targetNode.model.id,
port: `in${edgeData.inPort}`
});
} else {
link.target(targetNode.model);
}
const onCellMountedToDom = () => {
this._paper.findViewByModel(link).on('cell:pointerdown', () => {
if (this._config.readOnly) return;
onEdgeSelected(edgeData);
});
if (edgeData && Number.isFinite(edgeData.inPort)) {
this._graphView.updatePortStatesForEdge(link, true);
}
link.toBack();
};
if (this._graphView._batchingCells) {
this._graphView._cells.push(link);
this._graphView._cellMountedFunctions.push(onCellMountedToDom);
} else {
this._graph.addCell(link);
onCellMountedToDom();
}
this.model = link;
}
static createLink(defaultStyles, edgeSchema, edgeData) {
const link = new joint.shapes.standard.Link();
link.attr({
line: {
strokeWidth: edgeSchema.strokeWidth || defaultStyles.edge.strokeWidth,
stroke: edgeSchema.stroke || defaultStyles.edge.stroke
}
});
if (edgeSchema.smooth || defaultStyles.edge.connectionStyle === 'smooth') {
link.set('connector', { name: 'smooth' });
} else if (edgeSchema.smoothInOut || defaultStyles.edge.connectionStyle === 'smoothInOut') {
link.set('connector', { name: 'smoothInOut' });
}
if (edgeData && Number.isFinite(edgeData.outPort)) {
link.attr('line/targetMarker', null);
return link;
}
if (edgeSchema.targetMarker || defaultStyles.edge.targetMarker) {
link.attr('line/targetMarker', {
'type': 'path',
'd': 'm1.18355,0.8573c-0.56989,-0.39644 -0.57234,-1.2387 -0.00478,-1.63846l7.25619,-5.11089c0.66255,-0.46663 1.57585,0.00721 1.57585,0.81756l0,10.1587c0,0.8077 -0.908,1.2821 -1.57106,0.8209l-7.2562,-5.04781z',
'stroke': edgeSchema.stroke || defaultStyles.edge.stroke,
'fill': edgeSchema.stroke || defaultStyles.edge.stroke
});
} else {
link.attr('line/targetMarker', null);
}
if (edgeSchema.sourceMarker || defaultStyles.edge.sourceMarker) {
link.attr('line/sourceMarker', {
d: 'M 6 0 a 6 6 0 1 0 0 1'
});
}
return link;
}
addContextMenu(items) {
if (this._graphView._config.readOnly) return;
this._contextMenu = new Menu({
items: items
});
this._paper.el.appendChild(this._contextMenu.dom);
const edgeElement = this._paper.findViewByModel(this.model).el;
edgeElement.addEventListener('contextmenu', (e) => {
e.preventDefault();
this._contextMenu.position(e.clientX, e.clientY);
this._contextMenu.hidden = false;
});
}
select() {
const edgeSchema = this._edgeSchema;
this.model.attr('line/stroke', edgeSchema.strokeSelected || this._config.defaultStyles.edge.strokeSelected);
this.model.attr('line/strokeWidth', edgeSchema.strokeWidthSelected || this._config.defaultStyles.edge.strokeWidthSelected);
this.model.attr('line/targetMarker', {
stroke: edgeSchema.strokeSelected || this._config.defaultStyles.edge.strokeSelected,
fill: edgeSchema.strokeSelected || this._config.defaultStyles.edge.strokeSelected
});
}
deselect() {
const edgeSchema = this._edgeSchema;
this.model.attr('line/stroke', edgeSchema.stroke || this._config.defaultStyles.edge.stroke);
this.model.attr('line/strokeWidth', edgeSchema.strokeWidth || this._config.defaultStyles.edge.strokeWidth);
this.model.attr('line/targetMarker', {
'stroke': edgeSchema.stroke || this._config.defaultStyles.edge.stroke,
'fill': edgeSchema.stroke || this._config.defaultStyles.edge.stroke
});
this.state = GraphViewEdge.STATES.DEFAULT;
}
mute() {
const edgeSchema = this._edgeSchema;
this.model.attr('line/stroke', '#42495B');
this.model.attr('line/strokeWidth', edgeSchema.strokeWidth || this._config.defaultStyles.edge.stroke);
this.model.attr('line/targetMarker', {
stroke: '#9BA1A3',
fill: '#9BA1A3'
});
}
addSourceMarker() {
const edgeSchema = this._edgeSchema;
this.model.attr('line/sourceMarker', {
'type': 'path',
'd': 'm-2.57106,0.93353c-0.56989,-0.39644 -0.57234,-1.2387 -0.00478,-1.63846l7.25619,-5.11089c0.66251,-0.46663 1.57585,0.00721 1.57585,0.81756l0,10.1587c0,0.8077 -0.90803,1.2821 -1.57106,0.8209l-7.2562,-5.04781z',
'stroke': edgeSchema.stroke || this._config.defaultStyles.edge.stroke,
'fill': edgeSchema.stroke || this._config.defaultStyles.edge.stroke
});
}
addTargetMarker() {
const edgeSchema = this._edgeSchema;
this.model.attr('line/targetMarker', {
'type': 'path',
'd': 'm-2.57106,0.93353c-0.56989,-0.39644 -0.57234,-1.2387 -0.00478,-1.63846l7.25619,-5.11089c0.66251,-0.46663 1.57585,0.00721 1.57585,0.81756l0,10.1587c0,0.8077 -0.90803,1.2821 -1.57106,0.8209l-7.2562,-5.04781z',
'stroke': edgeSchema.stroke || this._config.defaultStyles.edge.stroke,
'fill': edgeSchema.stroke || this._config.defaultStyles.edge.stroke
});
}
}
GraphViewEdge.STATES = {
DEFAULT: 0,
SELECTED: 1
};
export default GraphViewEdge;

View File

@@ -0,0 +1,517 @@
import { Menu, Container, Label, TextInput, BooleanInput, NumericInput, VectorInput } from '@playcanvas/pcui';
import * as joint from 'jointjs/dist/joint.min.js';
const Colors = {
bcgDarkest: '#20292b',
bcgDarker: '#293538',
bcgDark: '#2c393c',
bcgPrimary: '#364346',
textDarkest: '#5b7073',
textDark: '#9ba1a3',
textSecondary: '#b1b8ba',
textPrimary: '#ffffff',
textActive: '#f60'
};
class GraphViewNode {
constructor(graphView, paper, graph, graphSchema, nodeData, nodeSchema, onCreateEdge, onNodeSelected) {
this._graphView = graphView;
this._config = graphView._config;
this._paper = paper;
this._graph = graph;
this._graphSchema = graphSchema;
this.nodeData = nodeData;
this.nodeSchema = nodeSchema;
this.state = GraphViewNode.STATES.DEFAULT;
const rectHeight = this.getSchemaValue('baseHeight');
let portHeight = 0;
let attributeHeight = 0;
if (nodeSchema.inPorts) {
portHeight = (nodeSchema.inPorts.length * 25) + 10;
}
if (nodeSchema.outPorts) {
const outHeight = (nodeSchema.outPorts.length * 25) + 10;
if (outHeight > portHeight) portHeight = outHeight;
}
const visibleAttributes = nodeSchema.attributes && nodeSchema.attributes.filter(a => !a.hidden);
if (visibleAttributes && visibleAttributes.length > 0) {
attributeHeight = visibleAttributes.length * 32 + 10;
}
const rectSize = { x: this.getSchemaValue('baseWidth'), y: rectHeight + portHeight + attributeHeight };
let labelName;
const formattedText = nodeSchema.headerTextFormatter && nodeSchema.headerTextFormatter(nodeData.attributes, nodeData.id);
if (typeof formattedText === 'string') {
labelName = nodeSchema.headerTextFormatter(nodeData.attributes, nodeData.id);
} else if (nodeSchema.outPorts || nodeSchema.inPorts) {
labelName = nodeData.attributes && nodeData.attributes.name ? `${nodeData.attributes.name} (${nodeSchema.name})` : nodeSchema.name;
} else {
labelName = nodeData.attributes && nodeData.attributes.name || nodeData.name;
}
const rect = new joint.shapes.html.Element({
attrs: {
body: {
fill: this.getSchemaValue('fill'),
stroke: this.getSchemaValue('stroke'),
strokeWidth: 2,
width: rectSize.x,
height: rectSize.y
},
labelBackground: {
fill: this.getSchemaValue('fill'),
refX: 2,
refY: 2,
width: rectSize.x - 4,
height: rectHeight - 4
},
labelSeparator: {
fill: this.getSchemaValue('stroke'),
width: rectSize.x - 2,
height: this.getSchemaValue('inPorts') || this.getSchemaValue('outPorts') ? 2 : 0,
refX: 1,
refY: rectHeight - 1
},
inBackground: {
fill: this.getSchemaValue('fillSecondary'),
width: this.getSchemaValue('inPorts') ? rectSize.x / 2 - 1 : rectSize.x - 2,
height: (rectSize.y - rectHeight - 2) >= 0 ? rectSize.y - rectHeight - 2 : 0,
refX: 1,
refY: rectHeight + 1
},
outBackground: {
fill: this.getSchemaValue('fill'),
width: this.getSchemaValue('inPorts') ? rectSize.x / 2 - 1 : 0,
height: (rectSize.y - rectHeight - 2) >= 0 ? rectSize.y - rectHeight - 2 : 0,
refX: rectSize.x / 2,
refY: rectHeight + 1
},
icon: this.getSchemaValue('includeIcon') ? {
text: this.getSchemaValue('icon'),
fontFamily: 'pc-icon',
fontSize: 14,
fill: this.getSchemaValue('iconColor'),
refX: 8,
refY: 8
} : undefined,
label: {
text: labelName,
fill: this.getSchemaValue('textColor'),
textAnchor: this.getSchemaValue('textAlignMiddle') ? 'middle' : 'left',
refX: !this.getSchemaValue('textAlignMiddle') ? (this.getSchemaValue('includeIcon') ? 28 : 14) : rectSize.x / 2,
refY: !this.getSchemaValue('textAlignMiddle') ? 14 : rectHeight / 2,
fontSize: 12,
fontWeight: 600,
width: rectSize.x,
height: rectHeight,
lineSpacing: 50,
lineHeight: this.getSchemaValue('lineHeight')
},
marker: nodeData.marker ? {
refX: rectSize.x - 20,
fill: this.getSchemaValue('stroke'),
d: 'M0 0 L20 0 L20 20 Z'
} : null,
texture: nodeData.texture ? {
href: nodeData.texture,
fill: 'red',
width: 95,
height: 95,
refX: 5,
refY: 65
} : null
},
ports: {
groups: {
'in': {
position: {
name: 'line',
args: {
start: { x: 0, y: rectHeight },
end: { x: 0, y: rectHeight + (25 * (nodeSchema.inPorts ? nodeSchema.inPorts.length : 0)) }
}
},
label: {
position: {
name: 'right',
args: {
y: 5
}
}
},
markup: '<circle class="port-body"></circle><circle class="port-inner-body" visibility="hidden"></circle>',
attrs: {
'.port-body': {
strokeWidth: 2,
fill: Colors.bcgDarkest,
magnet: true,
r: 5,
cy: 5,
cx: 1
},
'.port-inner-body': {
strokeWidth: 2,
stroke: this._config.defaultStyles.edge.stroke,
r: 1,
cy: 5,
cx: 1
}
}
},
'out': {
position: {
name: 'line',
args: {
start: { x: rectSize.x - 10, y: rectHeight },
end: { x: rectSize.x - 10, y: rectHeight + (25 * (nodeSchema.outPorts ? nodeSchema.outPorts.length : 0)) }
}
},
label: {
position: {
name: 'left', args: { y: 5, x: -5 }
}
},
markup: '<circle class="port-body"></circle><circle class="port-inner-body" visibility="hidden"></circle>',
attrs: {
'.port-body': {
strokeWidth: 2,
fill: Colors.bcgDarkest,
magnet: true,
r: 5,
cy: 5,
cx: 9
},
'.port-inner-body': {
strokeWidth: 2,
stroke: this._config.defaultStyles.edge.stroke,
r: 1,
cy: 5,
cx: 9
}
}
}
}
}
});
rect.position(nodeData.posX, nodeData.posY);
rect.resize(rectSize.x, rectSize.y);
if (nodeSchema.inPorts) {
nodeSchema.inPorts.forEach((port, i) => {
rect.addPort({
id: `in${i}`,
group: 'in',
edgeType: port.edgeType,
markup: `<circle class="port-body" id="${nodeData.id}-in${i}" edgeType="${port.type}"></circle><circle class="port-inner-body" visibility="hidden"></circle>`,
attrs: {
'.port-body': {
stroke: this._graphSchema.edges[port.type].stroke || this._config.defaultStyles.edge.stroke
},
text: {
text: port.textFormatter ? port.textFormatter(nodeData.attributes) : port.name,
fill: this.getSchemaValue('textColorSecondary'),
'font-size': 14
}
}
});
this._graph.on('change:target', (cell) => {
if (this._suppressChangeTargetEvent) return;
let target = cell.get('target');
let source = cell.get('source');
if (!target || !source) return;
if (target && target.port && target.port.includes('out')) {
const temp = target;
target = source;
source = temp;
}
if (!target || !target.id || target.id !== this.model.id) return;
if (source && source.port && target.port && Number(target.port.replace('in', '')) === i) {
const sourceNodeId = this._graphView.getNode(source.id).nodeData.id;
const edgeId = `${sourceNodeId},${source.port.replace('out', '')}-${this.nodeData.id},${target.port.replace('in', '')}`;
const edge = {
to: this.nodeData.id,
from: sourceNodeId,
outPort: Number(source.port.replace('out', '')),
inPort: Number(target.port.replace('in', '')),
edgeType: port.type
};
this._suppressChangeTargetEvent = true;
this._graph.removeCells(cell);
this._suppressChangeTargetEvent = false;
onCreateEdge(edgeId, edge);
}
});
});
}
if (nodeSchema.outPorts) {
nodeSchema.outPorts.forEach((port, i) => rect.addPort({
id: `out${i}`,
group: 'out',
markup: `<circle class="port-body" id="${nodeData.id}-out${i}" edgeType="${port.type}"></circle><circle class="port-inner-body" visibility="hidden"></circle>`,
attrs: {
type: port.type,
'.port-body': {
stroke: this._graphSchema.edges[port.type].stroke || this._config.defaultStyles.edge.stroke
},
text: {
text: port.textFormatter ? port.textFormatter(nodeData.attributes) : port.name,
fill: this.getSchemaValue('textColorSecondary'),
'font-size': 14
}
}
}));
}
const containers = [];
if (visibleAttributes) {
visibleAttributes.forEach((attribute, i) => {
const container = new Container({ class: 'graph-node-container' });
const label = new Label({ text: attribute.name, class: 'graph-node-label' });
let input;
let nodeValue;
if (nodeData.attributes) {
if (nodeData.attributes[attribute.name] !== undefined) {
nodeValue = nodeData.attributes[attribute.name];
} else {
Object.keys(nodeData.attributes).forEach((k) => {
const a = nodeData.attributes[k];
if (a.name === attribute.name) {
nodeValue = a.defaultValue;
}
});
}
}
if (!nodeValue) {
nodeValue = nodeData[attribute.name];
}
switch (attribute.type) {
case 'TEXT_INPUT':
input = new TextInput({ class: 'graph-node-input', value: nodeValue });
break;
case 'BOOLEAN_INPUT':
input = new BooleanInput({ class: 'graph-node-input', value: nodeValue });
break;
case 'NUMERIC_INPUT':
input = new NumericInput({ class: 'graph-node-input', hideSlider: true, value: nodeValue && nodeValue.x ? nodeValue.x : nodeValue });
break;
case 'VEC_2_INPUT':
input = new VectorInput({ dimensions: 2,
class: 'graph-node-input',
hideSlider: true,
value: [
nodeValue.x,
nodeValue.y
] });
input.dom.setAttribute('style', 'margin-right: 6px;');
input.inputs.forEach(i => i._sliderControl.dom.remove());
break;
case 'VEC_3_INPUT':
input = new VectorInput({ dimensions: 3,
class: 'graph-node-input',
hideSlider: true,
value: [
nodeValue.x,
nodeValue.y,
nodeValue.z
] });
input.dom.setAttribute('style', 'margin-right: 6px;');
input.inputs.forEach(i => i._sliderControl.dom.remove());
break;
case 'VEC_4_INPUT':
input = new VectorInput({ dimensions: 4,
class: 'graph-node-input',
hideSlider: true,
value: [
nodeValue.x,
nodeValue.y,
nodeValue.z,
nodeValue.w
] });
input.dom.setAttribute('style', 'margin-right: 6px;');
input.inputs.forEach(i => i._sliderControl.dom.remove());
break;
}
input.enabled = !this._graphView._config.readOnly;
input.dom.setAttribute('id', `input_${attribute.name}`);
container.dom.setAttribute('style', `margin-top: ${i === 0 ? 33 + portHeight : 5}px; margin-bottom: 5px;`);
container.append(label);
container.append(input);
containers.push(container);
});
}
const onCellMountedToDom = () => {
const nodeDiv = document.querySelector(`#nodediv_${rect.id}`);
containers.forEach((container) => {
nodeDiv.appendChild(container.dom);
});
this._paper.findViewByModel(rect).on('element:pointerdown', () => {
if (this._hasLinked) {
this._hasLinked = false;
return;
}
onNodeSelected(this.nodeData);
});
};
if (this._graphView._batchingCells) {
this._graphView._cells.push(rect);
this._graphView._cellMountedFunctions.push(onCellMountedToDom);
} else {
this._graph.addCell(rect);
onCellMountedToDom();
}
this.model = rect;
}
getSchemaValue(item) {
return this.nodeSchema[item] !== undefined ? this.nodeSchema[item] : this._config.defaultStyles.node[item];
}
addContextMenu(items) {
if (this._graphView._config.readOnly) return;
this._contextMenu = new Menu({
items: this._graphView._parent._initializeNodeContextMenuItems(this.nodeData, items)
});
this._paper.el.appendChild(this._contextMenu.dom);
const nodeElement = this._paper.findViewByModel(this.model).el;
nodeElement.addEventListener('contextmenu', (e) => {
e.preventDefault();
this._contextMenu.position(e.clientX, e.clientY);
this._contextMenu.hidden = false;
});
}
mapVectorToArray(v) {
const arr = [];
if (Number.isFinite(v.x)) arr.push(v.x);
if (Number.isFinite(v.y)) arr.push(v.y);
if (Number.isFinite(v.z)) arr.push(v.z);
if (Number.isFinite(v.w)) arr.push(v.w);
return arr;
}
updateFormattedTextFields() {
if (this.nodeSchema.headerTextFormatter) {
const formattedText = this.nodeSchema.headerTextFormatter(this.nodeData.attributes, this.nodeData.id);
if (typeof formattedText === 'string') {
this.model.attr('label/text', formattedText);
}
}
if (this.nodeSchema.outPorts) {
this.nodeSchema.outPorts.forEach((port, i) => {
if (port.textFormatter) {
document.getElementById(`${this.nodeData.id}-out${i}`).parentElement.parentElement.querySelector('tspan').innerHTML = port.textFormatter(this.nodeData.attributes);
}
});
}
if (this.nodeSchema.inPorts) {
this.nodeSchema.inPorts.forEach((port, i) => {
if (port.textFormatter) {
document.getElementById(`${this.nodeData.id}-in${i}`).parentElement.parentElement.querySelector('tspan').innerHTML = port.textFormatter(this.nodeData.attributes);
}
});
}
}
updateAttribute(attribute, value) {
this.nodeData.attributes[attribute] = value;
const attributeElement = document.querySelector(`#nodediv_${this.model.id}`).querySelector(`#input_${attribute}`);
if (attributeElement) {
attributeElement.ui.suspendEvents = true;
if (Number.isFinite(value.x)) {
attributeElement.ui.value = this.mapVectorToArray(value);
} else {
attributeElement.ui.value = value;
}
attributeElement.ui.error = false;
attributeElement.ui.suspendEvents = false;
}
this.updateFormattedTextFields();
}
setAttributeErrorState(attribute, value) {
const attributeElement = document.querySelector(`#nodediv_${this.model.id}`).querySelector(`#input_${attribute}`);
if (attributeElement) {
attributeElement.ui.error = value;
}
}
updateNodeType(nodeType) {
this._paper.findViewByModel(this.model).el.removeEventListener('contextmenu', this._contextMenu._contextMenuEvent);
this.addContextMenu(this._graphSchema.nodes[nodeType].contextMenuItems);
}
updatePosition(pos) {
this.model.position(pos.x, pos.y);
}
addEvent(event, callback, attribute) {
const nodeView = this._paper.findViewByModel(this.model);
switch (event) {
case 'updatePosition': {
nodeView.on('element:pointerup', () => {
const newPos = this._graphView.getWindowToGraphPosition(nodeView.getBBox(), false);
callback(this.nodeData.id, newPos);
});
break;
}
case 'updateAttribute': {
const attributeElement = document.querySelector(`#nodediv_${this.model.id}`).querySelector(`#input_${attribute.name}`);
if (!attributeElement) break;
attributeElement.ui.on('change', (value) => {
if (attribute.name === 'name') {
let nameTaken = false;
Object.keys(this._graphView._graphData.get('data.nodes')).forEach((nodeKey) => {
const node = this._graphView._graphData.get('data.nodes')[nodeKey];
if (node.name === value) {
nameTaken = true;
}
});
const attributeElement = document.querySelector(`#nodediv_${this.model.id}`).querySelector(`#input_${attribute.name}`);
if (nameTaken) {
attributeElement.ui.error = true;
return;
}
attributeElement.ui.error = false;
}
callback(this.nodeData.id, attribute, value);
});
break;
}
}
}
select() {
this.model.attr('body/stroke', this.getSchemaValue('strokeSelected'));
this.state = GraphViewNode.STATES.SELECTED;
}
hover() {
if (this.state === GraphViewNode.STATES.SELECTED) return;
this.model.attr('body/stroke', this.getSchemaValue('strokeHover'));
}
hoverRemove() {
if (this.state === GraphViewNode.STATES.DEFAULT) {
this.deselect();
} else if (this.state === GraphViewNode.STATES.SELECTED) {
this.select();
}
}
deselect() {
this.model.attr('body/stroke', this.getSchemaValue('stroke'));
this.state = GraphViewNode.STATES.DEFAULT;
}
}
GraphViewNode.STATES = {
DEFAULT: 0,
SELECTED: 1
};
export default GraphViewNode;

View File

@@ -0,0 +1,447 @@
import { Menu } from '@playcanvas/pcui';
import * as joint from 'jointjs/dist/joint.min.js';
import { GRAPH_ACTIONS } from './constants.js';
import GraphViewEdge from './graph-view-edge.js';
import GraphViewNode from './graph-view-node.js';
import JointGraph from './joint-graph.js';
import { jointShapeElement, jointShapeElementView } from './joint-shape-node.js';
// TODO replace with a lighter math library
import { Vec2 } from './lib/vec2.js';
class GraphView extends JointGraph {
constructor(parent, dom, graphSchema, graphData, config) {
super(dom, config);
this._parent = parent;
this._dom = dom;
this._graphSchema = graphSchema;
this._graphData = graphData;
this._config = config;
this._nodes = {};
this._edges = {};
this._cells = [];
this._cellMountedFunctions = [];
joint.shapes.html = {};
joint.shapes.html.Element = jointShapeElement();
joint.shapes.html.ElementView = jointShapeElementView(this._paper);
this._graph.on('remove', cell => this.updatePortStatesForEdge(cell, false));
this._graph.on('change:target', cell => this.updatePortStatesForEdge(cell, true));
this._paper.on('cell:mousewheel', () => {
parent._dispatchEvent(GRAPH_ACTIONS.UPDATE_SCALE, { scale: this._paper.scale().sx });
});
this._paper.on('blank:mousewheel', () => {
parent._dispatchEvent(GRAPH_ACTIONS.UPDATE_SCALE, { scale: this._paper.scale().sx });
});
this._paper.on('blank:pointerup', (event) => {
parent._dispatchEvent(GRAPH_ACTIONS.UPDATE_TRANSLATE, { pos: { x: this._paper.translate().tx, y: this._paper.translate().ty } });
});
this._paper.on({
'blank:contextmenu': (event) => {
this._viewMenu.position(event.clientX, event.clientY);
this._viewMenu.hidden = false;
}
});
this._paper.on({
'cell:mouseenter': (cellView) => {
let selectedEdge;
let selectedEdgeId;
const node = this.getNode(cellView.model.id);
if (node && node.state !== GraphViewNode.STATES.SELECTED) {
node.hover();
selectedEdge = this._parent._selectedItem && this._parent._selectedItem._type === 'EDGE' ? this.getEdge(this._parent._selectedItem._id) : null;
if (selectedEdge) selectedEdgeId = selectedEdge.model.id;
if (this._config.edgeHoverEffect) {
Object.keys(this._edges).forEach((edgeKey) => {
const currEdge = this.getEdge(edgeKey);
if (currEdge.model.id === selectedEdgeId) return;
if (![currEdge.edgeData.from, currEdge.edgeData.to].includes(node.nodeData.id)) {
currEdge.mute();
} else {
currEdge.deselect();
}
});
}
}
const edge = this.getEdge(cellView.model.id);
if (this._config.edgeHoverEffect && edge && edge.state !== GraphViewEdge.STATES.SELECTED) {
edge.deselect();
selectedEdge = this._parent._selectedItem && this._parent._selectedItem._type === 'EDGE' ? this.getEdge(this._parent._selectedItem._id) : null;
if (selectedEdge) selectedEdgeId = selectedEdge.model.id;
Object.keys(this._edges).forEach((edgeKey) => {
const currEdge = this.getEdge(edgeKey);
if ((edge.model.id !== currEdge.model.id) && (selectedEdgeId !== currEdge.model.id)) {
currEdge.mute();
}
});
this.getNode(edge.edgeData.from).hover();
this.getNode(edge.edgeData.to).hover();
}
},
'cell:mouseleave': (cellView, e) => {
let selectedEdge;
if (e.relatedTarget && e.relatedTarget.classList.contains('graph-node-input')) return;
const node = this.getNode(cellView.model.id);
if (node && node.state !== GraphViewNode.STATES.SELECTED) {
selectedEdge = this._parent._selectedItem && this._parent._selectedItem._type === 'EDGE' ? this.getEdge(this._parent._selectedItem._id) : null;
if (!selectedEdge || ![selectedEdge.edgeData.from, selectedEdge.edgeData.to].includes(node.nodeData.id)) {
node.hoverRemove();
}
if (this._config.edgeHoverEffect) {
Object.keys(this._edges).forEach((edgeKey) => {
const currEdge = this.getEdge(edgeKey);
if (selectedEdge && currEdge.model.id === selectedEdge.model.id) return;
currEdge.deselect();
});
}
}
const edge = this.getEdge(cellView.model.id);
if (this._config.edgeHoverEffect && edge && edge.state !== GraphViewEdge.STATES.SELECTED) {
Object.keys(this._edges).forEach((edgeKey) => {
const currEdge = this.getEdge(edgeKey);
if (currEdge.state === GraphViewEdge.STATES.SELECTED) {
currEdge.select();
} else if (currEdge.state === GraphViewEdge.STATES.DEFAULT) {
if (this._parent._selectedItem && this._parent._selectedItem._type === 'EDGE') {
currEdge.mute();
} else {
currEdge.deselect();
}
}
});
selectedEdge = this._parent._selectedItem && this._parent._selectedItem._type === 'EDGE' ? this.getEdge(this._parent._selectedItem._id) : null;
if (!selectedEdge || ![selectedEdge.edgeData.from, selectedEdge.edgeData.to].includes(edge.edgeData.from)) {
this.getNode(edge.edgeData.from).hoverRemove();
}
if (!selectedEdge || ![selectedEdge.edgeData.from, selectedEdge.edgeData.to].includes(edge.edgeData.to)) {
this.getNode(edge.edgeData.to).hoverRemove();
}
}
}
});
}
batchCells() {
this._batchingCells = true;
}
isBatchingCells() {
return this._batchingCells;
}
addCellMountedFunction(f) {
this._cellMountedFunctions.push(f);
}
applyBatchedCells() {
this._batchingCells = false;
this._graph.addCells(this._cells);
this._cellMountedFunctions.forEach(f => f());
this._cells = [];
this._cellMountedFunctions = [];
}
updatePortStatesForEdge(cell, connected) {
const source = cell.get('source');
const target = cell.get('target');
if (source && source.port && target && target.port) {
this._paper.findViewByModel(source.id)._portElementsCache[source.port].portContentElement.children()[1].attr('visibility', connected ? 'visible' : 'hidden');
this._paper.findViewByModel(target.id)._portElementsCache[target.port].portContentElement.children()[1].attr('visibility', connected ? 'visible' : 'hidden');
}
}
getWindowToGraphPosition(pos, usePaperPosition = true) {
const scale = this._paper.scale().sx;
const translate = this._paper.translate();
if (usePaperPosition) {
const paperPosition = this._paper.el.getBoundingClientRect();
pos.x -= paperPosition.x;
pos.y -= paperPosition.y;
}
return new Vec2(
(-translate.tx / scale) + (pos.x / scale),
(-translate.ty / scale) + (pos.y / scale)
);
}
addCanvasContextMenu(items) {
this._viewMenu = new Menu({
items: items
});
this._paper.el.appendChild(this._viewMenu.dom);
return this._viewMenu._contextMenuEvent;
}
addNodeContextMenu(id, items) {
const addNodeContextMenuFunction = () => {
const node = this.getNode(id);
node.addContextMenu(items);
};
if (this._batchingCells) {
this._cellMountedFunctions.push(addNodeContextMenuFunction);
} else {
addNodeContextMenuFunction();
}
}
addEdgeContextMenu(id, items) {
const edge = this.getEdge(id);
edge.addContextMenu(items);
}
getNode(id) {
return this._nodes[id];
}
addNode(nodeData, nodeSchema, onCreateEdge, onNodeSelected) {
const node = new GraphViewNode(
this,
this._paper,
this._graph,
this._graphSchema,
nodeData,
nodeSchema,
onCreateEdge,
onNodeSelected
);
this._nodes[nodeData.id] = node;
this._nodes[node.model.id] = node;
return node.nodeData;
}
removeNode(modelId) {
const node = this.getNode(modelId);
this._graph.removeCells(node.model);
delete this._nodes[node.nodeData.id];
delete this._nodes[modelId];
}
updateNodeAttribute(id, attribute, value) {
this.getNode(id).updateAttribute(attribute, value);
}
setNodeAttributeErrorState(id, attribute, value) {
this.getNode(id).setAttributeErrorState(attribute, value);
}
updateNodePosition(id, pos) {
this.getNode(id).updatePosition(pos);
}
updateNodeType(id, nodeType) {
this.getNode(id).updateNodeType(nodeType);
}
addNodeEvent(id, event, callback, attribute) {
const addNodeEventFunction = () => {
const node = this.getNode(id);
node.addEvent(event, callback, attribute);
};
if (this._batchingCells) {
this._cellMountedFunctions.push(addNodeEventFunction);
} else {
addNodeEventFunction();
}
}
getEdge(id) {
if (this._edges[id]) {
return this._edges[id];
}
}
addEdge(edgeData, edgeSchema, onEdgeSelected) {
let edge;
if (Number.isFinite(edgeData.outPort)) {
edge = this.getEdge(`${edgeData.from},${edgeData.outPort}-${edgeData.to},${edgeData.inPort}`);
} else {
edge = this.getEdge(`${edgeData.from}-${edgeData.to}`);
}
if (edge) {
if (edgeData.to === edge.edgeData.to) {
if (!edgeData.outPort) {
edge.addTargetMarker();
}
} else {
if (!edgeData.inPort) {
edge.addSourceMarker();
}
}
} else {
edge = new GraphViewEdge(
this,
this._paper,
this._graph,
this._graphSchema,
edgeData,
edgeSchema,
onEdgeSelected
);
if (Number.isFinite(edgeData.outPort)) {
this._edges[`${edgeData.from},${edgeData.outPort}-${edgeData.to},${edgeData.inPort}`] = edge;
} else {
this._edges[`${edgeData.from}-${edgeData.to}`] = edge;
}
this._edges[edge.model.id] = edge;
}
return edge.edgeData;
}
removeEdge(id) {
const edge = this.getEdge(id);
if (edge) {
this._graph.removeCells(edge.model);
delete this._edges[edge.model.id];
}
delete this._edges[id];
}
disableInputEvents() {
document.querySelectorAll('.graph-node-input').forEach((input) => {
input.classList.add('graph-node-input-no-pointer-events');
});
}
enableInputEvents() {
document.querySelectorAll('.graph-node-input').forEach((input) => {
input.classList.remove('graph-node-input-no-pointer-events');
});
}
addUnconnectedEdge(nodeId, edgeType, edgeSchema, validateEdge, onEdgeConnected) {
this.disableInputEvents();
const link = GraphViewEdge.createLink(this._config.defaultStyles, edgeSchema);
link.source(this.getNode(nodeId).model);
link.target(this.getNode(nodeId).model);
const mouseMoveEvent = (e) => {
const mousePos = this.getWindowToGraphPosition(new Vec2(e.clientX, e.clientY));
const sourceNodeView = this._paper.findViewByModel(this.getNode(nodeId).model);
const sourceNodePos = this.getGraphPosition(sourceNodeView.el.getBoundingClientRect());
let pointerVector = mousePos.clone().sub(sourceNodePos);
const direction = (new Vec2(e.clientX, e.clientY)).clone().sub(sourceNodeView.el.getBoundingClientRect()).normalize().mulScalar(20);
pointerVector = sourceNodePos.add(pointerVector).sub(direction);
link.target({
x: pointerVector.x,
y: pointerVector.y
});
};
const cellPointerDownEvent = (cellView) => {
if (!this.getNode(cellView.model.id)) return;
const targetNodeId = this.getNode(cellView.model.id).nodeData.id;
const nodeModel = this.getNode(nodeId).model;
// test whether a valid connection has been made
if ((cellView.model.id !== nodeModel.id) && !cellView.model.isLink() && validateEdge(edgeType, nodeId, targetNodeId)) {
link.target(cellView.model);
onEdgeConnected(edgeType, nodeId, targetNodeId);
}
this._graph.removeCells(link);
document.removeEventListener('mousemove', mouseMoveEvent);
this._paper.off('cell:pointerdown', cellPointerDownEvent);
this.enableInputEvents();
};
const mouseDownEvent = () => {
this._paper.off('cell:pointerdown', cellPointerDownEvent);
document.removeEventListener('mousemove', mouseMoveEvent);
this._graph.removeCells(link);
this.enableInputEvents();
};
document.addEventListener('mousemove', mouseMoveEvent);
document.addEventListener('mousedown', mouseDownEvent);
this._paper.on('cell:pointerdown', cellPointerDownEvent);
this._graph.addCell(link);
}
onBlankSelection(callback) {
this._paper.on('blank:pointerdown', () => {
callback();
});
}
selectNode(id) {
const node = this.getNode(id);
if (node) {
node.select();
Object.keys(this._edges).forEach((edgeKey) => {
const currEdge = this.getEdge(edgeKey);
currEdge.deselect();
});
}
}
deselectNode(id) {
const node = this.getNode(id);
if (node) node.deselect();
}
selectEdge(id) {
const edge = this.getEdge(id);
if (edge) {
edge.select();
Object.keys(this._edges).forEach((edgeKey) => {
const currEdge = this.getEdge(edgeKey);
if (edge.model.id !== currEdge.model.id) {
currEdge.mute();
}
});
this.getNode(edge.edgeData.from).hover();
this.getNode(edge.edgeData.to).hover();
}
}
deselectEdge(id) {
const edge = this.getEdge(id);
if (edge) {
Object.keys(this._edges).forEach((edgeKey) => {
const currEdge = this.getEdge(edgeKey);
currEdge.deselect();
});
this.getNode(edge.edgeData.from).hoverRemove();
this.getNode(edge.edgeData.to).hoverRemove();
}
}
setGraphPosition(posX, posY) {
this._paper.translate(posX, posY);
}
getGraphPosition() {
const t = this._paper.translate();
return new Vec2([t.tx, t.ty]);
}
setGraphScale(scale) {
this._paper.scale(scale);
}
getGraphScale() {
return this._paper.scale().sx;
}
getNodeDomElement(id) {
return this.getNode(id).model.findView(this._paper).el;
}
getEdgeDomElement(id) {
return this.getEdge(id).model.findView(this._paper).el;
}
destroy() {
this._graph.clear();
this._paper.remove();
}
}
export default GraphView;

View File

@@ -0,0 +1,670 @@
/**
* The PCUIGraph module is an extension of the PlayCanvas User Interface (PCUI) framework. It
* provides a new PCUI Element type for building interactive, node-based graphs.
*
* Key features include:
*
* - Scalable and customizable node-based graphs for visualizing complex data.
* - Interactive elements such as draggable nodes, clickable edges, and zoomable views.
* - Easy integration within a PCUI-based user interface.
*
* Whether it's for displaying network topologies, process flows, or complex relational data,
* PCUIGraph provides a robust and flexible solution for integrating graph visualizations into your
* web projects.
*
* @module PCUIGraph
*/
import { Observer } from '@playcanvas/observer';
import { Element } from '@playcanvas/pcui';
import { GRAPH_ACTIONS, DEFAULT_CONFIG } from './constants.js';
import GraphView from './graph-view.js';
import SelectedItem from './selected-item.js';
import { deepCopyFunction } from './util.js';
/**
* Represents a new Graph.
*/
class Graph extends Element {
/**
* Creates a new Graph.
*
* @param {object} schema - The graph schema.
* @param {object} [options] - The graph configuration. Optional.
* @param {object} [options.initialData] - The graph data to initialize the graph with.
* @param {HTMLElement} [options.dom] - If supplied, the graph will be attached to this element.
* @param {object[]} [options.contextMenuItems] - The context menu items to add to the graph.
* @param {boolean} [options.readOnly] - Whether the graph is read only. Optional. Defaults to
* false.
* @param {boolean} [options.passiveUIEvents] - If true, the graph will not update its data and
* view upon user interaction. Instead, these interactions can be handled explicitly by
* listening to fired events. Optional. Defaults to false.
* @param {boolean} [options.incrementNodeNames] - Whether the graph should increment the node
* name when a node with the same name already exists. Optional. Defaults to false.
* @param {boolean} [options.restrictTranslate] - Whether the graph should restrict the
* translate graph operation to the graph area. Optional. Defaults to false.
* @param {boolean} [options.edgeHoverEffect] - Whether the graph should show an edge highlight
* effect when the mouse is hovering over edges. Optional. Defaults to true.
* @param {object} [options.defaultStyles] - Used to override the graph's default styling. Check
* ./constants.js for a full list of style properties.
* @param {object} [options.adjustVertices] - If true, multiple edges connected between two
* nodes will be spaced apart.
*/
constructor(schema, options = {}) {
super({
dom: options.dom
});
this.class.add('pcui-graph');
this._graphSchema = schema;
this._graphData = new Observer({ data: options.initialData ? options.initialData : {} });
this._contextMenuItems = options.contextMenuItems || [];
this._suppressGraphDataEvents = false;
this._config = {
...DEFAULT_CONFIG,
readOnly: options.readOnly,
passiveUIEvents: options.passiveUIEvents,
incrementNodeNames: options.incrementNodeNames,
restrictTranslate: options.restrictTranslate,
edgeHoverEffect: options.edgeHoverEffect,
includeFonts: options.includeFonts,
adjustVertices: options.adjustVertices
};
if (options.defaultStyles) {
if (options.defaultStyles.background) {
this._config.defaultStyles.background = {
...this._config.defaultStyles.background,
...options.defaultStyles.background
};
}
if (options.defaultStyles.edge) {
this._config.defaultStyles.edge = {
...this._config.defaultStyles.edge,
...options.defaultStyles.edge
};
}
if (options.defaultStyles.node) {
this._config.defaultStyles.node = {
...this._config.defaultStyles.node,
...options.defaultStyles.node
};
}
}
if (this._config.readOnly) this._config.selfContainedMode = true;
this._buildGraphFromData();
if (options.defaultStyles.initialScale) {
this.setGraphScale(options.defaultStyles.initialScale);
}
if (options.defaultStyles.initialPosition) {
this.setGraphPosition(options.defaultStyles.initialPosition.x, options.defaultStyles.initialPosition.y);
}
}
/**
* The current graph data. Contains an object with any nodes and edges present in the graph.
* This can be passed into the graph constructor to reload the current graph.
*
* @type {object}
*/
get data() {
return this._graphData.get('data');
}
/**
* Destroy the graph. Clears the graph from the DOM and removes all event listeners associated
* with the graph.
*/
destroy() {
this.view.destroy();
}
_buildGraphFromData() {
this.view = new GraphView(this, this.dom, this._graphSchema, this._graphData, this._config);
this.view.batchCells();
const nodes = this._graphData.get('data.nodes');
if (nodes) {
Object.keys(nodes).forEach((nodeKey) => {
const node = nodes[nodeKey];
const nodeSchema = this._graphSchema.nodes[node.nodeType];
if (nodeSchema.attributes) {
if (nodeSchema.attributes && !node.attributes) {
node.attributes = {};
}
nodeSchema.attributes.forEach((attribute) => {
if (!node.attributes[attribute.name] && attribute.defaultValue) {
this._suppressGraphDataEvents = true;
this._graphData.set(`data.nodes.${nodeKey}.attributes.${attribute.name}`, attribute.defaultValue);
this._suppressGraphDataEvents = false;
}
});
}
this.createNode(this._graphData.get(`data.nodes.${nodeKey}`), undefined, true);
});
}
const edges = this._graphData.get('data.edges');
if (edges) {
Object.keys(edges).forEach((edgeKey) => {
this.createEdge(edges[edgeKey], edgeKey, true);
});
}
this.view.applyBatchedCells();
// handle context menus
if (!this._config.readOnly) {
this._addCanvasContextMenu();
}
this._selectedItem = null;
this.view.onBlankSelection(() => {
this._dispatchEvent(GRAPH_ACTIONS.DESELECT_ITEM, { prevItem: this._selectedItem });
});
if (!this._config.passiveUIEvents) {
this._registerInternalEventListeners();
}
}
_addCanvasContextMenu() {
const updateItem = (item) => {
switch (item.action) {
case GRAPH_ACTIONS.ADD_NODE: {
item.onSelect = (e) => {
const node = {
...item,
id: Number(`${Date.now()}${Math.floor(Math.random() * 10000)}`)
};
if (item.attributes) {
node.attributes = { ...item.attributes };
}
delete node.action;
delete node.text;
delete node.onClick;
const nodeSchema = this._graphSchema.nodes[node.nodeType];
if (nodeSchema.attributes && !node.attributes) {
node.attributes = {};
}
if (nodeSchema.attributes) {
nodeSchema.attributes.forEach((attribute) => {
if (!node.attributes[attribute.name] && attribute.defaultValue) {
node.attributes[attribute.name] = attribute.defaultValue;
}
});
}
if (this._config.incrementNodeNames && node.attributes.name) {
node.attributes.name = `${node.attributes.name} ${Object.keys(this._graphData.get('data.nodes')).length}`;
}
let element = e.target;
while (!element.classList.contains('pcui-menu-items')) {
element = element.parentElement;
}
let pos = {
x: Number(element.style.left.replace('px', '')),
y: Number(element.style.top.replace('px', ''))
};
pos = this.getWindowToGraphPosition(pos);
node.posX = pos.x;
node.posY = pos.y;
this._dispatchEvent(GRAPH_ACTIONS.ADD_NODE, { node });
};
}
}
return item;
};
const viewContextMenuItems = this._contextMenuItems.map((item) => {
item = updateItem(item);
if (!item.items) return item;
item.items.map((subitem) => {
return updateItem(subitem);
});
return item;
});
this.view.addCanvasContextMenu(viewContextMenuItems);
}
/**
* Select a node in the current graph.
*
* @param {object} node - The node to select
*/
selectNode(node) {
this.deselectItem();
this._selectedItem = new SelectedItem(this, 'NODE', node.id);
this._selectedItem.selectItem();
}
/**
* Select an edge in the current graph.
*
* @param {object} edge - The edge to select
* @param {number} edgeId - The edge id of the edge to select
*/
selectEdge(edge, edgeId) {
this.deselectItem();
this._selectedItem = new SelectedItem(this, 'EDGE', `${edge.from}-${edge.to}`, edgeId);
this._selectedItem.selectItem();
}
/**
* Deselect the currently selected item in the graph.
*/
deselectItem() {
if (this._selectedItem) {
this._selectedItem.deselectItem();
this._selectedItem = null;
}
}
_isValidEdge(edgeType, source, target) {
const edge = this._graphSchema.edges[edgeType];
return edge.from.includes(this._graphData.get(`data.nodes.${source}.nodeType`)) && edge.to.includes(this._graphData.get(`data.nodes.${target}.nodeType`));
}
/**
* Add an edge to the graph.
*
* @param {object} edge - The edge to add.
* @param {number} edgeId - The edge id for the new edge.
*/
createEdge(edge, edgeId) {
const edgeSchema = this._graphSchema.edges[edge.edgeType];
this.view.addEdge(edge, edgeSchema, (edge) => {
this._dispatchEvent(GRAPH_ACTIONS.SELECT_EDGE, { edge, prevItem: this._selectedItem });
});
if (edgeSchema.contextMenuItems) {
const contextMenuItems = deepCopyFunction(edgeSchema.contextMenuItems).map((item) => {
if (item.action === GRAPH_ACTIONS.DELETE_EDGE) {
item.onSelect = () => {
this._dispatchEvent(GRAPH_ACTIONS.DELETE_EDGE, { edgeId: edgeId, edge: this._graphData.get(`data.edges.${edgeId}`) });
};
}
return item;
});
const addEdgeContextMenuFunction = () => {
if (Number.isFinite(edge.outPort)) {
this.view.addEdgeContextMenu(`${edge.from},${edge.outPort}-${edge.to},${edge.inPort}`, contextMenuItems);
} else {
this.view.addEdgeContextMenu(`${edge.from}-${edge.to}`, contextMenuItems);
}
};
if (this.view.isBatchingCells()) {
this.view.addCellMountedFunction(addEdgeContextMenuFunction);
} else {
addEdgeContextMenuFunction();
}
}
if (!this._graphData.get(`data.edges.${edgeId}`)) {
this._graphData.set(`data.edges.${edgeId}`, edge);
}
}
_onEdgeConnected(edgeType, from, to) {
const edgeId = Number(`${Date.now()}${Math.floor(Math.random() * 10000)}`);
const edge = {
from: from,
to: to,
edgeType: edgeType,
conditions: {}
};
this._dispatchEvent(GRAPH_ACTIONS.ADD_EDGE, { edge, edgeId });
}
_createUnconnectedEdgeForNode(node, edgeType) {
const edgeSchema = this._graphSchema.edges[edgeType];
this.view.addUnconnectedEdge(node.id, edgeType, edgeSchema, this._isValidEdge.bind(this), this._onEdgeConnected.bind(this));
}
_onCreateEdge(edgeId, edge) {
this._dispatchEvent(GRAPH_ACTIONS.ADD_EDGE, { edge, edgeId });
}
_onNodeSelected(node) {
if (this.suppressNodeSelect) {
this.suppressNodeSelect = false;
} else {
this._dispatchEvent(GRAPH_ACTIONS.SELECT_NODE, { node, prevItem: this._selectedItem });
}
}
_onNodePositionUpdated(nodeId, pos) {
const node = this._graphData.get(`data.nodes.${nodeId}`);
const prevPosX = node.posX;
const prevPosY = node.posY;
if (pos.x !== node.posX || pos.y !== node.posY) {
node.posX = pos.x;
node.posY = pos.y;
this.updateNodePosition(nodeId, { x: prevPosX, y: prevPosY });
this._dispatchEvent(GRAPH_ACTIONS.UPDATE_NODE_POSITION, { nodeId, node });
}
}
_onNodeAttributeUpdated(nodeId, attribute, value) {
const node = this._graphData.get(`data.nodes.${nodeId}`);
let prevAttributeValue;
let attributeKey = node.attributes[attribute.name] !== undefined ? attribute.name : undefined;
if (!attributeKey) {
Object.keys(node.attributes).forEach((k) => {
const item = node.attributes[k];
if (item.name === attribute.name) attributeKey = k;
});
}
if (Number.isFinite(node.attributes[attributeKey].x)) {
prevAttributeValue = { ...node.attributes[attributeKey] };
} else {
prevAttributeValue = node.attributes[attributeKey];
}
if (Array.isArray(value)) {
const keyMap = ['x', 'y', 'z', 'w'];
value.forEach((v, i) => {
node.attributes[attributeKey][keyMap[i]] = v;
});
} else if (Object.keys(prevAttributeValue).includes('x') && Number.isFinite(value)) {
node.attributes[attributeKey].x = value;
} else {
node.attributes[attributeKey] = value;
}
if (JSON.stringify(node.attributes[attributeKey]) === JSON.stringify(prevAttributeValue)) return;
this.updateNodeAttribute(nodeId, attribute.name, value);
this._dispatchEvent(
GRAPH_ACTIONS.UPDATE_NODE_ATTRIBUTE,
{
node: node,
attribute: attribute.name,
attributeKey: attributeKey
}
);
}
_initializeNodeContextMenuItems(node, items) {
const contextMenuItems = deepCopyFunction(items).map((item) => {
if (item.action === GRAPH_ACTIONS.ADD_EDGE) {
item.onSelect = () => this._createUnconnectedEdgeForNode(node, item.edgeType);
}
if (item.action === GRAPH_ACTIONS.DELETE_NODE) {
item.onSelect = () => {
this._dispatchEvent(GRAPH_ACTIONS.DELETE_NODE, this._deleteNode(node.id));
};
}
return item;
});
return contextMenuItems;
}
/**
* Add a node to the graph.
*
* @param {object} node - The node to add.
*/
createNode(node) {
const nodeSchema = this._graphSchema.nodes[node.nodeType];
node = this.view.addNode(
node,
nodeSchema,
this._onCreateEdge.bind(this),
this._onNodeSelected.bind(this)
);
if (!this._graphData.get(`data.nodes.${node.id}`)) {
this._graphData.set(`data.nodes.${node.id}`, node);
}
this.view.addNodeEvent(
node.id,
'updatePosition',
this._onNodePositionUpdated.bind(this)
);
if (nodeSchema.attributes) {
nodeSchema.attributes.forEach((attribute) => {
this.view.addNodeEvent(
node.id,
'updateAttribute',
this._onNodeAttributeUpdated.bind(this),
attribute
);
});
}
if (nodeSchema.contextMenuItems) {
const contextMenuItems = this._initializeNodeContextMenuItems(node, nodeSchema.contextMenuItems);
this.view.addNodeContextMenu(node.id, contextMenuItems);
}
}
/**
* Update the position of a node.
*
* @param {number} nodeId - The node to add.
* @param {object} pos - The new position, given as an object containing x and y properties.
*/
updateNodePosition(nodeId, pos) {
if (!this._graphData.get(`data.nodes.${nodeId}`)) return;
this._graphData.set(`data.nodes.${nodeId}.posX`, pos.x);
this._graphData.set(`data.nodes.${nodeId}.posY`, pos.y);
this.view.updateNodePosition(nodeId, pos);
}
/**
* Update the value of an attribute of a node.
*
* @param {number} nodeId - The node to update.
* @param {string} attributeName - The name of the attribute to update.
* @param {object} value - The new value for the attribute.
*/
updateNodeAttribute(nodeId, attributeName, value) {
if (!this._graphData.get(`data.nodes.${nodeId}`)) return;
this._graphData.set(`data.nodes.${nodeId}.attributes.${attributeName}`, value);
this.view.updateNodeAttribute(nodeId, attributeName, value);
}
/**
* Set the error state of a node attribute.
*
* @param {number} nodeId - The node to update.
* @param {string} attributeName - The name of the attribute to update.
* @param {boolean} value - Whether the attribute should be set in the error state.
*/
setNodeAttributeErrorState(nodeId, attributeName, value) {
if (!this._graphData.get(`data.nodes.${nodeId}`)) return;
this.view.setNodeAttributeErrorState(nodeId, attributeName, value);
}
/**
* Update the type of a node.
*
* @param {number} nodeId - The node to update.
* @param {string} nodeType - The new type for the node.
*/
updateNodeType(nodeId, nodeType) {
if (Number.isFinite(nodeType) && this._graphData.get(`data.nodes.${nodeId}`)) {
this._graphData.set(`data.nodes.${nodeId}.nodeType`, nodeType);
this.view.updateNodeType(nodeId, nodeType);
}
}
_deleteNode(nodeId) {
if (!this._graphData.get(`data.nodes.${nodeId}`)) return;
if (this._selectedItem && this._selectedItem._id === nodeId) this.deselectItem();
const node = this._graphData.get(`data.nodes.${nodeId}`);
const edges = [];
const edgeData = {};
const edgeKeys = Object.keys(this._graphData.get('data.edges'));
for (let i = 0; i < edgeKeys.length; i++) {
const edge = this._graphData.get(`data.edges.${edgeKeys[i]}`);
edgeData[edgeKeys[i]] = edge;
if (edge.from === nodeId || edge.to === nodeId) {
edges.push(edgeKeys[i]);
}
}
return { node, edges, edgeData };
}
/**
* Delete a node from the graph.
*
* @param {number} nodeId - The node to delete.
*/
deleteNode(nodeId) {
const { node, edges, edgeData } = this._deleteNode(nodeId);
Object.values(edges).forEach((e) => {
const edge = edgeData[e];
this.deleteEdge(`${edge.from}-${edge.to}`);
});
this._graphData.unset(`data.nodes.${nodeId}`);
this.view.removeNode(node.id);
}
/**
* Delete an edge from the graph.
*
* @param {string} edgeId - The edge to delete.
*/
deleteEdge(edgeId) {
if (!this._graphData.get(`data.edges.${edgeId}`)) return;
const { from, to, outPort, inPort } = this._graphData.get(`data.edges.${edgeId}`) || {};
if (this._selectedItem && this._selectedItem._id === `${from}-${to}`) this.deselectItem();
if (Number.isFinite(outPort)) {
this.view.removeEdge(`${from},${outPort}-${to},${inPort}`);
} else {
this.view.removeEdge(`${from}-${to}`);
}
this.view.removeEdge(`${from}-${to}`);
this._graphData.unset(`data.edges.${edgeId}`);
const edges = this._graphData.get('data.edges');
Object.keys(edges).forEach((edgeKey) => {
const edge = edges[edgeKey];
const edgeSchema = this._graphSchema.edges[edge.edgeType];
if ([edge.from, edge.to].includes(from) && [edge.from, edge.to].includes(to)) {
this.view.addEdge(edge, edgeSchema, (edge) => {
this.selectEdge(edge, edgeKey);
});
this.selectEdge(edge, edgeKey);
}
});
}
/**
* Set the center of the viewport to the given position.
*
* @param {number} posX - The x position to set the center of the viewport to.
* @param {number} posY - The y position to set the center of the viewport to.
*/
setGraphPosition(posX, posY) {
this.view.setGraphPosition(posX, posY);
}
/**
* Get the current center position of the viewport in the graph.
*
* @returns {object} The current center position of the viewport in the graph as an object
* containing x and y.
*/
getGraphPosition() {
return this.view.getGraphPosition();
}
/**
* Set the scale of the graph.
*
* @param {number} scale - The new scale of the graph.
*/
setGraphScale(scale) {
this.view.setGraphScale(scale);
Object.keys(this.view._nodes).forEach((nodeKey) => {
this.view._paper.findViewByModel(this.view._nodes[nodeKey].model).updateBox();
});
}
/**
* Get the current scale of the graph.
*
* @returns {number} The current scale of the graph.
*/
getGraphScale() {
return this.view.getGraphScale();
}
/**
* Convert a position in window space to a position in graph space.
*
* @param {object} pos - A position in the window, as an object containing x and y.
* @returns {object} The position in the graph based on the given window position, as an object
* containing x and y.
*/
getWindowToGraphPosition(pos) {
return this.view.getWindowToGraphPosition(pos);
}
/**
* Add an event listener to the graph.
*
* @param {string} eventName - The name of the event to listen for.
* @param {Function} callback - The callback to call when the event is triggered.
*/
on(eventName, callback) {
if (this._config.readOnly && (!eventName.includes('EVENT_SELECT_') && !eventName.includes('EVENT_DESELECT'))) return;
this.dom.addEventListener(eventName, (e) => {
callback(e.detail);
});
}
_dispatchEvent(action, data) {
this.dom.dispatchEvent(new CustomEvent(action, { detail: data }));
}
_registerInternalEventListeners() {
this.on(GRAPH_ACTIONS.ADD_NODE, ({ node }) => {
this.createNode(node);
this.selectNode(node);
});
this.on(GRAPH_ACTIONS.DELETE_NODE, ({ node, edgeData, edges }) => {
this.deleteNode(node.id);
});
this.on(GRAPH_ACTIONS.SELECT_NODE, ({ node }) => {
if (this._selectedItem) {
this._selectedItem.deselectItem();
}
this._selectedItem = new SelectedItem(this, 'NODE', node.id);
this._selectedItem.selectItem();
});
this.on(GRAPH_ACTIONS.UPDATE_NODE_POSITION, ({ nodeId, node }) => {
this.updateNodePosition(nodeId, { x: node.posX, y: node.posY });
});
this.on(GRAPH_ACTIONS.UPDATE_NODE_ATTRIBUTE, ({ node }) => {
this._graphData.set(`data.nodes.${node.id}`, node);
});
this.on(GRAPH_ACTIONS.ADD_EDGE, ({ edge, edgeId }) => {
if (Number.isFinite(edge.inPort)) {
Object.keys(this._graphData.get('data.edges')).forEach((edgeKey) => {
const edgeToCompare = this._graphData.get(`data.edges.${edgeKey}`);
if (edgeToCompare.to === edge.to && edgeToCompare.inPort === edge.inPort) {
this.deleteEdge(edgeKey);
}
});
}
this.createEdge(edge, edgeId);
this.suppressNodeSelect = true;
this.selectEdge(edge, edgeId);
});
this.on(GRAPH_ACTIONS.DELETE_EDGE, ({ edgeId }) => {
this.deleteEdge(edgeId);
});
this.on(GRAPH_ACTIONS.SELECT_EDGE, ({ edge }) => {
if (this._selectedItem) {
this._selectedItem.deselectItem();
}
this._selectedItem = new SelectedItem(this, 'EDGE', `${edge.from}-${edge.to}`);
this._selectedItem.selectItem();
});
this.on(GRAPH_ACTIONS.DESELECT_ITEM, () => {
this.deselectItem();
});
}
}
Graph.GRAPH_ACTIONS = GRAPH_ACTIONS;
export default Graph;

View File

@@ -0,0 +1,287 @@
import 'jquery';
import * as joint from 'jointjs/dist/joint.min.js';
import _ from 'lodash';
import 'backbone';
// TODO replace with a lighter math library
import { Vec2 } from './lib/vec2.js';
joint.V.matrixToTransformString = function (matrix) {
matrix || (matrix = true); // eslint-disable-line no-unused-expressions
return `matrix(${[
matrix.a || 1,
matrix.b || 0,
matrix.c || 0,
matrix.d || 1,
matrix.e || 0,
matrix.f || 0
]})`;
};
joint.V.prototype.transform = function (matrix, opt) {
const node = this.node;
if (joint.V.isUndefined(matrix)) {
return (node.parentNode) ?
this.getTransformToElement(node.parentNode) :
node.getScreenCTM();
}
if (opt && opt.absolute) {
return this.attr('transform', joint.V.matrixToTransformString(matrix));
}
const svgTransform = joint.V.createSVGTransform(matrix);
node.transform.baseVal.appendItem(svgTransform);
return this;
};
class JointGraph {
constructor(dom, config = {}) {
this._config = config;
this._graph = new joint.dia.Graph({}, { cellNamespace: joint.shape });
this._paper = new joint.dia.Paper({
el: dom,
model: this._graph,
width: dom.offsetWidth,
cellViewNamespace: joint.shapes,
height: dom.offsetHeight,
clickThreshold: 1,
restrictTranslate: this._config.restrictTranslate,
background: {
color: config.defaultStyles.background.color
},
gridSize: config.defaultStyles.background.gridSize,
linkPinning: false,
interactive: !this._config.readOnly,
defaultLink: (cellView, magnet) => {
const defaultLink = new joint.shapes.standard.Link({
connector: {
name: 'normal'
}
});
defaultLink.attr({
line: {
stroke: joint.V(magnet).attr('stroke'),
strokeWidth: 2,
targetMarker: null
}
});
return defaultLink;
},
validateConnection: (cellViewS, magnetS, cellViewT, magnetT, end, linkView) => {
if (joint.V(cellViewS).id === joint.V(cellViewT).id) return false;
if (!joint.V(magnetS) || !joint.V(magnetT)) return false;
const sPort = joint.V(magnetS).attr('port');
const tPort = joint.V(magnetT.parentNode).attr('port');
if ((sPort.includes('in') && tPort.includes('in')) || (sPort.includes('out') && tPort.includes('out'))) return false;
if (sPort.includes('in') && joint.V(magnetS.children[1]).attr().visibility !== 'hidden') return false;
// if (tPort.includes('in') && joint.V(magnetT.parentNode.children[1]).attr().visibility !== 'hidden') return false;
if (cellViewS._portElementsCache[sPort].portContentElement.children()[0].attr().edgeType !== cellViewT._portElementsCache[tPort].portContentElement.children()[0].attr().edgeType) return false;
return true;
},
markAvailable: true,
drawGrid: {
name: 'doubleMesh',
args: [
{ color: '#0e1923', thickness: 1 },
{ color: '#06101b', scaleFactor: 10, thickness: 2 }
]
}
});
const graphResizeObserver = new ResizeObserver((_) => {
this._resizeGraph(dom);
});
graphResizeObserver.observe(dom);
this._panPaper = false;
this._translate = new Vec2();
this._totalTranslate = new Vec2();
this._pan = new Vec2();
this._mousePos = new Vec2();
this._paper.on('blank:pointerdown', (e) => {
this._panPaper = true;
this._mousePos = new Vec2(e.offsetX, e.offsetY);
});
this._paper.on('blank:pointerup', () => {
this._panPaper = false;
this._translate.add(this._pan);
});
dom.addEventListener('mousemove', (e) => {
if (this._panPaper) {
this._pan = this._mousePos.clone().sub(new Vec2(e.offsetX, e.offsetY));
this._mousePos = new Vec2(e.offsetX, e.offsetY);
this._paper.translate(this._paper.translate().tx - this._pan.x, this._paper.translate().ty - this._pan.y);
}
});
const handleCanvasMouseWheel = (e, x, y, delta) => {
e.preventDefault();
const oldScale = this._paper.scale().sx;
const newScale = oldScale + delta * 0.025;
this._scaleToPoint(newScale, x, y);
};
const handleCellMouseWheel = (cellView, e, x, y, delta) => handleCanvasMouseWheel(e, x, y, delta);
this._paper.on('cell:mousewheel', handleCellMouseWheel);
this._paper.on('blank:mousewheel', handleCanvasMouseWheel);
if (config.adjustVertices) {
const adjustGraphVertices = _.partial(this.adjustVertices.bind(this), this._graph);
// adjust vertices when a cell is removed or its source/target was changed
this._graph.on('add remove change:source change:target', adjustGraphVertices);
// adjust vertices when the user stops interacting with an element
this._paper.on('cell:pointerup', adjustGraphVertices);
}
}
_resizeGraph(dom) {
this._paper.setDimensions(dom.offsetWidth, dom.offsetHeight);
}
_scaleToPoint(nextScale, x, y) {
if (nextScale >= (this._config.minZoom || 0.25) && nextScale <= (this._config.maxZoom || 1.5)) {
const currentScale = this._paper.scale().sx;
const beta = currentScale / nextScale;
const ax = x - (x * beta);
const ay = y - (y * beta);
const translate = this._paper.translate();
const nextTx = translate.tx - ax * nextScale;
const nextTy = translate.ty - ay * nextScale;
this._paper.translate(nextTx, nextTy);
const ctm = this._paper.matrix();
ctm.a = nextScale;
ctm.d = nextScale;
this._paper.matrix(ctm);
}
}
adjustVertices(graph, cell) {
if (this.ignoreAdjustVertices) return;
// if `cell` is a view, find its model
cell = cell.model || cell;
if (cell instanceof joint.dia.Element) {
// `cell` is an element
_.chain(graph.getConnectedLinks(cell))
.groupBy((link) => {
// the key of the group is the model id of the link's source or target
// cell id is omitted
return _.omit([link.source().id, link.target().id], cell.id)[0];
})
.each((group, key) => {
// if the member of the group has both source and target model
// then adjust vertices
if (key !== 'undefined') this.adjustVertices(graph, _.first(group));
})
.value();
return;
}
// `cell` is a link
// get its source and target model IDs
const sourceId = cell.get('source').id || cell.previous('source').id;
const targetId = cell.get('target').id || cell.previous('target').id;
// if one of the ends is not a model
// (if the link is pinned to paper at a point)
// the link is interpreted as having no siblings
if (!sourceId || !targetId) return;
// identify link siblings
const siblings = _.filter(graph.getLinks(), (sibling) => {
const siblingSourceId = sibling.source().id;
const siblingTargetId = sibling.target().id;
// if source and target are the same
// or if source and target are reversed
return ((siblingSourceId === sourceId) && (siblingTargetId === targetId)) ||
((siblingSourceId === targetId) && (siblingTargetId === sourceId));
});
const numSiblings = siblings.length;
switch (numSiblings) {
case 0: {
// the link has no siblings
break;
} case 1: {
// there is only one link
// no vertices needed
cell.unset('vertices');
cell.set('connector', { name: 'normal' });
break;
} default: {
// there are multiple siblings
// we need to create vertices
// find the middle point of the link
const sourceCenter = graph.getCell(sourceId).getBBox().center();
const targetCenter = graph.getCell(targetId).getBBox().center();
joint.g.Line(sourceCenter, targetCenter).midpoint();
// find the angle of the link
const theta = sourceCenter.theta(targetCenter);
// constant
// the maximum distance between two sibling links
const GAP = 20;
_.each(siblings, (sibling, index) => {
// we want offset values to be calculated as 0, 20, 20, 40, 40, 60, 60 ...
let offset = GAP * Math.ceil(index / 2);
// place the vertices at points which are `offset` pixels perpendicularly away
// from the first link
//
// as index goes up, alternate left and right
//
// ^ odd indices
// |
// |----> index 0 sibling - centerline (between source and target centers)
// |
// v even indices
const sign = ((index % 2) ? 1 : -1);
// to assure symmetry, if there is an even number of siblings
// shift all vertices leftward perpendicularly away from the centerline
if ((numSiblings % 2) === 0) {
offset -= ((GAP / 2) * sign);
}
// make reverse links count the same as non-reverse
const reverse = ((theta < 180) ? 1 : -1);
// we found the vertex
const angle = joint.g.toRad(theta + (sign * reverse * 90));
const shift = joint.g.Point.fromPolar(offset * sign, angle, 0);
this.ignoreAdjustVertices = true;
sibling.source(sibling.getSourceCell(), {
anchor: {
name: 'center',
args: {
dx: shift.x,
dy: shift.y
}
}
});
sibling.target(sibling.getTargetCell(), {
anchor: {
name: 'center',
args: {
dx: shift.x,
dy: shift.y
}
}
});
this.ignoreAdjustVertices = false;
});
}
}
}
}
export default JointGraph;

View File

@@ -0,0 +1,89 @@
import 'jquery';
import 'backbone';
import * as joint from 'jointjs/dist/joint.min.js';
import _ from 'lodash';
const jointShapeElement = () => joint.shapes.standard.Rectangle.extend({
defaults: joint.util.deepSupplement({
type: 'html.Element',
markup: [{
tagName: 'rect',
selector: 'body'
}, {
tagName: 'rect',
selector: 'labelBackground'
}, {
tagName: 'rect',
selector: 'labelSeparator'
}, {
tagName: 'rect',
selector: 'inBackground'
}, {
tagName: 'rect',
selector: 'outBackground'
}, {
tagName: 'text',
selector: 'icon'
}, {
tagName: 'text',
selector: 'label'
}, {
tagName: 'image',
selector: 'texture'
}, {
tagName: 'path',
selector: 'marker'
}]
}, joint.shapes.standard.Rectangle.prototype.defaults)
});
const jointShapeElementView = paper => joint.dia.ElementView.extend({
initialize: function () {
_.bindAll(this, 'updateBox');
joint.dia.ElementView.prototype.initialize.apply(this, arguments);
this.div = document.createElement('div');
this.div.setAttribute('id', `nodediv_${this.model.id}`);
this.div.classList.add('graph-node-div');
// // Update the box position whenever the underlying model changes.
this.model.on('change', this.updateBox, this);
paper.on('cell:mousewheel', this.updateBox, this);
paper.on('blank:mousewheel', this.updateBox, this);
paper.on('blank:pointerup', this.updateBox, this);
document.addEventListener('mousemove', (e) => {
this.updateBox();
});
// // Remove the box when the model gets removed from the graph.
this.model.on('remove', this.removeBox, this);
this.updateBox();
},
render: function () {
joint.dia.ElementView.prototype.render.apply(this, arguments);
paper.$el.append(this.div);
this.updateBox();
return this;
},
updateBox: function () {
// Set the position and dimension of the box so that it covers the JointJS element.
const bbox = this.model.getBBox();
// Example of updating the HTML with a data stored in the cell model.
this.div.setAttribute('style', `
position: absolute;
width: ${bbox.width}px;
height: ${bbox.height}px;
left: ${bbox.width / 2 * paper.scale().sx}px;
top: ${bbox.height / 2 * paper.scale().sx}px;
transform: translate(${paper.translate().tx + paper.scale().sx * bbox.x - bbox.width / 2}px, ${paper.translate().ty + paper.scale().sx * bbox.y - (bbox.height / 2)}px) scale(${paper.scale().sx});
`);
},
removeBox: function (evt) {
this.div.remove();
}
});
export {
jointShapeElement,
jointShapeElementView
};

View File

@@ -0,0 +1,379 @@
/*! JointJS v3.4.1 (2021-08-18) - JavaScript diagramming library
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/*
A complete list of SVG properties that can be set through CSS is here:
http://www.w3.org/TR/SVG/styling.html
Important note: Presentation attributes have a lower precedence over CSS style rules.
*/
/* .viewport is a <g> node wrapping all diagram elements in the paper */
.joint-viewport {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.joint-paper > svg,
.joint-paper-background,
.joint-paper-grid {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/*
1. IE can't handle paths without the `d` attribute for bounding box calculation
2. IE can't even handle 'd' attribute as a css selector (e.g path[d]) so the following rule will
break the links rendering.
path:not([d]) {
display: none;
}
*/
/* magnet is an element that can be either source or a target of a link */
[magnet=true]:not(.joint-element) {
cursor: crosshair;
}
[magnet=true]:not(.joint-element):hover {
opacity: .7;
}
/*
Elements have CSS classes named by their types. E.g. type: basic.Rect has a CSS class "element basic Rect".
This makes it possible to easily style elements in CSS and have generic CSS rules applying to
the whole group of elements. Each plugin can provide its own stylesheet.
*/
.joint-element {
/* Give the user a hint that he can drag&drop the element. */
cursor: move;
}
.joint-element * {
user-drag: none;
}
.joint-element .scalable * {
/* The default behavior when scaling an element is not to scale the stroke in order to prevent the ugly effect of stroke with different proportions. */
vector-effect: non-scaling-stroke;
}
/*
connection-wrap is a <path> element of the joint.dia.Link that follows the .connection <path> of that link.
In other words, the `d` attribute of the .connection-wrap contains the same data as the `d` attribute of the
.connection <path>. The advantage of using .connection-wrap is to be able to catch pointer events
in the neighborhood of the .connection <path>. This is especially handy if the .connection <path> is
very thin.
*/
.marker-source,
.marker-target {
/* This makes the arrowheads point to the border of objects even though the transform: scale() is applied on them. */
vector-effect: non-scaling-stroke;
}
/* Paper */
.joint-paper {
position: relative;
}
/* Paper */
/* Highlighting */
.joint-highlight-opacity {
opacity: 0.3;
}
/* Highlighting */
/*
Vertex markers are `<circle>` elements that appear at connection vertex positions.
*/
.joint-link .connection-wrap,
.joint-link .connection {
fill: none;
}
/* <g> element wrapping .marker-vertex-group. */
.marker-vertices {
opacity: 0;
cursor: move;
}
.marker-arrowheads {
opacity: 0;
cursor: move;
cursor: -webkit-grab;
cursor: -moz-grab;
/* display: none; */ /* setting `display: none` on .marker-arrowheads effectively switches of links reconnecting */
}
.link-tools {
opacity: 0;
cursor: pointer;
}
.link-tools .tool-options {
display: none; /* by default, we don't display link options tool */
}
.joint-link:hover .marker-vertices,
.joint-link:hover .marker-arrowheads,
.joint-link:hover .link-tools {
opacity: 1;
}
/* <circle> element used to remove a vertex */
.marker-vertex-remove {
cursor: pointer;
opacity: .1;
}
.marker-vertex-group:hover .marker-vertex-remove {
opacity: 1;
}
.marker-vertex-remove-area {
opacity: .1;
cursor: pointer;
}
.marker-vertex-group:hover .marker-vertex-remove-area {
opacity: 1;
}
/*
Example of custom changes (in pure CSS only!):
Do not show marker vertices at all: .marker-vertices { display: none; }
Do not allow adding new vertices: .connection-wrap { pointer-events: none; }
*/
/* foreignObject inside the elements (i.e joint.shapes.basic.TextBlock) */
.joint-element .fobj {
overflow: hidden;
}
.joint-element .fobj body {
background-color: transparent;
margin: 0px;
position: static;
}
.joint-element .fobj div {
text-align: center;
vertical-align: middle;
display: table-cell;
padding: 0px 5px 0px 5px;
}
/* Paper */
.joint-paper.joint-theme-dark {
background-color: #18191b;
}
/* Paper */
/* Links */
.joint-link.joint-theme-dark .connection-wrap {
stroke: #8F8FF3;
stroke-width: 15;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0;
cursor: move;
}
.joint-link.joint-theme-dark .connection-wrap:hover {
opacity: .4;
stroke-opacity: .4;
}
.joint-link.joint-theme-dark .connection {
stroke-linejoin: round;
}
.joint-link.joint-theme-dark .link-tools .tool-remove circle {
fill: #F33636;
}
.joint-link.joint-theme-dark .link-tools .tool-remove path {
fill: white;
}
.joint-link.joint-theme-dark .link-tools [event="link:options"] circle {
fill: green;
}
/* <circle> element inside .marker-vertex-group <g> element */
.joint-link.joint-theme-dark .marker-vertex {
fill: #5652DB;
}
.joint-link.joint-theme-dark .marker-vertex:hover {
fill: #8E8CE1;
stroke: none;
}
.joint-link.joint-theme-dark .marker-arrowhead {
fill: #5652DB;
}
.joint-link.joint-theme-dark .marker-arrowhead:hover {
fill: #8E8CE1;
stroke: none;
}
/* <circle> element used to remove a vertex */
.joint-link.joint-theme-dark .marker-vertex-remove-area {
fill: green;
stroke: darkgreen;
}
.joint-link.joint-theme-dark .marker-vertex-remove {
fill: white;
stroke: white;
}
/* Links */
/* Paper */
.joint-paper.joint-theme-default {
background-color: #FFFFFF;
}
/* Paper */
/* Links */
.joint-link.joint-theme-default .connection-wrap {
stroke: #000000;
stroke-width: 15;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0;
cursor: move;
}
.joint-link.joint-theme-default .connection-wrap:hover {
opacity: .4;
stroke-opacity: .4;
}
.joint-link.joint-theme-default .connection {
stroke-linejoin: round;
}
.joint-link.joint-theme-default .link-tools .tool-remove circle {
fill: #FF0000;
}
.joint-link.joint-theme-default .link-tools .tool-remove path {
fill: #FFFFFF;
}
/* <circle> element inside .marker-vertex-group <g> element */
.joint-link.joint-theme-default .marker-vertex {
fill: #1ABC9C;
}
.joint-link.joint-theme-default .marker-vertex:hover {
fill: #34495E;
stroke: none;
}
.joint-link.joint-theme-default .marker-arrowhead {
fill: #1ABC9C;
}
.joint-link.joint-theme-default .marker-arrowhead:hover {
fill: #F39C12;
stroke: none;
}
/* <circle> element used to remove a vertex */
.joint-link.joint-theme-default .marker-vertex-remove {
fill: #FFFFFF;
}
/* Links */
.joint-link.joint-theme-material .connection-wrap {
stroke: #000000;
stroke-width: 15;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0;
cursor: move;
}
.joint-link.joint-theme-material .connection-wrap:hover {
opacity: .4;
stroke-opacity: .4;
}
.joint-link.joint-theme-material .connection {
stroke-linejoin: round;
}
.joint-link.joint-theme-material .link-tools .tool-remove circle {
fill: #C64242;
}
.joint-link.joint-theme-material .link-tools .tool-remove path {
fill: #FFFFFF;
}
/* <circle> element inside .marker-vertex-group <g> element */
.joint-link.joint-theme-material .marker-vertex {
fill: #d0d8e8;
}
.joint-link.joint-theme-material .marker-vertex:hover {
fill: #5fa9ee;
stroke: none;
}
.joint-link.joint-theme-material .marker-arrowhead {
fill: #d0d8e8;
}
.joint-link.joint-theme-material .marker-arrowhead:hover {
fill: #5fa9ee;
stroke: none;
}
/* <circle> element used to remove a vertex */
.joint-link.joint-theme-material .marker-vertex-remove-area {
fill: #5fa9ee;
}
.joint-link.joint-theme-material .marker-vertex-remove {
fill: white;
}
/* Links */
/* Links */
.joint-link.joint-theme-modern .connection-wrap {
stroke: #000000;
stroke-width: 15;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0;
cursor: move;
}
.joint-link.joint-theme-modern .connection-wrap:hover {
opacity: .4;
stroke-opacity: .4;
}
.joint-link.joint-theme-modern .connection {
stroke-linejoin: round;
}
.joint-link.joint-theme-modern .link-tools .tool-remove circle {
fill: #FF0000;
}
.joint-link.joint-theme-modern .link-tools .tool-remove path {
fill: #FFFFFF;
}
/* <circle> element inside .marker-vertex-group <g> element */
.joint-link.joint-theme-modern .marker-vertex {
fill: #1ABC9C;
}
.joint-link.joint-theme-modern .marker-vertex:hover {
fill: #34495E;
stroke: none;
}
.joint-link.joint-theme-modern .marker-arrowhead {
fill: #1ABC9C;
}
.joint-link.joint-theme-modern .marker-arrowhead:hover {
fill: #F39C12;
stroke: none;
}
/* <circle> element used to remove a vertex */
.joint-link.joint-theme-modern .marker-vertex-remove {
fill: white;
}
/* Links */

View File

@@ -0,0 +1,170 @@
/*
A complete list of SVG properties that can be set through CSS is here:
http://www.w3.org/TR/SVG/styling.html
Important note: Presentation attributes have a lower precedence over CSS style rules.
*/
/* .viewport is a <g> node wrapping all diagram elements in the paper */
.joint-viewport {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
.joint-paper > svg,
.joint-paper-background,
.joint-paper-grid {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
/*
1. IE can't handle paths without the `d` attribute for bounding box calculation
2. IE can't even handle 'd' attribute as a css selector (e.g path[d]) so the following rule will
break the links rendering.
path:not([d]) {
display: none;
}
*/
/* magnet is an element that can be either source or a target of a link */
[magnet=true]:not(.joint-element) {
cursor: crosshair;
}
[magnet=true]:not(.joint-element):hover {
opacity: .7;
}
/*
Elements have CSS classes named by their types. E.g. type: basic.Rect has a CSS class "element basic Rect".
This makes it possible to easily style elements in CSS and have generic CSS rules applying to
the whole group of elements. Each plugin can provide its own stylesheet.
*/
.joint-element {
/* Give the user a hint that he can drag&drop the element. */
cursor: move;
}
.joint-element * {
user-drag: none;
}
.joint-element .scalable * {
/* The default behavior when scaling an element is not to scale the stroke in order to prevent the ugly effect of stroke with different proportions. */
vector-effect: non-scaling-stroke;
}
/*
connection-wrap is a <path> element of the joint.dia.Link that follows the .connection <path> of that link.
In other words, the `d` attribute of the .connection-wrap contains the same data as the `d` attribute of the
.connection <path>. The advantage of using .connection-wrap is to be able to catch pointer events
in the neighborhood of the .connection <path>. This is especially handy if the .connection <path> is
very thin.
*/
.marker-source,
.marker-target {
/* This makes the arrowheads point to the border of objects even though the transform: scale() is applied on them. */
vector-effect: non-scaling-stroke;
}
/* Paper */
.joint-paper {
position: relative;
}
/* Paper */
/* Highlighting */
.joint-highlight-opacity {
opacity: 0.3;
}
/* Highlighting */
/*
Vertex markers are `<circle>` elements that appear at connection vertex positions.
*/
.joint-link .connection-wrap,
.joint-link .connection {
fill: none;
}
/* <g> element wrapping .marker-vertex-group. */
.marker-vertices {
opacity: 0;
cursor: move;
}
.marker-arrowheads {
opacity: 0;
cursor: move;
cursor: -webkit-grab;
cursor: -moz-grab;
/* display: none; */ /* setting `display: none` on .marker-arrowheads effectively switches of links reconnecting */
}
.link-tools {
opacity: 0;
cursor: pointer;
}
.link-tools .tool-options {
display: none; /* by default, we don't display link options tool */
}
.joint-link:hover .marker-vertices,
.joint-link:hover .marker-arrowheads,
.joint-link:hover .link-tools {
opacity: 1;
}
/* <circle> element used to remove a vertex */
.marker-vertex-remove {
cursor: pointer;
opacity: .1;
}
.marker-vertex-group:hover .marker-vertex-remove {
opacity: 1;
}
.marker-vertex-remove-area {
opacity: .1;
cursor: pointer;
}
.marker-vertex-group:hover .marker-vertex-remove-area {
opacity: 1;
}
/*
Example of custom changes (in pure CSS only!):
Do not show marker vertices at all: .marker-vertices { display: none; }
Do not allow adding new vertices: .connection-wrap { pointer-events: none; }
*/
/* foreignObject inside the elements (i.e joint.shapes.basic.TextBlock) */
.joint-element .fobj {
overflow: hidden;
}
.joint-element .fobj body {
background-color: transparent;
margin: 0px;
position: static;
}
.joint-element .fobj div {
text-align: center;
vertical-align: middle;
display: table-cell;
padding: 0px 5px 0px 5px;
}

View File

@@ -0,0 +1,48 @@
/* Links */
.joint-link.joint-theme-material .connection-wrap {
stroke: #000000;
stroke-width: 15;
stroke-linecap: round;
stroke-linejoin: round;
opacity: 0;
cursor: move;
}
.joint-link.joint-theme-material .connection-wrap:hover {
opacity: .4;
stroke-opacity: .4;
}
.joint-link.joint-theme-material .connection {
stroke-linejoin: round;
}
.joint-link.joint-theme-material .link-tools .tool-remove circle {
fill: #C64242;
}
.joint-link.joint-theme-material .link-tools .tool-remove path {
fill: #FFFFFF;
}
/* <circle> element inside .marker-vertex-group <g> element */
.joint-link.joint-theme-material .marker-vertex {
fill: #d0d8e8;
}
.joint-link.joint-theme-material .marker-vertex:hover {
fill: #5fa9ee;
stroke: none;
}
.joint-link.joint-theme-material .marker-arrowhead {
fill: #d0d8e8;
}
.joint-link.joint-theme-material .marker-arrowhead:hover {
fill: #5fa9ee;
stroke: none;
}
/* <circle> element used to remove a vertex */
.joint-link.joint-theme-material .marker-vertex-remove-area {
fill: #5fa9ee;
}
.joint-link.joint-theme-material .marker-vertex-remove {
fill: white;
}
/* Links */

View File

@@ -0,0 +1,620 @@
// Lib from https://raw.githubusercontent.com/playcanvas/engine/9083d81072c32d5dbb4394a72925e644fddc1c8a/src/math/vec2.js
/**
* A 2-dimensional vector.
*
* @ignore
*/
class Vec2 {
/**
* Create a new Vec2 instance.
*
* @param {number|number[]} [x] - The x value. Defaults to 0. If x is an array of length 2, the
* array will be used to populate all components.
* @param {number} [y] - The y value. Defaults to 0.
* @example
* var v = new pc.Vec2(1, 2);
*/
constructor(x = 0, y = 0) {
if (x.length === 2) {
/**
* The first component of the vector.
*
* @type {number}
*/
this.x = x[0];
/**
* The second component of the vector.
*
* @type {number}
*/
this.y = x[1];
} else {
this.x = x;
this.y = y;
}
}
/**
* Adds a 2-dimensional vector to another in place.
*
* @param {Vec2} rhs - The vector to add to the specified vector.
* @returns {Vec2} Self for chaining.
* @example
* var a = new pc.Vec2(10, 10);
* var b = new pc.Vec2(20, 20);
*
* a.add(b);
*
* // Outputs [30, 30]
* console.log("The result of the addition is: " + a.toString());
*/
add(rhs) {
this.x += rhs.x;
this.y += rhs.y;
return this;
}
/**
* Adds two 2-dimensional vectors together and returns the result.
*
* @param {Vec2} lhs - The first vector operand for the addition.
* @param {Vec2} rhs - The second vector operand for the addition.
* @returns {Vec2} Self for chaining.
* @example
* var a = new pc.Vec2(10, 10);
* var b = new pc.Vec2(20, 20);
* var r = new pc.Vec2();
*
* r.add2(a, b);
* // Outputs [30, 30]
*
* console.log("The result of the addition is: " + r.toString());
*/
add2(lhs, rhs) {
this.x = lhs.x + rhs.x;
this.y = lhs.y + rhs.y;
return this;
}
/**
* Adds a number to each element of a vector.
*
* @param {number} scalar - The number to add.
* @returns {Vec2} Self for chaining.
* @example
* var vec = new pc.Vec2(3, 4);
*
* vec.addScalar(2);
*
* // Outputs [5, 6]
* console.log("The result of the addition is: " + vec.toString());
*/
addScalar(scalar) {
this.x += scalar;
this.y += scalar;
return this;
}
/**
* Returns an identical copy of the specified 2-dimensional vector.
*
* @returns {Vec2} A 2-dimensional vector containing the result of the cloning.
* @example
* var v = new pc.Vec2(10, 20);
* var vclone = v.clone();
* console.log("The result of the cloning is: " + vclone.toString());
*/
clone() {
return new Vec2(this.x, this.y);
}
/**
* Copies the contents of a source 2-dimensional vector to a destination 2-dimensional vector.
*
* @param {Vec2} rhs - A vector to copy to the specified vector.
* @returns {Vec2} Self for chaining.
* @example
* var src = new pc.Vec2(10, 20);
* var dst = new pc.Vec2();
*
* dst.copy(src);
*
* console.log("The two vectors are " + (dst.equals(src) ? "equal" : "different"));
*/
copy(rhs) {
this.x = rhs.x;
this.y = rhs.y;
return this;
}
/**
* Returns the result of a cross product operation performed on the two specified 2-dimensional
* vectors.
*
* @param {Vec2} rhs - The second 2-dimensional vector operand of the cross product.
* @returns {number} The cross product of the two vectors.
* @example
* var right = new pc.Vec2(1, 0);
* var up = new pc.Vec2(0, 1);
* var crossProduct = right.cross(up);
*
* // Prints 1
* console.log("The result of the cross product is: " + crossProduct);
*/
cross(rhs) {
return this.x * rhs.y - this.y * rhs.x;
}
/**
* Returns the distance between the two specified 2-dimensional vectors.
*
* @param {Vec2} rhs - The second 2-dimensional vector to test.
* @returns {number} The distance between the two vectors.
* @example
* var v1 = new pc.Vec2(5, 10);
* var v2 = new pc.Vec2(10, 20);
* var d = v1.distance(v2);
* console.log("The distance between v1 and v2 is: " + d);
*/
distance(rhs) {
const x = this.x - rhs.x;
const y = this.y - rhs.y;
return Math.sqrt(x * x + y * y);
}
/**
* Divides a 2-dimensional vector by another in place.
*
* @param {Vec2} rhs - The vector to divide the specified vector by.
* @returns {Vec2} Self for chaining.
* @example
* var a = new pc.Vec2(4, 9);
* var b = new pc.Vec2(2, 3);
*
* a.div(b);
*
* // Outputs [2, 3]
* console.log("The result of the division is: " + a.toString());
*/
div(rhs) {
this.x /= rhs.x;
this.y /= rhs.y;
return this;
}
/**
* Divides one 2-dimensional vector by another and writes the result to the specified vector.
*
* @param {Vec2} lhs - The dividend vector (the vector being divided).
* @param {Vec2} rhs - The divisor vector (the vector dividing the dividend).
* @returns {Vec2} Self for chaining.
* @example
* var a = new pc.Vec2(4, 9);
* var b = new pc.Vec2(2, 3);
* var r = new pc.Vec2();
*
* r.div2(a, b);
* // Outputs [2, 3]
*
* console.log("The result of the division is: " + r.toString());
*/
div2(lhs, rhs) {
this.x = lhs.x / rhs.x;
this.y = lhs.y / rhs.y;
return this;
}
/**
* Divides each element of a vector by a number.
*
* @param {number} scalar - The number to divide by.
* @returns {Vec2} Self for chaining.
* @example
* var vec = new pc.Vec2(3, 6);
*
* vec.divScalar(3);
*
* // Outputs [1, 2]
* console.log("The result of the division is: " + vec.toString());
*/
divScalar(scalar) {
this.x /= scalar;
this.y /= scalar;
return this;
}
/**
* Returns the result of a dot product operation performed on the two specified 2-dimensional
* vectors.
*
* @param {Vec2} rhs - The second 2-dimensional vector operand of the dot product.
* @returns {number} The result of the dot product operation.
* @example
* var v1 = new pc.Vec2(5, 10);
* var v2 = new pc.Vec2(10, 20);
* var v1dotv2 = v1.dot(v2);
* console.log("The result of the dot product is: " + v1dotv2);
*/
dot(rhs) {
return this.x * rhs.x + this.y * rhs.y;
}
/**
* Reports whether two vectors are equal.
*
* @param {Vec2} rhs - The vector to compare to the specified vector.
* @returns {boolean} True if the vectors are equal and false otherwise.
* @example
* var a = new pc.Vec2(1, 2);
* var b = new pc.Vec2(4, 5);
* console.log("The two vectors are " + (a.equals(b) ? "equal" : "different"));
*/
equals(rhs) {
return this.x === rhs.x && this.y === rhs.y;
}
/**
* Returns the magnitude of the specified 2-dimensional vector.
*
* @returns {number} The magnitude of the specified 2-dimensional vector.
* @example
* var vec = new pc.Vec2(3, 4);
* var len = vec.length();
* // Outputs 5
* console.log("The length of the vector is: " + len);
*/
length() {
return Math.sqrt(this.x * this.x + this.y * this.y);
}
/**
* Returns the magnitude squared of the specified 2-dimensional vector.
*
* @returns {number} The magnitude of the specified 2-dimensional vector.
* @example
* var vec = new pc.Vec2(3, 4);
* var len = vec.lengthSq();
* // Outputs 25
* console.log("The length squared of the vector is: " + len);
*/
lengthSq() {
return this.x * this.x + this.y * this.y;
}
/**
* Returns the result of a linear interpolation between two specified 2-dimensional vectors.
*
* @param {Vec2} lhs - The 2-dimensional to interpolate from.
* @param {Vec2} rhs - The 2-dimensional to interpolate to.
* @param {number} alpha - The value controlling the point of interpolation. Between 0 and 1,
* the linear interpolant will occur on a straight line between lhs and rhs. Outside of this
* range, the linear interpolant will occur on a ray extrapolated from this line.
* @returns {Vec2} Self for chaining.
* @example
* var a = new pc.Vec2(0, 0);
* var b = new pc.Vec2(10, 10);
* var r = new pc.Vec2();
*
* r.lerp(a, b, 0); // r is equal to a
* r.lerp(a, b, 0.5); // r is 5, 5
* r.lerp(a, b, 1); // r is equal to b
*/
lerp(lhs, rhs, alpha) {
this.x = lhs.x + alpha * (rhs.x - lhs.x);
this.y = lhs.y + alpha * (rhs.y - lhs.y);
return this;
}
/**
* Multiplies a 2-dimensional vector to another in place.
*
* @param {Vec2} rhs - The 2-dimensional vector used as the second multiplicand of the operation.
* @returns {Vec2} Self for chaining.
* @example
* var a = new pc.Vec2(2, 3);
* var b = new pc.Vec2(4, 5);
*
* a.mul(b);
*
* // Outputs 8, 15
* console.log("The result of the multiplication is: " + a.toString());
*/
mul(rhs) {
this.x *= rhs.x;
this.y *= rhs.y;
return this;
}
/**
* Returns the result of multiplying the specified 2-dimensional vectors together.
*
* @param {Vec2} lhs - The 2-dimensional vector used as the first multiplicand of the operation.
* @param {Vec2} rhs - The 2-dimensional vector used as the second multiplicand of the operation.
* @returns {Vec2} Self for chaining.
* @example
* var a = new pc.Vec2(2, 3);
* var b = new pc.Vec2(4, 5);
* var r = new pc.Vec2();
*
* r.mul2(a, b);
*
* // Outputs 8, 15
* console.log("The result of the multiplication is: " + r.toString());
*/
mul2(lhs, rhs) {
this.x = lhs.x * rhs.x;
this.y = lhs.y * rhs.y;
return this;
}
/**
* Multiplies each element of a vector by a number.
*
* @param {number} scalar - The number to multiply by.
* @returns {Vec2} Self for chaining.
* @example
* var vec = new pc.Vec2(3, 6);
*
* vec.mulScalar(3);
*
* // Outputs [9, 18]
* console.log("The result of the multiplication is: " + vec.toString());
*/
mulScalar(scalar) {
this.x *= scalar;
this.y *= scalar;
return this;
}
/**
* Returns this 2-dimensional vector converted to a unit vector in place. If the vector has a
* length of zero, the vector's elements will be set to zero.
*
* @returns {Vec2} Self for chaining.
* @example
* var v = new pc.Vec2(25, 0);
*
* v.normalize();
*
* // Outputs 1, 0
* console.log("The result of the vector normalization is: " + v.toString());
*/
normalize() {
const lengthSq = this.x * this.x + this.y * this.y;
if (lengthSq > 0) {
const invLength = 1 / Math.sqrt(lengthSq);
this.x *= invLength;
this.y *= invLength;
}
return this;
}
/**
* Each element is set to the largest integer less than or equal to its value.
*
* @returns {Vec2} Self for chaining.
*/
floor() {
this.x = Math.floor(this.x);
this.y = Math.floor(this.y);
return this;
}
/**
* Each element is rounded up to the next largest integer.
*
* @returns {Vec2} Self for chaining.
*/
ceil() {
this.x = Math.ceil(this.x);
this.y = Math.ceil(this.y);
return this;
}
/**
* Each element is rounded up or down to the nearest integer.
*
* @returns {Vec2} Self for chaining.
*/
round() {
this.x = Math.round(this.x);
this.y = Math.round(this.y);
return this;
}
/**
* Each element is assigned a value from rhs parameter if it is smaller.
*
* @param {Vec2} rhs - The 2-dimensional vector used as the source of elements to compare to.
* @returns {Vec2} Self for chaining.
*/
min(rhs) {
if (rhs.x < this.x) this.x = rhs.x;
if (rhs.y < this.y) this.y = rhs.y;
return this;
}
/**
* Each element is assigned a value from rhs parameter if it is larger.
*
* @param {Vec2} rhs - The 2-dimensional vector used as the source of elements to compare to.
* @returns {Vec2} Self for chaining.
*/
max(rhs) {
if (rhs.x > this.x) this.x = rhs.x;
if (rhs.y > this.y) this.y = rhs.y;
return this;
}
/**
* Sets the specified 2-dimensional vector to the supplied numerical values.
*
* @param {number} x - The value to set on the first component of the vector.
* @param {number} y - The value to set on the second component of the vector.
* @returns {Vec2} Self for chaining.
* @example
* var v = new pc.Vec2();
* v.set(5, 10);
*
* // Outputs 5, 10
* console.log("The result of the vector set is: " + v.toString());
*/
set(x, y) {
this.x = x;
this.y = y;
return this;
}
/**
* Subtracts a 2-dimensional vector from another in place.
*
* @param {Vec2} rhs - The vector to add to the specified vector.
* @returns {Vec2} Self for chaining.
* @example
* var a = new pc.Vec2(10, 10);
* var b = new pc.Vec2(20, 20);
*
* a.sub(b);
*
* // Outputs [-10, -10]
* console.log("The result of the subtraction is: " + a.toString());
*/
sub(rhs) {
this.x -= rhs.x;
this.y -= rhs.y;
return this;
}
/**
* Subtracts two 2-dimensional vectors from one another and returns the result.
*
* @param {Vec2} lhs - The first vector operand for the addition.
* @param {Vec2} rhs - The second vector operand for the addition.
* @returns {Vec2} Self for chaining.
* @example
* var a = new pc.Vec2(10, 10);
* var b = new pc.Vec2(20, 20);
* var r = new pc.Vec2();
*
* r.sub2(a, b);
*
* // Outputs [-10, -10]
* console.log("The result of the subtraction is: " + r.toString());
*/
sub2(lhs, rhs) {
this.x = lhs.x - rhs.x;
this.y = lhs.y - rhs.y;
return this;
}
/**
* Subtracts a number from each element of a vector.
*
* @param {number} scalar - The number to subtract.
* @returns {Vec2} Self for chaining.
* @example
* var vec = new pc.Vec2(3, 4);
*
* vec.subScalar(2);
*
* // Outputs [1, 2]
* console.log("The result of the subtraction is: " + vec.toString());
*/
subScalar(scalar) {
this.x -= scalar;
this.y -= scalar;
return this;
}
/**
* Converts the vector to string form.
*
* @returns {string} The vector in string form.
* @example
* var v = new pc.Vec2(20, 10);
* // Outputs [20, 10]
* console.log(v.toString());
*/
toString() {
return `[${this.x}, ${this.y}]`;
}
/**
* Calculates the angle between two Vec2's in radians.
*
* @param {Vec2} lhs - The first vector operand for the calculation.
* @param {Vec2} rhs - The second vector operand for the calculation.
* @returns {number} The calculated angle in radians.
* @ignore
*/
static angleRad(lhs, rhs) {
return Math.atan2(lhs.x * rhs.y - lhs.y * rhs.x, lhs.x * rhs.x + lhs.y * rhs.y);
}
/**
* A constant vector set to [0, 0].
*
* @type {Vec2}
* @readonly
*/
static ZERO = Object.freeze(new Vec2(0, 0));
/**
* A constant vector set to [1, 1].
*
* @type {Vec2}
* @readonly
*/
static ONE = Object.freeze(new Vec2(1, 1));
/**
* A constant vector set to [0, 1].
*
* @type {Vec2}
* @readonly
*/
static UP = Object.freeze(new Vec2(0, 1));
/**
* A constant vector set to [0, -1].
*
* @type {Vec2}
* @readonly
*/
static DOWN = Object.freeze(new Vec2(0, -1));
/**
* A constant vector set to [1, 0].
*
* @type {Vec2}
* @readonly
*/
static RIGHT = Object.freeze(new Vec2(1, 0));
/**
* A constant vector set to [-1, 0].
*
* @type {Vec2}
* @readonly
*/
static LEFT = Object.freeze(new Vec2(-1, 0));
}
export { Vec2 };

View File

@@ -0,0 +1,44 @@
class SelectedItem {
constructor(graph, type, id, edgeId) {
this._graph = graph;
this._type = type;
this._id = id;
this._edgeId = edgeId;
}
get type() {
return this._type;
}
get id() {
return this._id;
}
get edgeId() {
return this._edgeId;
}
selectItem() {
switch (this._type) {
case 'NODE':
this._graph.view.selectNode(this._id);
break;
case 'EDGE':
this._graph.view.selectEdge(this._id);
break;
}
}
deselectItem() {
switch (this._type) {
case 'NODE':
this._graph.view.deselectNode(this._id);
break;
case 'EDGE':
this._graph.view.deselectEdge(this._id);
break;
}
}
}
export default SelectedItem;

View File

@@ -0,0 +1 @@
import './style.scss';

View File

@@ -0,0 +1,147 @@
@import '../lib/joint';
@import '../lib/layout';
@import '../lib/material';
.pcui-graph {
font-family: 'Helvetica Neue', Arial, Helvetica, sans-serif;
width: 100%;
height: 100%;
.available-magnet {
fill: greenyellow;
}
#paper-html-elements {
position: relative;
border: 1px solid gray;
display: inline-block;
background: transparent;
overflow: hidden;
}
#paper-html-elements svg {
background: transparent;
}
#paper-html-elements svg .link {
z-index: 2;
}
.html-element {
position: absolute;
background: #3498db;
/* Make sure events are propagated to the JointJS element so, e.g. dragging works. */
pointer-events: none;
user-select: none;
border-radius: 4px;
border: 2px solid #2980b9;
box-shadow: inset 0 0 5px black, 2px 2px 1px gray;
padding: 5px;
box-sizing: border-box;
z-index: 2;
}
.html-element select,
.html-element input,
.html-element button {
/* Enable interacting with inputs only. */
pointer-events: auto;
}
.html-element button.delete {
color: white;
border: none;
background-color: #c0392b;
border-radius: 20px;
width: 15px;
height: 15px;
line-height: 15px;
text-align: middle;
position: absolute;
top: -15px;
left: -15px;
padding: 0;
margin: 0;
font-weight: bold;
cursor: pointer;
}
.html-element button.delete:hover {
width: 20px;
height: 20px;
line-height: 20px;
}
.html-element select {
position: absolute;
right: 2px;
bottom: 28px;
}
.html-element input {
position: absolute;
bottom: 0;
left: 0;
right: 0;
border: none;
color: #333;
padding: 5px;
height: 16px;
}
.html-element label {
color: #333;
text-shadow: 1px 0 0 lightgray;
font-weight: bold;
}
.html-element span {
position: absolute;
top: 2px;
right: 9px;
color: white;
font-size: 10px;
}
.graph-node-div {
pointer-events: none;
}
.graph-node-input {
pointer-events: all;
}
.graph-node-input-no-pointer-events {
pointer-events: none;
}
.graph-node-container {
margin-top: 5px;
margin-bottom: 5px;
height: 28px;
display: flex;
align-items: center;
pointer-events: inherit;
}
.graph-node-container:first-child {
// margin-top: 33px !important;
}
.graph-node-label {
max-width: 50px;
min-width: 50px;
font-size: 12px;
margin-left: 13px;
}
.port-inner-body {
pointer-events: none;
}
.pcui-contextmenu-parent,
.pcui-contextmenu-child {
height: 27px;
}
}

View File

@@ -0,0 +1,19 @@
export const deepCopyFunction = (inObject) => {
let value, key;
if (typeof inObject !== 'object' || inObject === null) {
return inObject; // Return the value if inObject is not an object
}
// Create an array or object to hold the values
const outObject = Array.isArray(inObject) ? [] : {};
for (key in inObject) {
value = inObject[key];
// Recursively (deep) copy for nested objects, including arrays
outObject[key] = deepCopyFunction(value);
}
return outObject;
};