Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement libavoid example in angular #14

Open
wants to merge 3 commits into
base: angular-libavoid
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"@angular/platform-browser-dynamic": "^16.2.6",
"@angular/router": "^16.2.6",
"@joint/plus": "file:joint-plus.tgz",
"libavoid-js": "^0.4.0",
"rxjs": "^7.8.1",
"tslib": "^2.6.2",
"zone.js": "~0.13.0"
Expand Down
239 changes: 211 additions & 28 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { AfterViewInit, OnInit, Component, ElementRef, ViewChild } from '@angular/core';
import { dia, ui, shapes } from '@joint/plus';
import { linkTools, elementTools, dia, shapes, highlighters } from '@joint/plus';
import { Node, Edge } from '../shared/shapes';
import ResizeTool from '../shared/resize-tool';
import { AvoidRouter } from '../shared/avoid-router';

@Component({
selector: 'app-root',
Expand All @@ -11,47 +14,227 @@ export class AppComponent implements OnInit, AfterViewInit {

private graph: dia.Graph;
private paper: dia.Paper;
private scroller: ui.PaperScroller;

public ngOnInit(): void {
const graph = this.graph = new dia.Graph({}, { cellNamespace: shapes });
public async ngOnInit(): Promise<any> {
const cellNamespace = {
...shapes,
Node,
Edge,
};

const graph = this.graph = new dia.Graph({}, { cellNamespace });
const paper = this.paper = new dia.Paper({
model: graph,
background: {
color: '#F8F9FA',
model: graph,
cellViewNamespace: cellNamespace,
width: 1000,
height: 600,
gridSize: 10,
interactive: { linkMove: false },
linkPinning: false,
async: true,
frozen: true,
background: { color: '#F3F7F6' },
snapLinks: { radius: 30 },
overflow: true,
defaultConnector: {
name: 'straight',
args: {
cornerType: 'cubic',
cornerRadius: 4,
},
frozen: true,
async: true,
sorting: dia.Paper.sorting.APPROX,
cellViewNamespace: shapes
},
highlighting: {
default: {
name: 'mask',
options: {
padding: 2,
attrs: {
stroke: '#EA3C24',
strokeWidth: 2,
},
},
},
},
defaultLink: () => new Edge(),
validateConnection: (
sourceView,
sourceMagnet,
targetView,
targetMagnet,
end
) => {
const source = sourceView.model as dia.Element;
const target = targetView.model as dia.Element;
if (source.isLink() || target.isLink()) return false;
if (targetMagnet === sourceMagnet) return false;
if (end === 'target' ? targetMagnet : sourceMagnet) {
return true;
}
if (source === target) return false;
return end === 'target' ? !target.hasPorts() : !source.hasPorts();
},
});

const scroller = this.scroller = new ui.PaperScroller({
paper,
autoResizePaper: true,
cursor: 'grab'
// Add tools to the elements.
graph.getElements().forEach((el) => addElementTools(el, paper));
graph.on('add', (cell) => {
if (cell.isLink()) return;
addElementTools(cell, paper);
});

function addElementTools(el: dia.Element, paper: dia.Paper) {
const tools = [
new ResizeTool({
selector: 'body',
}),
new elementTools.Remove({
useModelGeometry: true,
x: -10,
y: -10,
}),
];
if (!el.hasPorts()) {
tools.push(
new elementTools.Connect({
useModelGeometry: true,
x: 'calc(w + 10)',
y: 'calc(h - 20)',
})
);
}

el.findView(paper).addTools(new dia.ToolsView({ tools }));
}

// Add tools to the links.
paper.on('link:mouseenter', (linkView) => {
linkView.addTools(
new dia.ToolsView({
tools: [
new linkTools.Remove(),
new linkTools.TargetArrowhead(),
],
})
);
});

paper.on('link:mouseleave', (linkView) => {
linkView.removeTools();
});

paper.on('blank:pointerdblclick', (evt, x, y) => {
const node = new Node({
position: { x: x - 50, y: y - 50 },
size: { width: 100, height: 100 },
});
graph.addCell(node);
});

scroller.render();
// Add a class to the links when they are being interacted with.
// See `styles.css` for the styles.

const rect = new shapes.standard.Rectangle({
position: { x: 100, y: 100 },
size: { width: 100, height: 50 },
attrs: {
label: {
text: 'Hello World'
}
}
paper.on('link:pointerdown', (linkView) => {
highlighters.addClass.add(linkView, 'line', 'active-link', {
className: 'active-link'
});
});

this.graph.addCell(rect);
paper.on('link:pointerup', (linkView) => {
highlighters.addClass.remove(linkView);
});
}

public ngAfterViewInit(): void {
const { scroller, paper, canvas } = this;
canvas.nativeElement.appendChild(this.scroller.el);
scroller.center();
const { canvas, graph, paper } = this;

const c1 = new Node({
position: { x: 100, y: 100 },
size: { width: 100, height: 100 },
ports: {
items: [
{
group: 'top',
id: 'port1',
},
{
group: 'top',
id: 'port2',
},
{
group: 'right',
id: 'port3',
},
{
group: 'left',
id: 'port4',
// TODO: we need to redefine the port on element resize
// The port is currently defined proportionally to the element size.
// args: {
// dy: 30
// }
},
],
},
});

const c2 = c1.clone().set({
position: { x: 300, y: 300 },
size: { width: 100, height: 100 },
});

const c3 = c1.clone().set({
position: { x: 500, y: 100 },
size: { width: 100, height: 100 },
});

const c4 = new Node({
position: { x: 100, y: 400 },
size: { width: 100, height: 100 },
});

const c5 = c4.clone().set({
position: { x: 500, y: 300 },
size: { width: 100, height: 100 },
});

const l1 = new Edge({
source: { id: c1.id, port: 'port4' },
target: { id: c2.id, port: 'port4' },
});

const l2 = new Edge({
source: { id: c2.id, port: 'port2' },
target: { id: c3.id, port: 'port4' },
});

const l3 = new Edge({
source: { id: c4.id },
target: { id: c5.id },
});

const l4 = new Edge({
source: { id: c5.id },
target: { id: c4.id },
});

graph.addCells([c1, c2, c3, c4, c5, l1, l2, l3, l4]);

canvas.nativeElement.appendChild(paper.el);

paper.unfreeze();
paper.fitToContent({
useModelGeometry: true,
padding: 100,
allowNewOrigin: 'any',
});

// Start the Avoid Router.
const router = new AvoidRouter(graph, {
shapeBufferDistance: 20,
idealNudgingDistance: 10,
portOverflow: 10,
});
router.addGraphListeners();
router.routeAll();
}
}
19 changes: 16 additions & 3 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { NgModule } from '@angular/core';
import { NgModule, APP_INITIALIZER } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { AvoidRouterService } from './avoid-router.service';

export function avoidRouterInit(avoidRouterService: AvoidRouterService) {
return () => {
return avoidRouterService.load();
};
}

@NgModule({
declarations: [
Expand All @@ -12,7 +18,14 @@ import { AppComponent } from './app.component';
BrowserModule,
AppRoutingModule
],
providers: [],
providers: [
{
provide: APP_INITIALIZER,
useFactory: avoidRouterInit,
multi: true,
deps: [AvoidRouterService]
}
],
bootstrap: [AppComponent]
})
export class AppModule { }
22 changes: 22 additions & 0 deletions src/app/avoid-router.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { Injectable } from '@angular/core';
import { AvoidRouter } from '../shared/avoid-router';

@Injectable({
providedIn: 'root'
})
export class AvoidRouterService {
public isLoaded: boolean;

constructor() {}

load(): Promise<any> {

const promise = AvoidRouter.load()
.then(() => {
this.isLoaded = true;
return true;
});

return promise;
}
}
Binary file added src/assets/wasm/libavoid.wasm
Binary file not shown.
Loading