more generics

This commit is contained in:
Dylan Vorster
2019-07-26 13:37:30 +02:00
parent 01ffbce479
commit fddec93370
12 changed files with 201 additions and 108 deletions

View File

@@ -1,14 +1,32 @@
import { Toolkit } from '../Toolkit';
import { Toolkit } from "../Toolkit";
export interface BaseEvent {
firing: boolean;
stopPropagation: () => any;
}
export interface BaseEventProxy extends BaseEvent {
function: string;
}
/**
* Listeners are always in the form of an object that contains methods that take events
*/
export type BaseListener = { [key: string]: (event: BaseEvent) => any };
export type BaseListener = {
/**
* Generic event that fires before a specific event was fired
*/
eventWillFire?: (event: BaseEvent & { function: string }) => void;
/**
* Generic event that fires after a specific event was fired (even if it was consumed)
*/
eventDidFire?: (event: BaseEvent & { function: string }) => void;
/**
* Type for other events that will fire
*/
[key: string]: (event: BaseEvent) => any
};
/**
* Base observer pattern class for working with listeners
@@ -20,6 +38,19 @@ export class BaseObserver<L extends BaseListener = BaseListener> {
this.listeners = {};
}
private fireEventInternal(fire: boolean, k: keyof L, event: BaseEvent, ) {
this.iterateListeners(listener => {
// returning false here will instruct itteration to stop
if (!fire && !event.firing) {
return false;
}
// fire selected listener
if (listener[k]) {
listener[k](event as BaseEvent);
}
});
}
fireEvent(event: Partial<BaseEvent> & object, k: keyof L) {
event = {
firing: true,
@@ -29,15 +60,20 @@ export class BaseObserver<L extends BaseListener = BaseListener> {
...event
};
this.iterateListeners(listener => {
// returning false here will instruct itteration to stop
if (!event.firing) {
return false;
}
if (listener[k]) {
listener[k](event as BaseEvent);
}
});
// fire pre
this.fireEventInternal( true, "eventWillFire", {
...event,
function: k
} as BaseEventProxy);
// fire main event
this.fireEventInternal( false, k, event as BaseEvent);
// fire post
this.fireEventInternal( true, "eventDidFire", {
...event,
function: k
} as BaseEventProxy);
}
iterateListeners(cb: (listener: L) => any) {

View File

@@ -1,10 +1,10 @@
import { BaseModel, BaseModelGenerics, BaseModelListener } from "../core-models/BaseModel";
import { PortModel } from './PortModel';
import { BaseModelListener } from "../core-models/BaseModel";
import * as _ from 'lodash';
import { DiagramEngine } from '../DiagramEngine';
import { BaseEntityEvent } from '../core-models/BaseEntity';
import { BasePositionModel, BasePositionModelGenerics } from "../core-models/BasePositionModel";
import { DiagramModel } from "./DiagramModel";
import { PortModel } from "./PortModel";
export interface NodeModelListener extends BaseModelListener {
positionChanged?(event: BaseEntityEvent<NodeModel>): void;
@@ -13,11 +13,12 @@ export interface NodeModelListener extends BaseModelListener {
export interface NodeModelGenerics extends BasePositionModelGenerics{
LISTENER: NodeModelListener;
PARENT: DiagramModel;
PORT: PortModel;
}
export class NodeModel<G extends NodeModelGenerics = NodeModelGenerics> extends BasePositionModel<G> {
ports: { [s: string]: PortModel };
ports: { [s: string]: G['PORT'] };
// calculated post rendering so routing can be done correctly
width: number;
@@ -100,7 +101,7 @@ export class NodeModel<G extends NodeModelGenerics = NodeModelGenerics> extends
});
}
getPortFromID(id): PortModel | null {
getPortFromID(id): G['PORT'] | null {
for (var i in this.ports) {
if (this.ports[i].getID() === id) {
return this.ports[i];
@@ -109,15 +110,15 @@ export class NodeModel<G extends NodeModelGenerics = NodeModelGenerics> extends
return null;
}
getPort(name: string): PortModel | null {
getPort(name: string): G['PORT'] | null {
return this.ports[name];
}
getPorts(): { [s: string]: PortModel } {
getPorts(): { [s: string]: G['PORT'] } {
return this.ports;
}
removePort(port: PortModel) {
removePort(port: G['PORT']) {
//clear the parent node reference
if (this.ports[port.getName()]) {
this.ports[port.getName()].setParent(null);
@@ -125,7 +126,7 @@ export class NodeModel<G extends NodeModelGenerics = NodeModelGenerics> extends
}
}
addPort<T extends PortModel>(port: T): T {
addPort<T extends G['PORT']>(port: T): T {
port.setParent(this);
this.ports[port.getName()] = port;
return port;

View File

@@ -1,26 +1,41 @@
import { DiagramEngine, LabelModel } from '@projectstorm/react-diagrams-core';
import {
BaseModelGenerics,
BaseModelOptions,
DiagramEngine,
LabelModel,
} from "@projectstorm/react-diagrams-core";
export class DefaultLabelModel extends LabelModel {
label: string;
export interface DefaultLabelModelOptions extends Omit<BaseModelOptions, 'type'>{
label?: string;
}
constructor() {
super('default');
export interface DefaultLabelModelGenerics{
OPTIONS: DefaultLabelModelOptions;
}
export class DefaultLabelModel extends LabelModel<BaseModelGenerics & DefaultLabelModelGenerics> {
constructor(options: DefaultLabelModelOptions = {}) {
super({
...options,
type: 'default'
});
this.offsetY = -23;
}
setLabel(label: string) {
this.label = label;
this.options.label = label;
}
deSerialize(ob, engine: DiagramEngine) {
super.deSerialize(ob, engine);
this.label = ob.label;
this.options.label = ob.label;
}
serialize() {
return {
...super.serialize(),
label: this.label
label: this.options.label
};
}
}

View File

@@ -12,6 +12,6 @@ export class DefaultLabelWidget extends BaseWidget<DefaultLabelWidgetProps> {
}
render() {
return <div {...this.getProps()}>{this.props.model.label}</div>;
return <div {...this.getProps()}>{this.props.model.getOptions().label}</div>;
}
}

View File

@@ -2,9 +2,7 @@ import * as React from 'react';
import { AbstractReactFactory } from '@projectstorm/react-diagrams-core';
import { DefaultNodeModel } from './DefaultNodeModel';
import { DefaultNodeWidget } from './DefaultNodeWidget';
/**
* @author Dylan Vorster
*/
export class DefaultNodeFactory extends AbstractReactFactory<DefaultNodeModel> {
constructor() {
super('default');

View File

@@ -1,48 +1,71 @@
import * as _ from 'lodash';
import { DiagramEngine, NodeModel, NodeModelListener, Toolkit } from '@projectstorm/react-diagrams-core';
import {
BaseModelOptions,
DiagramEngine,
NodeModel,
NodeModelGenerics,
} from "@projectstorm/react-diagrams-core";
import { DefaultPortModel } from '../port/DefaultPortModel';
export class DefaultNodeModel extends NodeModel<NodeModelListener> {
name: string;
color: string;
ports: { [s: string]: DefaultPortModel };
export interface DefaultNodeModelOptions extends Omit<BaseModelOptions, 'type'>{
name?: string;
color?: string;
}
constructor(name: string = 'Untitled', color: string = 'rgb(0,192,255)') {
super('default');
this.name = name;
this.color = color;
export interface DefaultNodeModelGenerics{
PORT: DefaultPortModel;
OPTIONS: DefaultNodeModelOptions;
}
export class DefaultNodeModel extends NodeModel<DefaultNodeModelGenerics & NodeModelGenerics> {
constructor(options: DefaultNodeModelOptions = {}) {
super({
type: 'default',
name: 'Untitled',
color: 'rgb(0,192,255)',
...options
});
}
addInPort(label: string): DefaultPortModel {
return this.addPort(new DefaultPortModel(true, Toolkit.UID(), label));
return this.addPort(new DefaultPortModel({
in: true,
name: label,
label: label
}));
}
addOutPort(label: string): DefaultPortModel {
return this.addPort(new DefaultPortModel(false, Toolkit.UID(), label));
return this.addPort(new DefaultPortModel({
in: false,
name: label,
label: label
}));
}
deSerialize(object, engine: DiagramEngine) {
super.deSerialize(object, engine);
this.name = object.name;
this.color = object.color;
this.options.name = object.name;
this.options.color = object.color;
}
serialize() {
return _.merge(super.serialize(), {
name: this.name,
color: this.color
name: this.options.name,
color: this.options.color
});
}
getInPorts(): DefaultPortModel[] {
return _.filter(this.ports, portModel => {
return portModel.in;
return portModel.getOptions().in;
});
}
getOutPorts(): DefaultPortModel[] {
return _.filter(this.ports, portModel => {
return !portModel.in;
return !portModel.getOptions().in;
});
}
}

View File

@@ -49,15 +49,34 @@ export interface DefaultNodeProps extends BaseWidgetProps {
* for both all the input ports on the left, and the output ports on the right.
*/
export class DefaultNodeWidget extends BaseWidget<DefaultNodeProps> {
listener: any;
generatePort = port => {
return <DefaultPortLabel model={port} key={port.id} />;
};
componentWillUnmount(): void {
// release repaint listener
if(this.listener){
this.listener();
}
}
componentDidMount(): void {
this.listener = this.props.node.registerListener({
eventDidFire: () => {
this.forceUpdate()
}
})
}
render() {
return (
<S.Node background={this.props.node.color}>
<S.Node background={this.props.node.getOptions().color}>
<S.Title>
<S.TitleName>{this.props.node.name}</S.TitleName>
<S.TitleName>{this.props.node.getOptions().name}</S.TitleName>
</S.Title>
<S.Ports>
<S.PortsContainer>{_.map(this.props.node.getInPorts(), this.generatePort)}</S.PortsContainer>

View File

@@ -7,6 +7,7 @@ import {
PortModelOptions
} from "@projectstorm/react-diagrams-core";
import { DefaultLinkModel } from '../link/DefaultLinkModel';
import { DefaultNodeModel } from "../node/DefaultNodeModel";
export interface DefaultPortModelOptions extends Omit<PortModelOptions, 'type'>{
label?: string;
@@ -15,7 +16,7 @@ export interface DefaultPortModelOptions extends Omit<PortModelOptions, 'type'>{
export interface DefaultPortModelGenerics{
OPTIONS: DefaultPortModelOptions;
PARENT: DefaultLinkModel;
PARENT: DefaultNodeModel;
}
export class DefaultPortModel extends PortModel<PortModelGenerics & DefaultPortModelGenerics> {

View File

@@ -1,5 +1,6 @@
import * as PF from 'pathfinding';
import { PathFindingLinkFactory } from '../link/PathFindingLinkFactory';
import { PointModel } from "@projectstorm/react-diagrams-core";
/*
it can be very expensive to calculate routes when every single pixel on the canvas
@@ -26,23 +27,17 @@ export default class PathFinding {
* finds a direct path from point A to B.
*/
calculateDirectPath(
from: {
x: number;
y: number;
},
to: {
x: number;
y: number;
}
from: PointModel,
to: PointModel
): number[][] {
const matrix = this.factory.getCanvasMatrix();
const grid = new PF.Grid(matrix);
return pathFinderInstance.findPath(
this.factory.translateRoutingX(Math.floor(from.x / this.factory.ROUTING_SCALING_FACTOR)),
this.factory.translateRoutingY(Math.floor(from.y / this.factory.ROUTING_SCALING_FACTOR)),
this.factory.translateRoutingX(Math.floor(to.x / this.factory.ROUTING_SCALING_FACTOR)),
this.factory.translateRoutingY(Math.floor(to.y / this.factory.ROUTING_SCALING_FACTOR)),
this.factory.translateRoutingX(Math.floor(from.getX() / this.factory.ROUTING_SCALING_FACTOR)),
this.factory.translateRoutingY(Math.floor(from.getY() / this.factory.ROUTING_SCALING_FACTOR)),
this.factory.translateRoutingX(Math.floor(to.getX() / this.factory.ROUTING_SCALING_FACTOR)),
this.factory.translateRoutingY(Math.floor(to.getY() / this.factory.ROUTING_SCALING_FACTOR)),
grid
);
}

View File

@@ -125,26 +125,26 @@ export class PathFindingLinkFactory extends AbstractReactFactory<PathFindingLink
vAdjustmentFactor: number;
} => {
const allNodesCoords = _.values(this.engine.diagramModel.getNodes()).map(item => ({
x: item.x,
x: item.getX(),
width: item.width,
y: item.y,
y: item.getY(),
height: item.height
}));
const allLinks = _.values(this.engine.diagramModel.getLinks());
const allPortsCoords = _.flatMap(allLinks.map(link => [link.sourcePort, link.targetPort]))
const allPortsCoords = _.flatMap(allLinks.map(link => [link.getSourcePort(), link.getTargetPort()]))
.filter(port => port !== null)
.map(item => ({
x: item.x,
x: item.getX(),
width: item.width,
y: item.y,
y: item.getY(),
height: item.height
}));
const allPointsCoords = _.flatMap(allLinks.map(link => link.points)).map(item => ({
const allPointsCoords = _.flatMap(allLinks.map(link => link.getPoints())).map(item => ({
// points don't have width/height, so let's just use 0
x: item.x,
x: item.getX(),
width: 0,
y: item.y,
y: item.getY(),
height: 0
}));
@@ -181,10 +181,10 @@ export class PathFindingLinkFactory extends AbstractReactFactory<PathFindingLink
*/
markNodes = (matrix: number[][]): void => {
_.values(this.engine.diagramModel.getNodes()).forEach(node => {
const startX = Math.floor(node.x / this.ROUTING_SCALING_FACTOR);
const endX = Math.ceil((node.x + node.width) / this.ROUTING_SCALING_FACTOR);
const startY = Math.floor(node.y / this.ROUTING_SCALING_FACTOR);
const endY = Math.ceil((node.y + node.height) / this.ROUTING_SCALING_FACTOR);
const startX = Math.floor(node.getX() / this.ROUTING_SCALING_FACTOR);
const endX = Math.ceil((node.getX() + node.width) / this.ROUTING_SCALING_FACTOR);
const startY = Math.floor(node.getY() / this.ROUTING_SCALING_FACTOR);
const endY = Math.ceil((node.getY() + node.height) / this.ROUTING_SCALING_FACTOR);
for (let x = startX - 1; x <= endX + 1; x++) {
for (let y = startY - 1; y < endY + 1; y++) {
@@ -199,7 +199,7 @@ export class PathFindingLinkFactory extends AbstractReactFactory<PathFindingLink
*/
markPorts = (matrix: number[][]): void => {
const allElements = _.flatMap(
_.values(this.engine.diagramModel.getLinks()).map(link => [].concat(link.sourcePort, link.targetPort))
_.values(this.engine.diagramModel.getLinks()).map(link => [].concat(link.getSourcePort(), link.getTargetPort()))
);
allElements
.filter(port => port !== null)

View File

@@ -3,6 +3,8 @@ import { LinkModel } from '@projectstorm/react-diagrams-core';
export class PathFindingLinkModel extends LinkModel {
constructor() {
super(PathFindingLinkFactory.NAME);
super({
type: PathFindingLinkFactory.NAME
});
}
}

View File

@@ -1,9 +1,9 @@
import * as React from 'react';
import * as _ from 'lodash';
import { BaseWidget, BaseWidgetProps, DiagramEngine, LabelModel, PointModel } from '@projectstorm/react-diagrams-core';
import PathFinding from '../engine/PathFinding';
import { PathFindingLinkFactory } from './PathFindingLinkFactory';
import { PathFindingLinkModel } from './PathFindingLinkModel';
import * as React from "react";
import * as _ from "lodash";
import { BaseWidget, BaseWidgetProps, DiagramEngine, LabelModel, PointModel } from "@projectstorm/react-diagrams-core";
import PathFinding from "../engine/PathFinding";
import { PathFindingLinkFactory } from "./PathFindingLinkFactory";
import { PathFindingLinkModel } from "./PathFindingLinkModel";
export interface PathFindingLinkWidgetProps extends BaseWidgetProps {
color?: string;
@@ -21,7 +21,7 @@ export interface PathFindingLinkWidgetState {
export class PathFindingLinkWidget extends BaseWidget<PathFindingLinkWidgetProps, PathFindingLinkWidgetState> {
public static defaultProps: PathFindingLinkWidgetProps = {
color: 'black',
color: "black",
width: 3,
link: null,
engine: null,
@@ -37,7 +37,7 @@ export class PathFindingLinkWidget extends BaseWidget<PathFindingLinkWidgetProps
pathFinding: PathFinding; // only set when smart routing is active
constructor(props: PathFindingLinkWidgetProps) {
super('srd-default-link', props);
super("srd-default-link", props);
this.refLabels = {};
this.refPaths = [];
@@ -48,37 +48,37 @@ export class PathFindingLinkWidget extends BaseWidget<PathFindingLinkWidgetProps
}
calculateAllLabelPosition() {
_.forEach(this.props.link.labels, (label, index) => {
_.forEach(this.props.link.getLabels(), (label, index) => {
this.calculateLabelPosition(label, index + 1);
});
}
componentDidUpdate() {
if (this.props.link.labels.length > 0) {
if (this.props.link.getLabels().length > 0) {
window.requestAnimationFrame(this.calculateAllLabelPosition.bind(this));
}
}
componentDidMount() {
if (this.props.link.labels.length > 0) {
if (this.props.link.getLabels().length > 0) {
window.requestAnimationFrame(this.calculateAllLabelPosition.bind(this));
}
}
generatePoint(pointIndex: number): JSX.Element {
let x = this.props.link.points[pointIndex].x;
let y = this.props.link.points[pointIndex].y;
let x = this.props.link.getPoints()[pointIndex].getX();
let y = this.props.link.getPoints()[pointIndex].getY();
return (
<g key={'point-' + this.props.link.points[pointIndex].getID()}>
<g key={"point-" + this.props.link.getPoints()[pointIndex].getID()}>
<circle
cx={x}
cy={y}
r={5}
className={
'point ' +
this.bem('__point') +
(this.props.link.points[pointIndex].isSelected() ? this.bem('--point-selected') : '')
"point " +
this.bem("__point") +
(this.props.link.getPoints()[pointIndex].isSelected() ? this.bem("--point-selected") : "")
}
/>
<circle
@@ -88,13 +88,13 @@ export class PathFindingLinkWidget extends BaseWidget<PathFindingLinkWidgetProps
onMouseEnter={() => {
this.setState({ selected: true });
}}
data-id={this.props.link.points[pointIndex].getID()}
data-id={this.props.link.getPoints()[pointIndex].getID()}
data-linkid={this.props.link.getID()}
cx={x}
cy={y}
r={15}
opacity={0}
className={'point ' + this.bem('__point')}
className={"point " + this.bem("__point")}
/>
</g>
);
@@ -104,9 +104,12 @@ export class PathFindingLinkWidget extends BaseWidget<PathFindingLinkWidgetProps
if (
!event.shiftKey &&
!this.props.diagramEngine.isModelLocked(this.props.link) &&
this.props.link.points.length - 1 <= this.props.diagramEngine.getMaxNumberPointsPerLink()
this.props.link.getPoints().length - 1 <= this.props.diagramEngine.getMaxNumberPointsPerLink()
) {
const point = new PointModel(this.props.link, this.props.diagramEngine.getRelativeMousePoint(event));
const point = new PointModel({
link: this.props.link,
points: this.props.diagramEngine.getRelativeMousePoint(event)
});
point.setSelected(true);
this.forceUpdate();
this.props.link.addPoint(point, index);
@@ -119,7 +122,7 @@ export class PathFindingLinkWidget extends BaseWidget<PathFindingLinkWidgetProps
return (
<foreignObject
key={label.getID()}
className={this.bem('__label')}
className={this.bem("__label")}
width={canvas.offsetWidth}
height={canvas.offsetHeight}>
<div ref={ref => (this.refLabels[label.getID()] = ref)}>
@@ -132,7 +135,7 @@ export class PathFindingLinkWidget extends BaseWidget<PathFindingLinkWidgetProps
generateLink(path: string, extraProps: any, id: string | number): JSX.Element {
let Bottom = (
<path
className={this.state.selected ? this.bem('--path-selected') : ''}
className={this.state.selected ? this.bem("--path-selected") : ""}
strokeWidth={this.props.width}
stroke={this.props.color}
ref={ref => ref && this.refPaths.push(ref)}
@@ -142,7 +145,7 @@ export class PathFindingLinkWidget extends BaseWidget<PathFindingLinkWidgetProps
var Top = React.cloneElement(Bottom, {
...extraProps,
strokeLinecap: 'round',
strokeLinecap: "round",
onMouseLeave: () => {
this.setState({ selected: false });
},
@@ -150,7 +153,7 @@ export class PathFindingLinkWidget extends BaseWidget<PathFindingLinkWidgetProps
this.setState({ selected: true });
},
ref: null,
'data-linkid': this.props.link.getID(),
"data-linkid": this.props.link.getID(),
strokeOpacity: this.state.selected ? 0.1 : 0,
strokeWidth: 20,
onContextMenu: () => {
@@ -162,7 +165,7 @@ export class PathFindingLinkWidget extends BaseWidget<PathFindingLinkWidgetProps
});
return (
<g key={'link-' + id}>
<g key={"link-" + id}>
{Bottom}
{Top}
</g>
@@ -176,7 +179,7 @@ export class PathFindingLinkWidget extends BaseWidget<PathFindingLinkWidgetProps
// calculate the point where we want to display the label
let labelPosition =
lengths.reduce((previousValue, currentValue) => previousValue + currentValue, 0) *
(index / (this.props.link.labels.length + 1));
(index / (this.props.link.getLabels().length + 1));
// find the path where the label will be rendered and calculate the relative position
let pathIndex = 0;
@@ -214,7 +217,7 @@ export class PathFindingLinkWidget extends BaseWidget<PathFindingLinkWidgetProps
y: pathCentre.y - labelDimensions.height / 2 + label.offsetY
};
this.refLabels[label.id].setAttribute(
'style',
"style",
`transform: translate(${labelCoordinates.x}px, ${labelCoordinates.y}px);`
);
};
@@ -226,7 +229,7 @@ export class PathFindingLinkWidget extends BaseWidget<PathFindingLinkWidgetProps
}
//ensure id is present for all points on the path
var points = this.props.link.points;
var points = this.props.link.getPoints();
var paths = [];
// first step: calculate a direct path between the points being linked
@@ -252,7 +255,7 @@ export class PathFindingLinkWidget extends BaseWidget<PathFindingLinkWidgetProps
this.addPointToLink(event, 1);
}
},
'0'
"0"
)
);
}
@@ -261,7 +264,7 @@ export class PathFindingLinkWidget extends BaseWidget<PathFindingLinkWidgetProps
return (
<g {...this.getProps()}>
{paths}
{_.map(this.props.link.labels, labelModel => {
{_.map(this.props.link.getLabels(), labelModel => {
return this.generateLabel(labelModel);
})}
</g>