Skip to content

Commit

Permalink
feat(examples.libavoid): use web worker to run libavoid router
Browse files Browse the repository at this point in the history
  • Loading branch information
kumilingus committed Jan 10, 2025
1 parent 12b0c32 commit 5cdbedb
Show file tree
Hide file tree
Showing 4 changed files with 256 additions and 74 deletions.
194 changes: 124 additions & 70 deletions examples/libavoid/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,77 +73,47 @@ export const init = async () => {
},
});

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' },
// create n nodes with links between each other in a chain
const n = 100;
const nodes = Array.from({ length: n }, (_, i) => {
return new Node({
position: { x: 100, y: i * 200},
size: { width: 100, height: 100 },
ports: {
items: [
{
group: 'top',
id: `port${i + 1}`,
},
{
group: 'right',
id: `port${i + 2}`,
},
{
group: 'left',
id: `port${i + 3}`,
},
],
},
});
});

const l2 = new Edge({
source: { id: c2.id, port: 'port2' },
target: { id: c3.id, port: 'port4' },
const links = nodes.slice(0, -1).map((node, i) => {
return new Edge({
source: { id: node.id, port: `port${i + 1}` },
target: { id: nodes[i + 1].id, port: `port${i + 2}` },
router: { name: 'rightAngle'}
});
});

const l3 = new Edge({
source: { id: c4.id },
target: { id: c5.id },
});
graph.addCells([...nodes, ...links]);

const l4 = new Edge({
source: { id: c5.id },
target: { id: c4.id },
links.forEach((link) => {
highlighters.addClass.add(link.findView(paper), 'line', 'awaiting-update', {
className: 'awaiting-update'
});
});

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

canvasEl.appendChild(paper.el);

Expand Down Expand Up @@ -224,12 +194,96 @@ export const init = async () => {

// Start the Avoid Router.

const router = new AvoidRouter(graph, {
shapeBufferDistance: 20,
idealNudgingDistance: 10,
portOverflow: Node.PORT_RADIUS,
// const router = new AvoidRouter(graph, {
// shapeBufferDistance: 20,
// idealNudgingDistance: 10,
// portOverflow: Node.PORT_RADIUS,
// });

// router.addGraphListeners();
// router.routeAll();

const routerWorker = new Worker(new URL("./worker.js", import.meta.url));

routerWorker.onmessage = (e) => {
const { command, ...data } = e.data;
switch (command) {
case 'routed': {
const { cells } = data;
cells.forEach((cell) => {
const model = graph.getCell(cell.id);
if (model.isElement()) return;
model.set({
vertices: cell.vertices,
source: cell.source,
target: cell.target,
router: null
}, {
fromWorker: true
});
});
highlighters.addClass.removeAll(paper, 'awaiting-update');
break;
}
default:
console.log('Unknown command', command);
break;
}
}

routerWorker.postMessage([{
command: 'reset',
cells: graph.toJSON().cells
}]);

graph.on('change', (cell, opt) => {

if (opt.fromWorker) {
return;
}

routerWorker.postMessage([{
command: 'change',
cell: cell.toJSON()
}]);

if (cell.isElement() && (cell.hasChanged('position') || cell.hasChanged('size'))) {
const links = graph.getConnectedLinks(cell);
links.forEach((link) => {
link.router() || link.router('rightAngle');
highlighters.addClass.add(link.findView(paper), 'line', 'awaiting-update', {
className: 'awaiting-update'
});
});
}

});

graph.on('remove', (cell) => {
routerWorker.postMessage([{
command: 'remove',
id: cell.id
}]);
});

graph.on('add', (cell) => {
routerWorker.postMessage([{
command: 'add',
cell: cell.toJSON()
}]);
});

paper.on('link:snap:connect', (linkView) => {
linkView.model.set({
router: { name: 'rightAngle' }
});
});

paper.on('link:snap:disconnect', (linkView) => {
linkView.model.set({
vertices: [],
router: null
});
});

router.addGraphListeners();
router.routeAll();
};
6 changes: 3 additions & 3 deletions examples/libavoid/src/avoid-router.js
Original file line number Diff line number Diff line change
Expand Up @@ -466,9 +466,9 @@ export class AvoidRouter {
}
// TODO:
// if ("ports" in cell.changed) {}
if (needsRerouting) {
this.avoidRouter.processTransaction();
}
// if (needsRerouting) {
// this.avoidRouter.processTransaction();
// }
}

onAvoidConnectorChange(connRefPtr) {
Expand Down
116 changes: 116 additions & 0 deletions examples/libavoid/src/worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import { AvoidRouter } from './avoid-router';
import { dia, shapes, util } from '@joint/core';

const routerLoaded = AvoidRouter.load();

onmessage = async(e) => {

await routerLoaded;

const [{ command, ...data }] = e.data;
switch (command) {
case 'reset': {
const { json } = data;
graph.resetCells(data.cells, { fromBrowser: true });
router.routeAll();
break;
}
case 'change': {
const { cell } = data;
const model = graph.getCell(cell.id);
if (!model) {
console.error(`Cell with id ${cell.id} not found.`);
return;
}
if (model.isElement()) {
// A node was changed
model.set({
position: cell.position,
size: cell.size,
}, {
fromBrowser: true
});
} else {
// An edge was changed.
model.set({
source: cell.source,
target: cell.target
}, {
fromBrowser: true
});
}
break;
}
case 'remove': {
const { id } = data;
const model = graph.getCell(id);
if (!model) break;
model.remove({ fromBrowser: true });
break;
}
case 'add': {
const { cell } = data;
graph.addCell(cell, { fromBrowser: true });
break;
}
default:
console.log('Unknown command', command);
break;
}
};

await routerLoaded;

const graph = new dia.Graph({}, {
cellNamespace: {
...shapes,
Node: shapes.standard.Rectangle,
Edge: shapes.standard.Link,
}
});

const router = new AvoidRouter(graph, {
shapeBufferDistance: 20,
idealNudgingDistance: 10,
portOverflow: 8,
});

let changed = {};

const debouncedProcessTransaction = util.debounce(() => {
router.avoidRouter.processTransaction();
setTimeout(() => {
if (debouncedProcessTransaction.pending()) return;
postMessage({
command: 'routed',
cells: Object.values(changed),
});
changed = {};
}, 0);
}, 100);

router.addGraphListeners();

graph.on('change', (cell, opt) => {
if (opt.fromBrowser) {
debouncedProcessTransaction();
return;
}
changed[cell.id] = cell.toJSON();
});

graph.on('reset', (collection, opt) => {
if (!opt.fromBrowser) return;
debouncedProcessTransaction();
});

graph.on('add', (cell, opt) => {
if (!opt.fromBrowser) return;
debouncedProcessTransaction();
});

graph.on('remove', (cell, opt) => {
delete changed[cell.id];
if (!opt.fromBrowser) return;
debouncedProcessTransaction();
});
14 changes: 13 additions & 1 deletion examples/libavoid/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ body {
width: 100vw;
height: 100vh;
margin: 0;
overflow: hidden;
overflow: scroll;
}

.canvas {
Expand All @@ -26,4 +26,16 @@ body {
stroke-dasharray: 5, 2;
}
}

.awaiting-update {
opacity: 0.3;
stroke-dasharray: 5, 5;
animation: dash 20s linear;
}

@keyframes dash {
to {
stroke-dashoffset: 1000;
}
}
}

0 comments on commit 5cdbedb

Please sign in to comment.