UNPKG

rete-angular-jellytech-plugin

Version:

Angular Jellytech ==== Plugin for displaying SCADA and overview elements

937 lines (919 loc) 96.7 kB
import { getUID, ClassicPreset, Scope } from 'rete'; import { getDOMSocketPosition, loopConnectionPath, classicConnectionPath } from 'rete-render-utils'; import * as i0 from '@angular/core'; import { Directive, Input, Component, HostBinding, HostListener, EventEmitter, Output, NgModule } from '@angular/core'; import * as i1 from '@angular/common'; import { CommonModule } from '@angular/common'; import { BaseAreaPlugin } from 'rete-area-plugin'; import { createCustomElement } from '@angular/elements'; class RefDirective { el; data; emit; constructor(el) { this.el = el; } ngOnChanges() { this.emit({ type: 'render', data: { ...this.data, element: this.el.nativeElement } }); } ngOnDestroy() { this.emit({ type: 'unmount', data: { element: this.el.nativeElement } }); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: RefDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "18.2.4", type: RefDirective, selector: "[refComponent]", inputs: { data: "data", emit: "emit" }, usesOnChanges: true, ngImport: i0 }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: RefDirective, decorators: [{ type: Directive, args: [{ selector: '[refComponent]' }] }], ctorParameters: () => [{ type: i0.ElementRef }], propDecorators: { data: [{ type: Input }], emit: [{ type: Input }] } }); class NodeComponent { cdr; data; emit; rendered; seed = 0; get width() { return this.data.width; } get height() { return this.data.height; } get selected() { return this.data.selected; } constructor(cdr) { this.cdr = cdr; this.cdr.detach(); } ngOnChanges() { this.cdr.detectChanges(); requestAnimationFrame(() => this.rendered()); this.seed++; // force render sockets } sortByIndex(a, b) { const ai = a.value?.index || 0; const bi = b.value?.index || 0; return ai - bi; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: NodeComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.4", type: NodeComponent, selector: "ng-component", inputs: { data: "data", emit: "emit", rendered: "rendered" }, host: { attributes: { "data-testid": "node" }, properties: { "style.width.px": "this.width", "style.height.px": "this.height", "class.selected": "this.selected" } }, usesOnChanges: true, ngImport: i0, template: "<div class=\"title\" data-testid=\"title\">{{ data.label }}</div>\n<div\n class=\"output\"\n *ngFor=\"let output of data.outputs | keyvalue : sortByIndex\"\n [attr.data-testid]=\"'output-' + output.key\"\n>\n <div class=\"output-title\" data-testid=\"output-title\">\n {{ output.value?.label }}\n </div>\n <div\n class=\"output-socket\"\n refComponent\n [data]=\"{\n type: 'socket',\n side: 'output',\n key: output.key,\n nodeId: data.id,\n payload: output.value?.socket,\n seed: seed\n }\"\n [emit]=\"emit\"\n data-testid=\"output-socket\"\n ></div>\n</div>\n<div\n class=\"control\"\n *ngFor=\"let control of data.controls | keyvalue : sortByIndex\"\n refComponent\n [data]=\"{ type: 'control', payload: control.value }\"\n [emit]=\"emit\"\n [attr.data-testid]=\"'control-' + control.key\"\n></div>\n<div\n class=\"input\"\n *ngFor=\"let input of data.inputs | keyvalue : sortByIndex\"\n [attr.data-testid]=\"'input-' + input.key\"\n>\n <div\n class=\"input-socket\"\n refComponent\n [data]=\"{\n type: 'socket',\n side: 'input',\n key: input.key,\n nodeId: data.id,\n payload: input.value?.socket,\n seed: seed\n }\"\n [emit]=\"emit\"\n data-testid=\"input-socket\"\n ></div>\n <div\n class=\"input-title\"\n data-testid=\"input-title\"\n *ngIf=\"!input.value?.control || !input.value?.showControl\"\n >\n {{ input.value?.label }}\n </div>\n <div\n class=\"input-control\"\n [style.display]=\"\n input.value?.control && input.value?.showControl ? '' : 'none'\n \"\n refComponent\n [data]=\"{ type: 'control', payload: input.value?.control }\"\n [emit]=\"emit\"\n data-testid=\"input-control\"\n ></div>\n</div>\n", styles: [":host{display:block;background:#6e88ffcc;border:2px solid #4e58bf;border-radius:10px;cursor:pointer;box-sizing:border-box;width:180px;height:auto;padding-bottom:6px;position:relative;-webkit-user-select:none;user-select:none;line-height:initial;font-family:Arial}:host:hover{background:#8299ffcc}:host.selected{background:#ffd92c;border-color:#e3c000}:host .title{color:#fff;font-family:sans-serif;font-size:18px;padding:8px}:host .output{text-align:right}:host .input{text-align:left}:host .input-title,:host .output-title{vertical-align:middle;color:#fff;display:inline-block;font-family:sans-serif;font-size:14px;margin:6px;line-height:24px}:host .input-title[hidden],:host .output-title[hidden]{display:none}:host .output-socket{text-align:right;margin-right:-18px;display:inline-block}:host .input-socket{text-align:left;margin-left:-18px;display:inline-block}:host .input-control{z-index:1;width:calc(100% - 36px);vertical-align:middle;display:inline-block}:host .control{padding:6px 18px}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: RefDirective, selector: "[refComponent]", inputs: ["data", "emit"] }, { kind: "pipe", type: i1.KeyValuePipe, name: "keyvalue" }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: NodeComponent, decorators: [{ type: Component, args: [{ host: { 'data-testid': 'node' }, template: "<div class=\"title\" data-testid=\"title\">{{ data.label }}</div>\n<div\n class=\"output\"\n *ngFor=\"let output of data.outputs | keyvalue : sortByIndex\"\n [attr.data-testid]=\"'output-' + output.key\"\n>\n <div class=\"output-title\" data-testid=\"output-title\">\n {{ output.value?.label }}\n </div>\n <div\n class=\"output-socket\"\n refComponent\n [data]=\"{\n type: 'socket',\n side: 'output',\n key: output.key,\n nodeId: data.id,\n payload: output.value?.socket,\n seed: seed\n }\"\n [emit]=\"emit\"\n data-testid=\"output-socket\"\n ></div>\n</div>\n<div\n class=\"control\"\n *ngFor=\"let control of data.controls | keyvalue : sortByIndex\"\n refComponent\n [data]=\"{ type: 'control', payload: control.value }\"\n [emit]=\"emit\"\n [attr.data-testid]=\"'control-' + control.key\"\n></div>\n<div\n class=\"input\"\n *ngFor=\"let input of data.inputs | keyvalue : sortByIndex\"\n [attr.data-testid]=\"'input-' + input.key\"\n>\n <div\n class=\"input-socket\"\n refComponent\n [data]=\"{\n type: 'socket',\n side: 'input',\n key: input.key,\n nodeId: data.id,\n payload: input.value?.socket,\n seed: seed\n }\"\n [emit]=\"emit\"\n data-testid=\"input-socket\"\n ></div>\n <div\n class=\"input-title\"\n data-testid=\"input-title\"\n *ngIf=\"!input.value?.control || !input.value?.showControl\"\n >\n {{ input.value?.label }}\n </div>\n <div\n class=\"input-control\"\n [style.display]=\"\n input.value?.control && input.value?.showControl ? '' : 'none'\n \"\n refComponent\n [data]=\"{ type: 'control', payload: input.value?.control }\"\n [emit]=\"emit\"\n data-testid=\"input-control\"\n ></div>\n</div>\n", styles: [":host{display:block;background:#6e88ffcc;border:2px solid #4e58bf;border-radius:10px;cursor:pointer;box-sizing:border-box;width:180px;height:auto;padding-bottom:6px;position:relative;-webkit-user-select:none;user-select:none;line-height:initial;font-family:Arial}:host:hover{background:#8299ffcc}:host.selected{background:#ffd92c;border-color:#e3c000}:host .title{color:#fff;font-family:sans-serif;font-size:18px;padding:8px}:host .output{text-align:right}:host .input{text-align:left}:host .input-title,:host .output-title{vertical-align:middle;color:#fff;display:inline-block;font-family:sans-serif;font-size:14px;margin:6px;line-height:24px}:host .input-title[hidden],:host .output-title[hidden]{display:none}:host .output-socket{text-align:right;margin-right:-18px;display:inline-block}:host .input-socket{text-align:left;margin-left:-18px;display:inline-block}:host .input-control{z-index:1;width:calc(100% - 36px);vertical-align:middle;display:inline-block}:host .control{padding:6px 18px}\n"] }] }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { data: [{ type: Input }], emit: [{ type: Input }], rendered: [{ type: Input }], width: [{ type: HostBinding, args: ['style.width.px'] }], height: [{ type: HostBinding, args: ['style.height.px'] }], selected: [{ type: HostBinding, args: ['class.selected'] }] } }); class SocketComponent { cdr; data; side; rendered; get title() { return this.data.name; } get color() { return this.side === "input" ? "red" : "blue"; } constructor(cdr) { this.cdr = cdr; this.cdr.detach(); } ngOnChanges() { this.cdr.detectChanges(); requestAnimationFrame(() => this.rendered()); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: SocketComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.4", type: SocketComponent, selector: "ng-component", inputs: { data: "data", side: "side", rendered: "rendered" }, host: { properties: { "title": "this.title", "style.background": "this.color" } }, usesOnChanges: true, ngImport: i0, template: ``, isInline: true, styles: [":host{display:inline-block;cursor:pointer;border:1px solid white;border-radius:12px;width:24px;height:24px;margin:6px;vertical-align:middle;z-index:2;box-sizing:border-box}:host:hover{border-width:4px}:host.multiple{border-color:#ff0}\n"] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: SocketComponent, decorators: [{ type: Component, args: [{ template: ``, styles: [":host{display:inline-block;cursor:pointer;border:1px solid white;border-radius:12px;width:24px;height:24px;margin:6px;vertical-align:middle;z-index:2;box-sizing:border-box}:host:hover{border-width:4px}:host.multiple{border-color:#ff0}\n"] }] }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { data: [{ type: Input }], side: [{ type: Input }], rendered: [{ type: Input }], title: [{ type: HostBinding, args: ["title"] }], color: [{ type: HostBinding, args: ["style.background"] }] } }); class ControlComponent { cdr; data; rendered; pointerdown(event) { event.stopPropagation(); } constructor(cdr) { this.cdr = cdr; this.cdr.detach(); } ngOnChanges(changes) { const seed = changes['seed']; const data = changes['data']; if ((seed && seed.currentValue !== seed.previousValue) || (data && data.currentValue !== data.previousValue)) { this.cdr.detectChanges(); } requestAnimationFrame(() => this.rendered()); } onChange(e) { const target = e.target; const value = (this.data.type === 'number' ? +target.value : target.value); this.data.setValue(value); this.cdr.detectChanges(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: ControlComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.4", type: ControlComponent, selector: "ng-component", inputs: { data: "data", rendered: "rendered" }, host: { listeners: { "pointerdown": "pointerdown($event)" } }, usesOnChanges: true, ngImport: i0, template: "<input\n [value]=\"data.value\"\n [readonly]=\"data.readonly\"\n [type]=\"data.type\"\n (input)=\"onChange($event)\"\n/>\n", styles: ["input{width:100%;border-radius:30px;background-color:#fff;padding:2px 6px;border:1px solid #999;font-size:110%;box-sizing:border-box}\n"] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: ControlComponent, decorators: [{ type: Component, args: [{ template: "<input\n [value]=\"data.value\"\n [readonly]=\"data.readonly\"\n [type]=\"data.type\"\n (input)=\"onChange($event)\"\n/>\n", styles: ["input{width:100%;border-radius:30px;background-color:#fff;padding:2px 6px;border:1px solid #999;font-size:110%;box-sizing:border-box}\n"] }] }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { data: [{ type: Input }], rendered: [{ type: Input }], pointerdown: [{ type: HostListener, args: ['pointerdown', ['$event']] }] } }); class ConnectionComponent { data; start; end; path; set color(value) { this._color = value; } set direction(value) { this._pathClass = value === "forward" ? "animated-path" : value === "backward" ? "reverse-ants" : "static-dash"; } set value(value) { this._value = value; } _value = 0; _color = "rgba(188,202,210,0.3)"; _pathClass = "static-dash"; static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: ConnectionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.4", type: ConnectionComponent, selector: "connection", inputs: { data: "data", start: "start", end: "end", path: "path", color: "color", direction: "direction", value: "value" }, ngImport: i0, template: "<svg data-testid=\"connection\">\n <path\n [attr.d]=\"path\"\n [attr.stroke]=\"_color\"\n fill=\"none\"\n [ngClass]=\"_pathClass\"\n />\n</svg>\n", styles: ["svg{overflow:visible!important;position:absolute;pointer-events:none;width:9999px;height:9999px}svg path{fill:none;stroke-width:.15rem;pointer-events:auto}.animated-path{stroke-dasharray:20 10;stroke-dashoffset:0;animation:dashmove 1s linear infinite}.reverse-ants{stroke-dasharray:20 10;stroke-dashoffset:0;animation:dashmove-reverse 1s linear infinite}.static-dash{animation:none}@keyframes dashmove{to{stroke-dashoffset:-30}}@keyframes dashmove-reverse{to{stroke-dashoffset:30}}\n"], dependencies: [{ kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: ConnectionComponent, decorators: [{ type: Component, args: [{ selector: "connection", template: "<svg data-testid=\"connection\">\n <path\n [attr.d]=\"path\"\n [attr.stroke]=\"_color\"\n fill=\"none\"\n [ngClass]=\"_pathClass\"\n />\n</svg>\n", styles: ["svg{overflow:visible!important;position:absolute;pointer-events:none;width:9999px;height:9999px}svg path{fill:none;stroke-width:.15rem;pointer-events:auto}.animated-path{stroke-dasharray:20 10;stroke-dashoffset:0;animation:dashmove 1s linear infinite}.reverse-ants{stroke-dasharray:20 10;stroke-dashoffset:0;animation:dashmove-reverse 1s linear infinite}.static-dash{animation:none}@keyframes dashmove{to{stroke-dashoffset:-30}}@keyframes dashmove-reverse{to{stroke-dashoffset:30}}\n"] }] }], propDecorators: { data: [{ type: Input }], start: [{ type: Input }], end: [{ type: Input }], path: [{ type: Input }], color: [{ type: Input }], direction: [{ type: Input }], value: [{ type: Input }] } }); class ConnectionWrapperComponent { cdr; viewContainerRef; componentFactoryResolver; data; start; end; path; rendered; connectionComponent; color; ref; startOb = null; get _start() { return "x" in this.start ? this.start : this.startOb; } endOb = null; get _end() { return "x" in this.end ? this.end : this.endOb; } _path; constructor(cdr, viewContainerRef, componentFactoryResolver) { this.cdr = cdr; this.viewContainerRef = viewContainerRef; this.componentFactoryResolver = componentFactoryResolver; this.cdr.detach(); } async ngOnChanges() { await this.updatePath(); requestAnimationFrame(() => this.rendered()); this.cdr.detectChanges(); this.update(); } async updatePath() { if (this._start && this._end) { this._path = await this.path(this._start, this._end); } } ngOnInit() { if (typeof this.start === "function") { this.start(async (value) => { this.startOb = value; await this.updatePath(); this.cdr.detectChanges(); this.update(); }); } if (typeof this.end === "function") { this.end(async (value) => { this.endOb = value; await this.updatePath(); this.cdr.detectChanges(); this.update(); }); } const componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.connectionComponent); this.viewContainerRef.clear(); this.ref = this.viewContainerRef.createComponent(componentFactory); this.update(); } update() { this.ref.instance.data = this.data; this.ref.instance.start = this._start; this.ref.instance.end = this._end; this.ref.instance.path = this._path; this.ref.instance.color = this.data.color || "rgba(188,202,210,0.3)"; this.ref.instance.direction = this.data.direction || "static-dash"; this.ref.instance.value = this.data.value || 0; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: ConnectionWrapperComponent, deps: [{ token: i0.ChangeDetectorRef }, { token: i0.ViewContainerRef }, { token: i0.ComponentFactoryResolver }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.4", type: ConnectionWrapperComponent, selector: "ng-component", inputs: { data: "data", start: "start", end: "end", path: "path", rendered: "rendered", connectionComponent: "connectionComponent", color: "color" }, usesOnChanges: true, ngImport: i0, template: "", isInline: true }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: ConnectionWrapperComponent, decorators: [{ type: Component, args: [{ template: "", }] }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }, { type: i0.ViewContainerRef }, { type: i0.ComponentFactoryResolver }], propDecorators: { data: [{ type: Input }], start: [{ type: Input }], end: [{ type: Input }], path: [{ type: Input }], rendered: [{ type: Input }], connectionComponent: [{ type: Input }], color: [{ type: Input }] } }); /** * Classic preset for rendering nodes, connections, controls and sockets. */ function setup$3(props) { const positionWatcher = typeof props?.socketPositionWatcher === "undefined" ? getDOMSocketPosition() // fix Type instantiation is excessively deep and possibly infinite. : props?.socketPositionWatcher; const { node, connection, socket, control } = props?.customize || {}; return { attach(plugin) { positionWatcher.attach(plugin); }, update(context) { const data = context.data.payload; if (context.data.type === "connection") { const { start, end } = context.data; return { data, ...(start ? { start } : {}), ...(end ? { end } : {}), }; } return { data }; }, updateNode(context, plugin) { const parent = plugin.parentScope(); const emit = parent.emit.bind(parent); const rendered = () => { emit({ type: "rendered", data: context.data }); }; if (context.data.type === "node") { const component = node ? node(context.data) : NodeComponent; return { key: `node-${context.data.payload.id}`, component, props: { data: context.data.payload, emit, rendered, }, }; } return; }, mount(context, plugin) { const parent = plugin.parentScope(); const emit = parent.emit.bind(parent); const rendered = () => { emit({ type: "rendered", data: context.data }); }; if (context.data.type === "node") { const component = node ? node(context.data) : NodeComponent; return { key: `node-${context.data.payload.id}`, component, props: { data: context.data.payload, emit, rendered, }, }; } if (context.data.type === "connection") { const component = connection ? connection(context.data) : ConnectionComponent; const id = context.data.payload.id; const { sourceOutput, targetInput, source, target } = context.data.payload; const { start, end, payload } = context.data; return { key: `connection-${id}`, component: ConnectionWrapperComponent, props: { connectionComponent: component, data: payload, start: start || ((change) => positionWatcher.listen(source, "output", sourceOutput, change)), end: end || ((change) => positionWatcher.listen(target, "input", targetInput, change)), path: async (start, end) => { const response = await plugin.emit({ type: "connectionpath", data: { payload, points: [start, end] }, }); if (!response) return ""; let { path, points } = response.data; const curvature = 0.3; if (payload.type === "straight") path = buildStepStraightPath(start, end); if (payload.type === "90deg") { path = buildStep90DegPath(start, end, payload.sourceDirection, payload.targetDirection); } if (payload.type === "curve") { path = buildCurvePath(start, end, payload.sourceDirection, payload.targetDirection, curvature, payload.curveDirection, payload.curvatureOffset); } if (!path && points.length !== 2) throw new Error("cannot render connection with a custom number of points"); if (!path) return payload.isLoop ? loopConnectionPath(points, curvature, 120) : classicConnectionPath(points, curvature); return path; }, rendered, }, }; } if (context.data.type === "socket") { const component = socket ? socket(context.data) : SocketComponent; return { key: `socket-${getUID()}`, component, props: { data: context.data.payload, side: context.data.side, rendered, }, }; } if (context.data.type === "control") { const component = control ? control(context.data) : context.data.payload instanceof ClassicPreset.InputControl ? ControlComponent : null; if (component) { return { key: `control-${context.data.payload.id}`, component, props: { data: context.data.payload, rendered, }, }; } return; } return; }, }; } function buildStepStraightPath(start, end) { const f = (n) => Math.round(n * 10) / 10; const segments = []; segments.push(`M${f(start.x)},${f(start.y)}`); segments.push(`L${f(end.x)},${f(end.y)}`); return segments.join(""); } function buildStep90DegPath(start, end, sourceDirection, targetDirection) { const preX = sourceDirection === "left" ? start.x - 50 : sourceDirection === "right" ? start.x + 50 : start.x; const postX = targetDirection === "left" ? end.x - 50 : targetDirection === "right" ? end.x + 50 : end.x; const preY = sourceDirection === "up" ? start.y - 50 : sourceDirection === "down" ? start.y + 50 : start.y; const postY = targetDirection === "up" ? end.y - 50 : targetDirection === "down" ? end.y + 50 : end.y; const midX = (preX + postX) / 2; const midY = (preY + postY) / 2; const f = (n) => Math.round(n * 10) / 10; const segments = []; segments.push(`M${f(start.x)},${f(start.y)}`); segments.push(`L${f(preX)},${f(preY)}`); { segments.push(`L${f(midX)},${f(preY)}`); segments.push(`L${f(midX)},${f(midY)}`); segments.push(`L${f(postX)},${f(midY)}`); segments.push(`L${f(postX)},${f(postY)}`); } segments.push(`L${f(postX)},${f(postY)}`); segments.push(`L${f(end.x)},${f(end.y)}`); return segments.join(""); } function buildCurvePath(start, end, sourceDirection, targetDirection, curvature, curveDirection, curvatureOffset) { const preX = start.x; const postX = end.x; const preY = start.y; const postY = end.y; const f = (n) => Math.round(n * 10) / 10; const curv = typeof curvature === "number" ? curvature : 0.3; const curvOffset = typeof curvatureOffset === "number" ? curvatureOffset : 3; const midX = (preX + postX) / 2; const midY = (preY + postY) / 2; let ctrlX = midX; let ctrlY = midY; const remSize = parseFloat(getComputedStyle(document.documentElement).fontSize); const dx = Math.abs(postX - preX); const dy = Math.abs(postY - preY); switch (curveDirection) { case "right": ctrlX = Math.max(preX, postX) + dx * curv + curvOffset * remSize; break; case "left": ctrlX = Math.max(preX, postX) + dx * curv - curvOffset * remSize; break; case "up": ctrlY = Math.max(preY, postY) + dy * curv - curvOffset * remSize; break; case "down": ctrlY = Math.max(preY, postY) + dy * curv + curvOffset * remSize; break; } // Quadratic Bezier for smooth parenthesis-like curve return `M${f(preX)} ${f(preY)} Q ${f(ctrlX)} ${f(ctrlY)} ${f(postX)} ${f(postY)} `; } var index$4 = /*#__PURE__*/Object.freeze({ __proto__: null, buildCurvePath: buildCurvePath, buildStep90DegPath: buildStep90DegPath, buildStepStraightPath: buildStepStraightPath, setup: setup$3 }); function debounce(cb) { return { timeout: null, cancel() { if (this.timeout) { window.clearTimeout(this.timeout); this.timeout = null; } }, call(delay) { this.timeout = window.setTimeout(() => { cb(); }, delay); } }; } class ContextMenuSearchComponent { value; update = new EventEmitter(); static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: ContextMenuSearchComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.4", type: ContextMenuSearchComponent, selector: "context-menu-search", inputs: { value: "value" }, outputs: { update: "update" }, ngImport: i0, template: "<input class=\"search\" [value]=\"value\" (input)=\"update.emit($any($event.target)?.value || '')\"\n data-testid=\"context-menu-search-input\" />\n", styles: [".search{color:#fff;padding:1px 8px;border:1px solid white;border-radius:10px;font-size:16px;font-family:serif;width:100%;box-sizing:border-box;background:transparent}\n"] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: ContextMenuSearchComponent, decorators: [{ type: Component, args: [{ selector: 'context-menu-search', template: "<input class=\"search\" [value]=\"value\" (input)=\"update.emit($any($event.target)?.value || '')\"\n data-testid=\"context-menu-search-input\" />\n", styles: [".search{color:#fff;padding:1px 8px;border:1px solid white;border-radius:10px;font-size:16px;font-family:serif;width:100%;box-sizing:border-box;background:transparent}\n"] }] }], propDecorators: { value: [{ type: Input }], update: [{ type: Output }] } }); // [imports] class ContextMenuItemComponent { cdr; subitems; delay; select = new EventEmitter(); hide = new EventEmitter(); get block() { return true; } get hasSubitems() { return this.subitems; } click(event) { event.stopPropagation(); this.select.emit(); this.hide.emit(); } pointerdown(event) { event.stopPropagation(); } wheel(event) { event.stopPropagation(); } hideSubitems = debounce(() => { this.visibleSubitems = false; this.cdr.detectChanges(); }); visibleSubitems = false; pointerover() { this.hideSubitems.cancel(); this.visibleSubitems = true; this.cdr.detectChanges(); } pointerleave() { this.hideSubitems.call(this.delay); this.cdr.detectChanges(); } constructor(cdr) { this.cdr = cdr; this.cdr.detach(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: ContextMenuItemComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.4", type: ContextMenuItemComponent, selector: "context-menu-item", inputs: { subitems: "subitems", delay: "delay" }, outputs: { select: "select", hide: "hide" }, host: { attributes: { "data-testid": "context-menu-item" }, listeners: { "click": "click($event)", "pointerdown": "pointerdown($event)", "wheel": "wheel($event)", "pointerover": "pointerover()", "pointerleave": "pointerleave()" }, properties: { "class.block": "this.block", "class.hasSubitems": "this.hasSubitems" } }, ngImport: i0, template: "<ng-content></ng-content>\n<div class=\"subitems\" *ngIf=\"subitems && visibleSubitems\">\n <context-menu-item\n *ngFor=\"let item of subitems\"\n [delay]=\"delay\"\n (select)=\"item.handler()\"\n (hide)=\"hide.emit()\"\n >\n {{ item.label }}\n </context-menu-item>\n</div>\n", styles: ["@charset \"UTF-8\";:host(.hasSubitems):after{content:\"\\25ba\";position:absolute;opacity:.6;right:5px;top:5px;font-family:initial}.subitems{position:absolute;top:0;left:100%;width:120px}\n", ".block{display:block;color:#fff;padding:4px;border-bottom:1px solid rgba(69,103,255,.8);background-color:#6e88ffcc;cursor:pointer;width:100%;position:relative}.block:first-child{border-top-left-radius:5px;border-top-right-radius:5px}.block:last-child{border-bottom-left-radius:5px;border-bottom-right-radius:5px}.block:hover{background-color:#8299ffcc}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ContextMenuItemComponent, selector: "context-menu-item", inputs: ["subitems", "delay"], outputs: ["select", "hide"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: ContextMenuItemComponent, decorators: [{ type: Component, args: [{ selector: 'context-menu-item', host: { 'data-testid': 'context-menu-item' }, template: "<ng-content></ng-content>\n<div class=\"subitems\" *ngIf=\"subitems && visibleSubitems\">\n <context-menu-item\n *ngFor=\"let item of subitems\"\n [delay]=\"delay\"\n (select)=\"item.handler()\"\n (hide)=\"hide.emit()\"\n >\n {{ item.label }}\n </context-menu-item>\n</div>\n", styles: ["@charset \"UTF-8\";:host(.hasSubitems):after{content:\"\\25ba\";position:absolute;opacity:.6;right:5px;top:5px;font-family:initial}.subitems{position:absolute;top:0;left:100%;width:120px}\n", ".block{display:block;color:#fff;padding:4px;border-bottom:1px solid rgba(69,103,255,.8);background-color:#6e88ffcc;cursor:pointer;width:100%;position:relative}.block:first-child{border-top-left-radius:5px;border-top-right-radius:5px}.block:last-child{border-bottom-left-radius:5px;border-bottom-right-radius:5px}.block:hover{background-color:#8299ffcc}\n"] }] }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { subitems: [{ type: Input }], delay: [{ type: Input }], select: [{ type: Output }], hide: [{ type: Output }], block: [{ type: HostBinding, args: ['class.block'] }], hasSubitems: [{ type: HostBinding, args: ['class.hasSubitems'] }], click: [{ type: HostListener, args: ['click', ['$event']] }], pointerdown: [{ type: HostListener, args: ['pointerdown', ['$event']] }], wheel: [{ type: HostListener, args: ['wheel', ['$event']] }], pointerover: [{ type: HostListener, args: ['pointerover'] }], pointerleave: [{ type: HostListener, args: ['pointerleave'] }] } }); // [imports] class ContextMenuComponent { cdr; items; delay; searchBar; onHide; rendered; filter = ''; hide = debounce(() => { this.onHide(); this.cdr.detectChanges(); }); customAttribute = ''; pointerover() { this.hide.cancel(); this.cdr.detectChanges(); } pointerleave() { this.hide.call(this.delay); this.cdr.detectChanges(); } constructor(cdr) { this.cdr = cdr; this.cdr.detach(); } ngOnChanges() { this.cdr.detectChanges(); requestAnimationFrame(() => this.rendered()); } setFilter(value) { this.filter = value; this.cdr.detectChanges(); } getItems() { const filterRegexp = new RegExp(this.filter, 'i'); const filteredList = this.items.filter(item => (item.label.match(filterRegexp))); return filteredList; } ngOnDestroy() { if (this.hide) this.hide.cancel(); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: ContextMenuComponent, deps: [{ token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.4", type: ContextMenuComponent, selector: "ng-component", inputs: { items: "items", delay: "delay", searchBar: "searchBar", onHide: "onHide", rendered: "rendered" }, host: { attributes: { "data-testid": "context-menu" }, listeners: { "mouseover": "pointerover()", "mouseleave": "pointerleave()" }, properties: { "attr.rete-context-menu": "this.customAttribute" } }, usesOnChanges: true, ngImport: i0, template: "<div class=\"block\" *ngIf=\"searchBar\">\n <context-menu-search\n [value]=\"filter\"\n (update)=\"setFilter($event)\"\n ></context-menu-search>\n</div>\n\n<context-menu-item\n *ngFor=\"let item of getItems()\"\n [delay]=\"delay\"\n (select)=\"item.handler()\"\n [subitems]=\"item.subitems\"\n (hide)=\"onHide()\"\n>\n {{ item.label }}\n</context-menu-item>\n", styles: [":host{display:block;padding:10px;width:120px;margin-top:-20px;margin-left:-60px}\n", ".block{display:block;color:#fff;padding:4px;border-bottom:1px solid rgba(69,103,255,.8);background-color:#6e88ffcc;cursor:pointer;width:100%;position:relative}.block:first-child{border-top-left-radius:5px;border-top-right-radius:5px}.block:last-child{border-bottom-left-radius:5px;border-bottom-right-radius:5px}.block:hover{background-color:#8299ffcc}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: ContextMenuSearchComponent, selector: "context-menu-search", inputs: ["value"], outputs: ["update"] }, { kind: "component", type: ContextMenuItemComponent, selector: "context-menu-item", inputs: ["subitems", "delay"], outputs: ["select", "hide"] }] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: ContextMenuComponent, decorators: [{ type: Component, args: [{ host: { 'data-testid': 'context-menu' }, template: "<div class=\"block\" *ngIf=\"searchBar\">\n <context-menu-search\n [value]=\"filter\"\n (update)=\"setFilter($event)\"\n ></context-menu-search>\n</div>\n\n<context-menu-item\n *ngFor=\"let item of getItems()\"\n [delay]=\"delay\"\n (select)=\"item.handler()\"\n [subitems]=\"item.subitems\"\n (hide)=\"onHide()\"\n>\n {{ item.label }}\n</context-menu-item>\n", styles: [":host{display:block;padding:10px;width:120px;margin-top:-20px;margin-left:-60px}\n", ".block{display:block;color:#fff;padding:4px;border-bottom:1px solid rgba(69,103,255,.8);background-color:#6e88ffcc;cursor:pointer;width:100%;position:relative}.block:first-child{border-top-left-radius:5px;border-top-right-radius:5px}.block:last-child{border-bottom-left-radius:5px;border-bottom-right-radius:5px}.block:hover{background-color:#8299ffcc}\n"] }] }], ctorParameters: () => [{ type: i0.ChangeDetectorRef }], propDecorators: { items: [{ type: Input }], delay: [{ type: Input }], searchBar: [{ type: Input }], onHide: [{ type: Input }], rendered: [{ type: Input }], customAttribute: [{ type: HostBinding, args: ['attr.rete-context-menu'] }], pointerover: [{ type: HostListener, args: ['mouseover'] }], pointerleave: [{ type: HostListener, args: ['mouseleave'] }] } }); /** * Preset for rendering context menu. */ function setup$2(props) { const delay = typeof props?.delay === "undefined" ? 1000 : props.delay; return { update(context) { if (context.data.type === "contextmenu") { return { items: context.data.items, delay, searchBar: context.data.searchBar, onHide: context.data.onHide, }; } return null; }, mount(context, plugin) { const parent = plugin.parentScope(); const emit = parent.emit.bind(parent); const rendered = () => { emit({ type: "rendered", data: context.data }); }; if (context.data.type === "contextmenu") { return { key: "context-menu", component: ContextMenuComponent, props: { items: context.data.items, delay, searchBar: context.data.searchBar, onHide: context.data.onHide, rendered, }, }; } return null; }, }; } var index$3 = /*#__PURE__*/Object.freeze({ __proto__: null, setup: setup$2 }); function useDrag(translate, getPointer) { return { start(e) { let previous = { ...getPointer(e) }; function move(moveEvent) { const current = { ...getPointer(moveEvent) }; const dx = current.x - previous.x; const dy = current.y - previous.y; previous = current; translate(dx, dy); } function up() { window.removeEventListener('pointermove', move); window.removeEventListener('pointerup', up); window.removeEventListener('pointercancel', up); } window.addEventListener('pointermove', move); window.addEventListener('pointerup', up); window.addEventListener('pointercancel', up); } }; } class MiniViewportComponent { left; top; width; height; containerWidth; translate; drag = useDrag((dx, dy) => this.onDrag(dx, dy), e => ({ x: e.pageX, y: e.pageY })); get styleLeft() { return this.px(this.scale(this.left)); } get styleTop() { return this.px(this.scale(this.top)); } get styleWidth() { return this.px(this.scale(this.width)); } get styleHeight() { return this.px(this.scale(this.height)); } pointerdown(event) { event.stopPropagation(); this.drag.start(event); } px(value) { return `${value}px`; } scale(v) { return v * this.containerWidth; } invert(v) { return v / this.containerWidth; } onDrag(dx, dy) { this.translate(this.invert(-dx), this.invert(-dy)); } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: MiniViewportComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.4", type: MiniViewportComponent, selector: "minimap-mini-viewport", inputs: { left: "left", top: "top", width: "width", height: "height", containerWidth: "containerWidth", translate: "translate" }, host: { attributes: { "data-testid": "minimap-viewport" }, listeners: { "pointerdown": "pointerdown($event)" }, properties: { "style.left": "this.styleLeft", "style.top": "this.styleTop", "style.width": "this.styleWidth", "style.height": "this.styleHeight" } }, ngImport: i0, template: "", styles: [":host{display:block;position:absolute;background:#fffb8052;border:1px solid #ffe52b}\n"] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: MiniViewportComponent, decorators: [{ type: Component, args: [{ selector: 'minimap-mini-viewport', host: { 'data-testid': 'minimap-viewport' }, template: "", styles: [":host{display:block;position:absolute;background:#fffb8052;border:1px solid #ffe52b}\n"] }] }], propDecorators: { left: [{ type: Input }], top: [{ type: Input }], width: [{ type: Input }], height: [{ type: Input }], containerWidth: [{ type: Input }], translate: [{ type: Input }], styleLeft: [{ type: HostBinding, args: ['style.left'] }], styleTop: [{ type: HostBinding, args: ['style.top'] }], styleWidth: [{ type: HostBinding, args: ['style.width'] }], styleHeight: [{ type: HostBinding, args: ['style.height'] }], pointerdown: [{ type: HostListener, args: ['pointerdown', ['$event']] }] } }); class MiniNodeComponent { left; top; width; height; get styleLeft() { return this.px(this.left); } get styleTop() { return this.px(this.top); } get styleWidth() { return this.px(this.width); } get styleHeight() { return this.px(this.height); } px(value) { return `${value}px`; } static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: MiniNodeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "18.2.4", type: MiniNodeComponent, selector: "minimap-mini-node", inputs: { left: "left", top: "top", width: "width", height: "height" }, host: { attributes: { "data-testid": "minimap-node" }, properties: { "style.left": "this.styleLeft", "style.top": "this.styleTop", "style.width": "this.styleWidth", "style.height": "this.styleHeight" } }, ngImport: i0, template: "", styles: [":host{display:block;position:absolute;background:#6e88ffcc;border:1px solid rgba(192,206,212,.6)}\n"] }); } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.2.4", ngImport: i0, type: MiniNodeComponent, decorators: [{ type: Component, args: [{ selector: 'minimap-mini-node', host: { 'data-testid': 'minimap-node' }, template: "", styles: [":host{display:block;position:absolute;background:#6e88ffcc;border:1px solid rgba(192,206,212,.6)}\n"] }] }], propDecorators: { left: [{ type: Input }], top: [{ type: Input }], width: [{ type: Input }], height: [{ type: Input }], styleLeft: [{ type: HostBinding, args: ['style.left'] }], styleTop: [{ type: HostBinding, args: ['style.top'] }], styleWidth: [{ type: HostBinding, args: ['style.width'] }], styleHeight: [{ type: HostBinding, args: ['style.height'] }] } }); // [imports] class MinimapComponent { el; cdr; rendered; size; ratio; nodes; viewport; translate; point; get width() { return this.px(this.size * this.ratio); } get height() { return this.px(this.size); } pointerdown(event) { event.stopPropagation(); event.preventDefault(); } dblclick(event) { event.stopPropagation(); event.preventDefault(); if (!this.el.nativeElement) return; const box = this.el.nativeElement.getBoundingClientRect(); const x = (event.c