First commit

This commit is contained in:
2025-12-25 10:36:24 +01:00
commit 17ce5ef2fa
7 changed files with 2961 additions and 0 deletions

778
GraphExplorer.js Normal file
View File

@@ -0,0 +1,778 @@
if( typeof document != "undefined" ) {
document.nodes = { };
}
const colors = {
background: "#222",
gridLine: "#444",
connectionWire: "#00aaff",
nodeBackground: "#2d2d2d",
nodeBorder: "#777",
nodeNameBackground: "#2d2d2d",
nodeNameText: "#fff",
inputPortFill: "#aaa",
outputPortFill: "#00aaff",
};
export class Node {
name = "";
x = 0;
y = 0;
inputs = new Array();
outputs = new Array();
width = 0;
cornerRadius = 10;
constructor(name, x, y, inputs, outputs) {
this.name = name;
this.x = x;
this.y = y;
this.inputs = inputs;
this.outputs = outputs;
}
getHeight() {
return 40 + Math.max(this.inputs.length, this.outputs.length) * 20;
}
updateWidth(ctx) {
const textWidth = ctx.measureText(this.name).width;
this.width = Math.max(200, textWidth + 40);
}
roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.quadraticCurveTo(x + w, y, x + w, y + r);
ctx.lineTo(x + w, y + h - r);
ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h);
ctx.lineTo(x + r, y + h);
ctx.quadraticCurveTo(x, y + h, x, y + h - r);
ctx.lineTo(x, y + r);
ctx.quadraticCurveTo(x, y, x + r, y);
ctx.closePath();
}
draw( ctx ) {
const nodeHeight = this.getHeight();
if( typeof document != "undefined" ) {
document.nodes[ this.name ] = { x:this.x, y: this.y };
}
this.roundRect(ctx, this.x, this.y, this.width, nodeHeight, this.cornerRadius);
ctx.fillStyle = colors.nodeBackground;
ctx.fill();
ctx.strokeStyle = colors.nodeBorder;
ctx.lineWidth = 2;
this.roundRect(ctx, this.x, this.y, this.width, nodeHeight, this.cornerRadius);
ctx.stroke();
ctx.fillStyle = colors.nodeNameText;
ctx.fillText(this.name, this.x + 20, this.y + 20);
ctx.fillStyle = colors.inputPortFill;
for (let i = 0; i < this.inputs.length; i++) {
const pos = this.getInputPos(i);
ctx.beginPath();
ctx.arc(pos.x, pos.y, 6, 0, Math.PI * 2);
ctx.fill();
ctx.fillText(this.inputs[i], pos.x + 12, pos.y);
}
ctx.fillStyle = colors.outputPortFill;
for (let i = 0; i < this.outputs.length; i++) {
const pos = this.getOutputPos(i);
ctx.beginPath();
ctx.arc(pos.x, pos.y, 6, 0, Math.PI * 2);
ctx.fill();
const label = this.outputs[i];
const labelWidth = ctx.measureText(label).width;
ctx.fillText(label, pos.x - 12 - labelWidth, pos.y);
}
}
getInputPos(index) {
return { x: this.x, y: this.y + 40 + index * 20 };
}
getOutputPos(index) {
return { x: this.x + this.width, y: this.y + 40 + index * 20 };
}
containsPoint(x, y) {
const nodeHeight = this.getHeight();
return x >= this.x && x <= this.x + this.width && y >= this.y && y <= this.y + nodeHeight;
}
}
export class Edge {
fromNode = null;
fromIndex = 0;
toNode = null;
toIndex = 0;
toPos = null;
constructor(fromNode, fromIndex, toNode = null, toIndex = null, toPos = null) {
this.fromNode = fromNode;
this.fromIndex = fromIndex;
this.toNode = toNode;
this.toIndex = toIndex;
this.toPos = toPos;
}
draw(ctx) {
const fromPos = this.fromNode.getOutputPos(this.fromIndex);
let toPos;
if (this.toNode !== null && this.toIndex !== null) {
toPos = this.toNode.getInputPos(this.toIndex);
} else if (this.toPos !== null) {
toPos = this.toPos;
} else {
return;
}
const cp1 = { x: fromPos.x + 50, y: fromPos.y };
const cp2 = { x: toPos.x - 50, y: toPos.y };
ctx.beginPath();
ctx.moveTo(fromPos.x, fromPos.y);
ctx.bezierCurveTo(cp1.x, cp1.y, cp2.x, cp2.y, toPos.x, toPos.y);
ctx.stroke();
}
distance(a, b) {
return Math.hypot(a.x - b.x, a.y - b.y);
}
pointNear(pt, tolerance) {
const fromPos = this.fromNode.getOutputPos(this.fromIndex);
let toPos;
if (this.toNode !== null && this.toIndex !== null) {
toPos = this.toNode.getInputPos(this.toIndex);
} else if (this.toPos !== null) {
toPos = this.toPos;
} else {
return false;
}
const cp1 = { x: fromPos.x + 50, y: fromPos.y };
const cp2 = { x: toPos.x - 50, y: toPos.y };
const samples = 20;
for (let i = 0; i <= samples; i++) {
const t = i / samples;
const x =
Math.pow(1 - t, 3) * fromPos.x +
3 * Math.pow(1 - t, 2) * t * cp1.x +
3 * (1 - t) * Math.pow(t, 2) * cp2.x +
Math.pow(t, 3) * toPos.x;
const y =
Math.pow(1 - t, 3) * fromPos.y +
3 * Math.pow(1 - t, 2) * t * cp1.y +
3 * (1 - t) * Math.pow(t, 2) * cp2.y +
Math.pow(t, 3) * toPos.y;
if (this.distance(pt, { x, y }) < tolerance) {
return true;
}
}
return false;
}
}
export class GraphExplorer {
isPanning = false;
dragNode = null;
isDrawScheduled = false;
dragOffsetX = 0;
dragOffsetY = 0;
nodes = new Array();
edges = new Array();
dragWire = null;
lastMousePos = null;
panX = 40;
panY = 20;
panStartMouse = null;
panStartOffsetX = 0;
panStartOffsetY = 0;
// Inertia variables
velocityX = 0;
velocityY = 0;
friction = 0.93;
velocityThreshold = 0.1;
zoom = 1.;
minZoom = 0.1;
maxZoom = 4;
canvas;
context;
requestDraw() {
if ( this.isDrawScheduled ) {
return;
}
this.isDrawScheduled = true;
requestAnimationFrame(() => {
this.isDrawScheduled = false;
this.draw();
});
}
draw( clear = true ) {
this.context.setTransform(1, 0, 0, 1, 0, 0);
if( clear ){
this.context.clearRect(0, 0, this.width, this.height);
}
this.context.save();
this.context.translate(this.panX, this.panY);
this.context.scale(this.zoom, this.zoom);
this.drawGrid(this.context);
for (const node of this.nodes) {
node.updateWidth(this.context);
node.draw(this.context);
}
for (const edge of this.edges) {
edge.draw(this.context);
}
this.context.restore();
}
screenToWorld(x, y) {
return {
x: (x - this.panX) / this.zoom,
y: (y - this.panY) / this.zoom,
};
}
worldToScreen(x, y) {
return {
x: x * this.zoom + this.panX,
y: y * this.zoom + this.panY,
};
}
getNodeAtPoint(x, y) {
const pt = this.screenToWorld(x, y);
for (let i = this.nodes.length - 1; i >= 0; i--) {
if ( this.nodes[i].containsPoint(pt.x, pt.y) ) {
return this.nodes[i];
}
}
return null;
}
getWireAtPoint(x, y) {
const pt = this.screenToWorld(x, y);
for (let i = this.edges.length - 1; i >= 0; i--) {
if (this.edges[i].pointNear(pt, 10 / this.zoom)) {
return this.edges[i];
}
}
return null;
}
drawGrid( context ) {
const gridSpacing = 50;
const panWorldX = -this.panX / this.zoom;
const panWorldY = -this.panY / this.zoom;
const widthWorld = this.context.canvas.width / this.zoom;
const heightWorld = this.context.canvas.height / this.zoom;
const startX = Math.floor(panWorldX / gridSpacing) * gridSpacing;
const endX = panWorldX + widthWorld;
const startY = Math.floor(panWorldY / gridSpacing) * gridSpacing;
const endY = panWorldY + heightWorld;
this.context.strokeStyle = "#282727";
this.context.lineWidth = 1 / this.zoom;
this.context.beginPath();
// Draw vertical lines within viewport
for (let x = startX; x <= endX; x += gridSpacing) {
this.context.moveTo(x, panWorldY);
this.context.lineTo(x, panWorldY + heightWorld);
}
// Draw horizontal lines within viewport
for (let y = startY; y <= endY; y += gridSpacing) {
this.context.moveTo(panWorldX, y);
this.context.lineTo(panWorldX + widthWorld, y);
}
this.context.stroke();
}
getMousePos( e ) {
const rect = this.canvas.getBoundingClientRect();
return {
x: (e.clientX - rect.left),
y: (e.clientY - rect.top),
};
}
setup( canvas ) {
this.canvas = canvas;
this.context = canvas.getContext("2d");
this.canvas.addEventListener("mousedown", this.onMouseDown.bind( this ) );
this.canvas.addEventListener("mousemove", this.onMouseMove.bind( this ) );
this.canvas.addEventListener("mouseup", this.onMouseUp.bind( this ) );
this.canvas.addEventListener("contextmenu", this.onContextMenu.bind( this ) );
window.addEventListener("resize", this.onResize.bind( this ) );
document.addEventListener("click", this.onClick.bind( this ) );
document.getElementById("deleteNodeBtn").addEventListener("click", this.deleteNodeClick.bind(this) );
document.getElementById("deleteWireBtn").addEventListener("click", this.deleteWireButton.bind( this ) );
canvas.addEventListener("wheel", this.onWheel.bind( this ) );
}
async wait( milliseconds ) {
return new Promise(function( resolve ) {
setTimeout( resolve, milliseconds );
});
}
async onResize( windowWidth, windowHeight ) {
const devicePixelRatio = window.devicePixelRatio || 1;
this.width = windowWidth;
this.height = windowHeight;
this.canvas.width = this.width * devicePixelRatio;
this.canvas.height = this.height * devicePixelRatio;
this.canvas.style.width = this.width + "px";
this.canvas.style.height = this.height + "px";
this.context.setTransform( 1, 0, 0, 1, 0, 0 );
this.context.scale( devicePixelRatio, devicePixelRatio );
//await this.wait(100)
this.draw( false );
}
onMouseDown( e ) {
const pos = this.getMousePos(e);
const worldPos = this.screenToWorld( pos.x, pos.y );
if (e.button === 0) {
const node = this.getNodeAtPoint( pos.x, pos.y );
if (node) {
// Check if clicked on output port to start wire drag
for (let i = 0; i < node.outputs.length; i++) {
const outputPos = node.getOutputPos(i);
const dist = Math.hypot(worldPos.x - outputPos.x, worldPos.y - outputPos.y);
if (dist < 10) { // 10 is port radius threshold
this.dragWire = new Edge(node, i, null, null, worldPos);
this.edges.push(this.dragWire);
this.requestDraw();
return; // Early return to prevent node dragging start
}
}
// Otherwise start dragging node
this.dragNode = node;
this.dragOffsetX = worldPos.x - node.x;
this.dragOffsetY = worldPos.y - node.y;
this.velocityX = 0;
this.velocityY = 0;
} else {
// Start panning canvas if clicked empty background
this.isPanning = true;
this.panStartMouse = { x: pos.x, y: pos.y };
this.panStartOffsetX = this.panX;
this.panStartOffsetY = this.panY;
this.velocityX = 0;
this.velocityY = 0;
}
}
this.lastMousePos = pos;
this.requestDraw();
}
onMouseMove( e ) {
console.log("test");
const pos = this.getMousePos(e);
const worldPos = this.screenToWorld(pos.x, pos.y);
if (this.dragWire) {
// Update wire temporary end point in world coordinates
this.dragWire.toPos = worldPos;
this.requestDraw();
} else if (this.dragNode) {
const prevX = this.dragNode.x;
const prevY = this.dragNode.y;
this.dragNode.x = worldPos.x - this.dragOffsetX;
this.dragNode.y = worldPos.y - this.dragOffsetY;
this.velocityX = (this.dragNode.x - prevX) / (e.movementX || 1);
this.velocityY = (this.dragNode.y - prevY) / (e.movementY || 1);
this.requestDraw();
} else if (this.isPanning) {
const dx = pos.x - this.panStartMouse.x;
const dy = pos.y - this.panStartMouse.y;
const prevPanX = this.panX;
const prevPanY = this.panY;
this.panX = this.panStartOffsetX + dx;
this.panY = this.panStartOffsetY + dy;
this.velocityX = this.panX - prevPanX;
this.velocityY = this.panY - prevPanY;
this.requestDraw();
}
this.lastMousePos = pos;
}
onMouseUp( e ) {
if ( e.button === 0) {
if ( this.dragWire ) {
const pos = this.getMousePos( e );
const worldPos = this.screenToWorld( pos.x, pos.y );
// Try to attach to input port of a node
let connected = false;
for (const node of this.nodes) {
for (let i = 0; i < node.inputs.length; i++) {
const inputPos = node.getInputPos( i );
const dist = Math.hypot( worldPos.x - inputPos.x, worldPos.y - inputPos.y );
if (dist < 10) {
// Attach wire end to this input port
this.dragWire.toNode = node;
this.dragWire.toIndex = i;
this.dragWire.toPos = null;
connected = true;
break;
}
}
if (connected) break;
}
if (!connected) {
// Remove the wire if not connected
const idx = this.edges.indexOf(this.dragWire);
if (idx >= 0) this.edges.splice(idx, 1);
}
this.dragWire = null;
this.requestDraw();
} else if (this.dragNode) {
this.dragNode = null;
} else if (this.isPanning) {
this.isPanning = false;
}
}
}
onContextMenu(e) {
const wireMenu = document.getElementById("wireMenu");
const nodeMenu = document.getElementById("nodeMenu");
e.preventDefault();
const pos = this.getMousePos( e );
const worldPos = this.screenToWorld( pos.x, pos.y );
// Hide both menus by default
wireMenu.style.display = "none";
nodeMenu.style.display = "none";
const node = this.getNodeAtPoint( pos.x, pos.y );
if (node) {
// Show node context menu
nodeMenu.style.left = e.clientX + "px";
nodeMenu.style.top = e.clientY + "px";
nodeMenu.style.display = "block";
// Store clicked node reference for later use (e.g., deletion)
nodeMenu.currentNode = node;
} else {
const wire = this.getWireAtPoint( pos.x, pos.y );
if (wire) {
// Show wire context menu
wireMenu.style.left = e.clientX + "px";
wireMenu.style.top = e.clientY + "px";
wireMenu.style.display = "block";
// Store clicked wire reference for later use
wireMenu.currentWire = wire;
}
}
}
onClick( e ) {
const wireMenu = document.getElementById("wireMenu");
const nodeMenu = document.getElementById("nodeMenu");
// Hide context menus on any click outside them
if (e.target !== nodeMenu && !nodeMenu.contains(e.target)) {
nodeMenu.style.display = "none";
nodeMenu.currentNode = null;
}
if (e.target !== wireMenu && !wireMenu.contains(e.target)) {
wireMenu.style.display = "none";
wireMenu.currentWire = null;
}
}
deleteNodeClick() {
const wireMenu = document.getElementById("wireMenu");
const nodeMenu = document.getElementById("nodeMenu");
if (nodeMenu.currentNode) {
const index = this.nodes.indexOf(nodeMenu.currentNode);
if (index !== -1) {
// Remove edges connected to this node
for (let i = this.edges.length - 1; i >= 0; i--) {
if (this.edges[i].fromNode === nodeMenu.currentNode || this.edges[i].toNode === nodeMenu.currentNode) {
this.edges.splice(i, 1);
}
}
this.nodes.splice(index, 1);
}
nodeMenu.style.display = "none";
nodeMenu.currentNode = null;
this.requestDraw();
}
}
deleteWireButton() {
const wireMenu = document.getElementById("wireMenu");
if (wireMenu.currentWire) {
const index = this.edges.indexOf(wireMenu.currentWire);
if (index !== -1) {
this.edges.splice(index, 1);
}
wireMenu.style.display = "none";
wireMenu.currentWire = null;
this.requestDraw();
}
}
onWheel( event ) {
event.preventDefault();
const zoomFactor = event.deltaY < 0 ? 1.1 : 0.9;
this.zoomAtCenter( zoomFactor, this.canvas.width, this.canvas.height);
this.requestDraw();
}
zoomAtCenter( zoomDelta, canvasWidth, canvasHeight ) {
const oldZoom = this.zoom;
const centerX = canvasWidth / 2;
const centerY = canvasHeight / 2;
// Clamp new this.zoom
this.zoom = Math.min(this.maxZoom, Math.max(this.minZoom, this.zoom * zoomDelta));
// Adjust pan so this.zoom happens at canvas center (keep world pos under center fixed)
this.panX = centerX - (centerX - this.panX) * (this.zoom / oldZoom);
this.panY = centerY - (centerY - this.panY) * (this.zoom / oldZoom);
}
inertiaLoop() {
if (!this.isPanning && !this.dragNode) {
// Apply this.friction
this.velocityX *= this.friction;
this.velocityY *= this.friction;
this.panX += this.velocityX;
this.panY += this.velocityY;
if (Math.abs(this.velocityX) > this.velocityThreshold || Math.abs(this.velocityY) > this.velocityThreshold) {
this.draw();
requestAnimationFrame(this.inertiaLoop.bind(this));
}
}
else {
requestAnimationFrame(this.inertiaLoop.bind(this));
}
}
addNode( node ) {
this.nodes.push( node );
}
addEdge( edge ) {
this.edges.push( edge );
}
}

518
WindowController.js Normal file
View File

@@ -0,0 +1,518 @@
var orderIndex = 1000;
export class WindowController {
win = null;
titlebar = null;
canvas = null;
resizers = null;
btnClose = null;
btnMaximize = null;
isSnapped = false;
isMaximized = false;
isDragging = false;
dragType = null;
currentResizer = null;
savedPosition = null;
savedBeforeMaximize = null;
order = orderIndex++;
startX = 0;
startY = 0;
startLeft = 100;
startTop = 100;
startWidth = 500;
startHeight = 500;
width = this.startWidth;
height = this.startHeight;
constructor() {
}
setElement( element ) {
this.canvas = element;
console.log("set element",element);
}
create( element ) {
var windowFrame = document.createElement( "div" );
windowFrame.id = "win"
windowFrame.style.width = this.startWidth + "px";
windowFrame.style.height = this.startHeight + "px";
windowFrame.style.left = this.startLeft + "px";
windowFrame.style.top = this.startTop + "px";
// Title bar
var titleBar = document.createElement( "div" );
titleBar.className = "titlebar";
titleBar.textContent = "Source Code Graph";
var buttons = document.createElement( "div" );
buttons.className = "buttons";
var btnMaximize = document.createElement( "button" );
btnMaximize.id = "btnMaximize";
btnMaximize.title = "Maximize";
btnMaximize.textContent = "m";
var btnClose = document.createElement( "button" );
btnClose.id = "btnClose";
btnClose.title = "Close";
btnClose.textContent = "×";
buttons.appendChild( btnMaximize );
buttons.appendChild( btnClose );
titleBar.appendChild( buttons );
windowFrame.appendChild( titleBar );
// Content
var content = document.createElement( "div" );
content.className = "content";
console.log(this.canvas);
content.appendChild( this.canvas );
windowFrame.appendChild( content );
// Resizers
var resizerClasses = [
"top-left", "top", "top-right", "right",
"bottom-right", "bottom", "bottom-left", "left"
];
this.resizers = new Array();
for (var i = 0; i < resizerClasses.length; i++) {
var resizer = document.createElement( "div" );
resizer.className = "resizer " + resizerClasses[i];
windowFrame.appendChild( resizer );
this.resizers.push(resizer)
}
this.win = windowFrame;
this.titlebar = titleBar;
this.btnClose = btnClose;
this.btnMaximize = btnMaximize;
this.titlebar.addEventListener("mousedown", this.onTitlebarMouseDown.bind(this));
this.resizers.forEach(this.bindResizer.bind(this));
this.btnClose.addEventListener("click", this.onCloseClick.bind(this));
this.btnMaximize.addEventListener("click", this.onMaximizeClick.bind(this));
document.body.appendChild( windowFrame );
this.win.addEventListener("mousedown", this.reOrder.bind( this ) );
}
setup() {
this.create();
}
setGraphExplorer( graphExplorer ) {
this.graphExplorer = graphExplorer;
}
saveWindowState() {
this.savedPosition = {
left: this.win.offsetLeft,
top: this.win.offsetTop,
width: this.win.offsetWidth,
height: this.win.offsetHeight
};
}
hide() {
this.win.style.display = "none"
}
show() {
this.win.style.display = "flex"
}
restoreWindowState() {
if (!this.savedPosition) return;
this.win.style.left = this.savedPosition.left + "px";
this.win.style.top = this.savedPosition.top + "px";
this.win.style.width = this.savedPosition.width + "px";
this.win.style.height = this.savedPosition.height + "px";
this.savedPosition = null;
this.isSnapped = false;
}
resizeCanvas() {
const style = getComputedStyle(this.win);
const width = parseInt(style.width);
const height = parseInt(style.height);
//this.canvas.width = width;
//this.canvas.height = height;
//this.canvas.style.width = width + "px";
//this.canvas.style.height = height + "px";
//console.log(this.graphExplorer);
if(this.graphExplorer)
this.graphExplorer.onResize( width, height );
//this.draw();
}
snapWindow(mouseX, mouseY) {
if (this.isMaximized) return;
const snapMargin = 30;
const vw = window.innerWidth;
const vh = window.innerHeight;
const nearLeft = mouseX <= snapMargin;
const nearRight = mouseX >= vw - snapMargin;
const nearTop = mouseY <= snapMargin;
const nearBottom = mouseY >= vh - snapMargin;
if (!this.isSnapped && (nearLeft || nearRight || nearTop || nearBottom)) {
this.saveWindowState();
this.isSnapped = true;
} else if (!nearLeft && !nearRight && !nearTop && !nearBottom && this.isSnapped) {
this.restoreWindowState();
return;
}
if (!this.isSnapped) return;
if (nearTop && nearLeft) {
this.setWindowRect(0, 0, vw / 2, vh / 2);
} else if (nearTop && nearRight) {
this.setWindowRect(vw / 2, 0, vw / 2, vh / 2);
} else if (nearBottom && nearLeft) {
this.setWindowRect(0, vh / 2, vw / 2, vh / 2);
} else if (nearBottom && nearRight) {
this.setWindowRect(vw / 2, vh / 2, vw / 2, vh / 2);
} else if (nearTop) {
this.setWindowRect(0, 0, vw, vh / 2);
} else if (nearBottom) {
this.setWindowRect(0, vh / 2, vw, vh / 2);
} else if (nearLeft) {
this.setWindowRect(0, 0, vw / 2, vh);
} else if (nearRight) {
this.setWindowRect(vw / 2, 0, vw / 2, vh);
}
this.resizeCanvas();
}
setWindowRect(left, top, width, height) {
this.win.style.left = Math.floor(left) + "px";
this.win.style.top = Math.floor(top) + "px";
this.win.style.width = Math.floor(width) + "px";
this.win.style.height = Math.floor(height) + "px";
}
onTitlebarMouseDown(event) {
event.preventDefault();
this.dragType = "move";
const offsetX = event.clientX - this.win.offsetLeft;
const offsetY = event.clientY - this.win.offsetTop;
if (this.isSnapped) {
this.restoreWindowState();
this.startX = event.clientX;
this.startY = event.clientY;
this.startLeft = event.clientX - 100;
this.startTop = event.clientY - offsetY;
this.win.style.left = this.startLeft + "px";
this.win.style.top = this.startTop + "px";
} else {
this.startX = event.clientX;
this.startY = event.clientY;
this.startLeft = this.win.offsetLeft;
this.startTop = this.win.offsetTop;
}
this.isDragging = true;
document.addEventListener("mousemove", this.onDragMove);
document.addEventListener("mouseup", this.onDragEnd);
}
onDragMove = (event) => {
event.preventDefault();
if (!this.isDragging) return;
const dx = event.clientX - this.startX;
const dy = event.clientY - this.startY;
if (this.dragType === "move") {
let newLeft = this.startLeft + dx;
let newTop = this.startTop + dy;
newLeft = Math.max(0, Math.min(window.innerWidth - this.win.offsetWidth, newLeft));
newTop = Math.max(0, Math.min(window.innerHeight - this.win.offsetHeight, newTop));
this.win.style.left = newLeft + "px";
this.win.style.top = newTop + "px";
this.snapWindow(event.clientX, event.clientY);
} else if (this.dragType === "resize") {
let newWidth = this.startWidth;
let newHeight = this.startHeight;
let newLeft = this.startLeft;
let newTop = this.startTop;
switch (this.currentResizer) {
case "top":
newHeight = this.startHeight - dy;
newTop = this.startTop + dy;
break;
case "bottom":
newHeight = this.startHeight + dy;
break;
case "left":
newWidth = this.startWidth - dx;
newLeft = this.startLeft + dx;
break;
case "right":
newWidth = this.startWidth + dx;
break;
case "top-left":
newWidth = this.startWidth - dx;
newLeft = this.startLeft + dx;
newHeight = this.startHeight - dy;
newTop = this.startTop + dy;
break;
case "top-right":
newWidth = this.startWidth + dx;
newHeight = this.startHeight - dy;
newTop = this.startTop + dy;
break;
case "bottom-left":
newWidth = this.startWidth - dx;
newLeft = this.startLeft + dx;
newHeight = this.startHeight + dy;
break;
case "bottom-right":
newWidth = this.startWidth + dx;
newHeight = this.startHeight + dy;
break;
}
if (newWidth > 100) {
this.win.style.width = newWidth + "px";
this.win.style.left = newLeft + "px";
}
if (newHeight > 100) {
this.win.style.height = newHeight + "px";
this.win.style.top = newTop + "px";
}
}
this.resizeCanvas();
};
onDragEnd = () => {
this.isDragging = false;
this.dragType = null;
this.currentResizer = null;
document.removeEventListener("mousemove", this.onDragMove);
document.removeEventListener("mouseup", this.onDragEnd);
};
reOrder() {
orderIndex++;
this.win.style['z-index'] = orderIndex;
console.log("this.win.style['z-index']", this.win.style['z-index']);
}
onResizerMouseDown(event) {
event.preventDefault();
this.startX = event.clientX;
this.startY = event.clientY;
this.startLeft = this.win.offsetLeft;
this.startTop = this.win.offsetTop;
this.startWidth = this.win.offsetWidth;
this.startHeight = this.win.offsetHeight;
this.isDragging = true;
this.dragType = "resize";
this.currentResizer = event.target.classList[1];
document.addEventListener("mousemove", this.onDragMove);
document.addEventListener("mouseup", this.onDragEnd);
}
bindResizer(resizerElement) {
resizerElement.addEventListener("mousedown", this.onResizerMouseDown.bind(this));
}
maximizeWindow() {
if (this.isMaximized) return;
this.savedBeforeMaximize = {
left: this.win.offsetLeft,
top: this.win.offsetTop,
width: this.win.offsetWidth,
height: this.win.offsetHeight
};
this.setWindowRect(0, 0, window.innerWidth, window.innerHeight);
this.resizeCanvas();
this.isMaximized = true;
this.isSnapped = false;
this.savedPosition = null;
}
restoreFromMaximize() {
if (!this.isMaximized || !this.savedBeforeMaximize) return;
this.setWindowRect(
this.savedBeforeMaximize.left,
this.savedBeforeMaximize.top,
this.savedBeforeMaximize.width,
this.savedBeforeMaximize.height
);
this.resizeCanvas();
this.isMaximized = false;
this.savedBeforeMaximize = null;
}
onMaximizeClick() {
if (this.isMaximized) {
this.restoreFromMaximize();
} else {
this.maximizeWindow();
}
}
onCloseClick() {
this.win.style.display = "none";
}
}

238
extract_nodes.js Normal file
View File

@@ -0,0 +1,238 @@
import { parse } from "@babel/parser";
import * as babelTraverse from "@babel/traverse";
import generator from "@babel/generator";
const generate = generator.default;
import Shader from "../framework/WebGpu.js";
import { readFile } from "fs/promises";
import { writeFile } from "fs/promises";
const traverse = babelTraverse.default.default;
async function loadFile( pathName ) {
const content = await readFile( pathName, "utf-8" );
return content;
}
async function saveToJsonFile( pathName, object ) {
const jsonContent = JSON.stringify( object, null, 4 );
await writeFile( pathName, jsonContent, "utf-8" );
}
function parseBindings( wgsl ) {
var bindings = [];
const regex = /@group\((\d+)\)\s+@binding\((\d+)\)\s+var<(\w+)(?:,\s*(\w+))?>\s+(\w+)\s*:\s*([^;]+);/g;
let match;
while ( ( match = regex.exec( wgsl ) ) !== null ) {
const group = Number( match[1] );
const binding = Number( match[2] );
const storageClass = match[3];
const access = match[4];
const varName = match[5];
let type;
if ( storageClass === "storage" ) {
type = access === "read" ? "read-only-storage" : "storage";
} else if ( storageClass === "uniform" ) {
type = "uniform";
} else {
type = "storage";
}
const varType = match[6];
bindings.push({ group, binding, type, varName, varType });
}
bindings.sort( ( a, b ) => a.binding - b.binding );
return bindings;
}
function extractShaderEdges(code) {
const shaderInstances = new Map();
const edges = [];
const ifStatements = [];
const definitions = [];
const ast = parse(code, {
sourceType: "module",
plugins: ["classProperties"]
});
traverse(ast, {
IfStatement(path) {
const { test, consequent, alternate, start, end } = path.node;
ifStatements.push({
test: code.slice(test.start, test.end),
consequent: code.slice(consequent.start, consequent.end),
alternate: alternate ? code.slice(alternate.start, alternate.end) : null,
start,
end
});
},
AssignmentExpression(path) {
const node = path.node;
if (node.left.type === "MemberExpression") {
const shaderName = node.left.property.name;
if (node.right.type === "NewExpression" && node.right.callee.name === "Shader") {
const args = node.right.arguments;
if (args.length >= 2 && args[1].type === "StringLiteral") {
const shaderPath = args[1].value;
// Store more precise location info for matching
shaderInstances.set(shaderName, {
name: shaderName,
path: shaderPath,
start: path.parentPath.node.start,
end: path.parentPath.node.end
});
}
}
}
},
CallExpression(path) {
const node = path.node;
if (node.callee.type === "MemberExpression") {
const object = node.callee.object;
const method = node.callee.property.name;
if (object.type === "MemberExpression" && object.object.type === "ThisExpression") {
const toShaderName = object.property.name;
if (method === "setBuffer" && node.arguments.length === 2) {
const bufferName = node.arguments[0].type === "StringLiteral"
? node.arguments[0].value
: null;
const arg = node.arguments[1];
if (
arg.type === "CallExpression" &&
arg.callee.type === "MemberExpression" &&
arg.callee.property.name === "getBuffer" &&
arg.callee.object.type === "MemberExpression" &&
arg.callee.object.object.type === "ThisExpression"
) {
const fromShaderName = arg.callee.object.property.name;
const fromBufferName = arg.arguments.length === 1 && arg.arguments[0].type === "StringLiteral"
? arg.arguments[0].value
: null;
edges.push({
from: fromShaderName,
to: toShaderName,
buffer: bufferName,
fromBuffer: fromBufferName
});
}
}
}
}
},
VariableDeclarator(path) {
const name = path.node.id.name;
const init = path.node.init ? generate(path.node.init).code : null;
definitions.push({
name,
init,
start: path.node.start,
end: path.node.end
});
},
ClassProperty(path) {
const name = path.node.key.name;
const init = path.node.value ? generate(path.node.value).code : null;
definitions.push({
name,
init,
start: path.node.start,
end: path.node.end
});
}
});
// Attach ifStatements to shaders they are declared within
for (const shader of shaderInstances.values()) {
for (const ifStmt of ifStatements) {
if (shader.start >= ifStmt.start && shader.end <= ifStmt.end) {
if (!shader.ifStatements) shader.ifStatements = [];
shader.ifStatements.push(ifStmt);
}
}
}
return {
shaders: Array.from(shaderInstances.values()),
edges: edges,
definitions: definitions
};
}
const code = await loadFile("../Graphics/demo.js");
const result = extractShaderEdges(code);
for (var i = 0; i < result.shaders.length; i++) {
var shaderInfo = result.shaders[i];
var source = await loadFile(shaderInfo.path);
var bindings = parseBindings(source);
shaderInfo.bindings = bindings;
}
await saveToJsonFile("nodes.json", result);
console.log(result);

291
index.html Normal file
View File

@@ -0,0 +1,291 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Live Snap Drag Window with Canvas</title>
</head>
<body>
<div class="top-panel">
<button id="graphExplorer">Graph Explorer</button>
<button id="resetParticlesButton">Reset Particles</button>
</div>
<div id="wireMenu" style="
position: absolute;
display: none;
background: #333;
color: #eee;
padding: 8px;
border-radius: 4px;
font-family: Arial, sans-serif;
font-size: 14px;
z-index: 1000;
box-shadow: 0 2px 6px rgba(0,0,0,0.8);
">
<button id="deleteWireBtn" style="background:#900; color:#fff; border:none; padding:4px 8px; cursor:pointer;">Delete Wire</button>
</div>
<!-- Node context menu -->
<div id="nodeMenu" style="
position: absolute;
display: none;
background: #333;
color: #eee;
padding: 8px;
border-radius: 4px;
font-family: Arial, sans-serif;
font-size: 14px;
z-index: 1000;
box-shadow: 0 2px 6px rgba(0,0,0,0.8);
">
<button id="deleteNodeBtn" style="background:#900; color:#fff; border:none; padding:4px 8px; cursor:pointer;">Delete Node</button>
</div>
<link rel="stylesheet" href="./style/main.css" >
<script type="module">
function autoLayoutNodes( graphExplorer ) {
const placedNodes = new Set();
const visited = new Set();
const levels = new Map();
function dfs( node, level ) {
if ( visited.has( node ) ) return;
visited.add( node );
// Assign the current level (depth) if its deeper
if ( !levels.has( node ) || levels.get( node ) < level ) {
levels.set( node, level );
}
// Traverse all outgoing edges
for ( var i = 0; i < graphExplorer.edges.length; i++ ) {
const edge = graphExplorer.edges[i];
if ( edge.fromNode === node ) {
dfs( edge.toNode, level + 1 );
}
}
}
// Start from nodes with no incoming edges
for ( var i = 0; i < graphExplorer.nodes.length; i++ ) {
const node = graphExplorer.nodes[i];
const hasIncoming = graphExplorer.edges.some( edge => edge.toNode === node );
if ( !hasIncoming ) {
dfs( node, 0 );
}
}
// Group nodes by level
const layers = {};
for ( const [node, level] of levels ) {
if ( !layers[level] ) layers[level] = [];
layers[level].push( node );
}
const xSpacing = 400;
const yBase = 150;
for ( const levelEntry of Object.entries( layers ) ) {
const levelStr = levelEntry[0];
const nodesAtLevel = levelEntry[1];
const level = Number( levelStr );
const spacing = yBase + Math.max(...nodesAtLevel.map(n => n.inputs.length)) * 25;
for ( let i = 0; i < nodesAtLevel.length; i++ ) {
const node = nodesAtLevel[i];
const xJitter = ( Math.random() - 0.5 ) * 80; // wider jitter horizontally
const yJitter = ( Math.random() - 0.5 ) * 80; // wider jitter vertically
node.x = level * xSpacing + xJitter;
node.y = i * spacing + yJitter;
}
}
}
async function loadJSON( pathName ) {
const response = await fetch( pathName );
if ( !response.ok ) {
throw new Error( `Failed to load shader: ${ pathName }` );
}
return await response.json();
}
function getShaderByName( shaders, shaderName ) {
for (var i = 0; i < shaders.length; i++) {
var shader = shaders[i]
if( shader.name == shaderName ) {
return shader;
}
}
}
function getPortIndex( bindings, variableName ) {
for (var i = 0; i < bindings.length; i++) {
var binding = bindings[i]
if( binding.varName == variableName ) {
return i;
}
}
}
import { Node } from "./GraphExplorer.js"
import { Edge } from "./GraphExplorer.js"
import { GraphExplorer } from "./GraphExplorer.js"
import { WindowController } from "./WindowController.js"
var graphExplorer = new GraphExplorer();
const windowController = new WindowController();
windowController.setGraphExplorer( graphExplorer );
var graphCanvas = document.createElement( "canvas" );
graphCanvas.id = "graphCanvas";
graphCanvas.width = windowController.startWidth;
graphCanvas.height = windowController.startHeight;
console.log("set element", graphCanvas);
await windowController.setElement( graphCanvas );
await windowController.setup();
windowController.maximizeWindow();
var nodes = await loadJSON("./nodes.json");
console.log(nodes);
var shaders = nodes.shaders;
for (var i = 0; i < shaders.length; i++) {
var shader = shaders[i]
var bindings = shader.bindings;
var newNode = new Node(shader.name, 50 + 240 * i, 100, [], []);
for (var j = 0; j < bindings.length; j++) {
var binding = bindings[j]
var varName = binding.varName;
newNode.inputs[j] = varName;
newNode.outputs[j] = varName;
}
shader.node = newNode;
graphExplorer.addNode( newNode );
}
var edges = nodes.edges;
for (var i = 0; i < edges.length; i++) {
var edge = edges[i]
var fromShader = getShaderByName( shaders, edge.from );
var toShader = getShaderByName( shaders, edge.to );
var fromPortIndex = getPortIndex( fromShader.bindings, edge.fromBuffer );
var toPortIndex = getPortIndex( toShader.bindings, edge.buffer );
var edge = new Edge( fromShader.node, fromPortIndex, toShader.node, toPortIndex );
graphExplorer.addEdge( edge );
//console.log(edge, shaders, edge.from, fromShader);
}
autoLayoutNodes( graphExplorer );
graphExplorer.setup( graphCanvas );
graphExplorer.inertiaLoop();
graphExplorer.draw();
document.querySelector("#graphExplorer").addEventListener("click", function () {
windowController.show();
});
windowController.resizeCanvas();
</script>
</body>
</html>

876
nodes.json Normal file
View File

@@ -0,0 +1,876 @@
{
"shaders": [
{
"name": "gravityShader",
"path": "../shaders/gravity.wgsl",
"start": 1750,
"end": 1826,
"bindings": [
{
"group": 0,
"binding": 0,
"type": "storage",
"varName": "positions",
"varType": "array<vec3<f32>>"
},
{
"group": 0,
"binding": 1,
"type": "storage",
"varName": "velocities",
"varType": "array<vec3<f32>>"
},
{
"group": 0,
"binding": 2,
"type": "storage",
"varName": "distances",
"varType": "array<f32>"
},
{
"group": 0,
"binding": 3,
"type": "storage",
"varName": "indices",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 4,
"type": "uniform",
"varName": "deltaTimeSeconds",
"varType": "f32"
},
{
"group": 0,
"binding": 5,
"type": "uniform",
"varName": "cameraPosition",
"varType": "vec3<f32>"
},
{
"group": 0,
"binding": 6,
"type": "uniform",
"varName": "updateDistancesAndIndices",
"varType": "u32"
},
{
"group": 0,
"binding": 7,
"type": "uniform",
"varName": "cellCount",
"varType": "u32"
},
{
"group": 0,
"binding": 8,
"type": "uniform",
"varName": "gravity",
"varType": "f32"
}
]
},
{
"name": "copyIndicesShader",
"path": "../shaders/copyBuffer.wgsl",
"start": 2067,
"end": 2149,
"bindings": [
{
"group": 0,
"binding": 0,
"type": "read-only-storage",
"varName": "indices",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 1,
"type": "storage",
"varName": "sortedIndices",
"varType": "array<u32>"
}
]
},
{
"name": "renderShader",
"path": "../shaders/points.wgsl",
"start": 2237,
"end": 2311,
"bindings": [
{
"group": 0,
"binding": 0,
"type": "read-only-storage",
"varName": "positions",
"varType": "array<Point>"
},
{
"group": 0,
"binding": 1,
"type": "read-only-storage",
"varName": "sortedIndices",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 2,
"type": "uniform",
"varName": "viewProjectionMatrix",
"varType": "mat4x4<f32>"
},
{
"group": 0,
"binding": 3,
"type": "uniform",
"varName": "cameraRight",
"varType": "BillboardAxis"
},
{
"group": 0,
"binding": 4,
"type": "uniform",
"varName": "cameraUp",
"varType": "BillboardAxis"
}
]
},
{
"name": "findGridHashShader",
"path": "../shaders/findGridHash.wgsl",
"start": 2785,
"end": 2869,
"bindings": [
{
"group": 0,
"binding": 0,
"type": "storage",
"varName": "positions",
"varType": "array<vec3<f32>>"
},
{
"group": 0,
"binding": 1,
"type": "storage",
"varName": "gridHashes",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 2,
"type": "storage",
"varName": "indices",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 3,
"type": "uniform",
"varName": "cellCount",
"varType": "u32"
},
{
"group": 0,
"binding": 4,
"type": "uniform",
"varName": "gridMin",
"varType": "vec3<f32>"
},
{
"group": 0,
"binding": 5,
"type": "uniform",
"varName": "gridMax",
"varType": "vec3<f32>"
}
]
},
{
"name": "localSortShader",
"path": "../shaders/localSort.wgsl",
"start": 3401,
"end": 3478,
"ifStatements": [
{
"test": "this.useLocalSort",
"consequent": "{\n\n\t\t\tthis.localSortShader = new Shader( this.device, \"../shaders/localSort.wgsl\");\n\n\t\t\tawait this.localSortShader.addStage(\"main\", GPUShaderStage.COMPUTE );\n\n\t\t\tthis.localSortShader.setBuffer( \"gridHashes\", this.findGridHashShader.getBuffer(\"gridHashes\" ) );\n\n\t\t\tthis.localSortShader.setBuffer( \"indices\", this.findGridHashShader.getBuffer(\"indices\" ) );\n\n\t\t\tthis.localSortShader.setVariable( \"totalCount\", this.particleCount );\n\n\t\t}",
"alternate": null,
"start": 3371,
"end": 3829
}
],
"bindings": [
{
"group": 0,
"binding": 0,
"type": "storage",
"varName": "gridHashes",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 1,
"type": "storage",
"varName": "indices",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 2,
"type": "uniform",
"varName": "totalCount",
"varType": "u32"
}
]
},
{
"name": "bitonicSortGridHashShader",
"path": "../shaders/bitonicSortUIntMultiPass.wgsl",
"start": 3833,
"end": 3935,
"bindings": [
{
"group": 0,
"binding": 0,
"type": "storage",
"varName": "gridHashes",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 1,
"type": "storage",
"varName": "threadPassIndices",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 2,
"type": "storage",
"varName": "kArray",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 3,
"type": "storage",
"varName": "jArray",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 4,
"type": "storage",
"varName": "indices",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 5,
"type": "uniform",
"varName": "totalCount",
"varType": "u32"
}
]
},
{
"name": "findGridHashRangeShader",
"path": "../shaders/findGridHashRanges.wgsl",
"start": 4714,
"end": 4809,
"bindings": [
{
"group": 0,
"binding": 0,
"type": "read-only-storage",
"varName": "gridHashes",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 1,
"type": "read-only-storage",
"varName": "indices",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 2,
"type": "storage",
"varName": "startIndices",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 3,
"type": "storage",
"varName": "endIndices",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 4,
"type": "uniform",
"varName": "totalCount",
"varType": "u32"
}
]
},
{
"name": "collisionDetectionShader",
"path": "../shaders/collisionDetection.wgsl",
"start": 4911,
"end": 5006,
"bindings": [
{
"group": 0,
"binding": 0,
"type": "storage",
"varName": "positions",
"varType": "array<vec3<f32>>"
},
{
"group": 0,
"binding": 1,
"type": "storage",
"varName": "velocities",
"varType": "array<vec3<f32>>"
},
{
"group": 0,
"binding": 2,
"type": "storage",
"varName": "gridHashes",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 3,
"type": "storage",
"varName": "hashSortedIndices",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 4,
"type": "uniform",
"varName": "cellCount",
"varType": "u32"
},
{
"group": 0,
"binding": 5,
"type": "uniform",
"varName": "gridMin",
"varType": "vec3<f32>"
},
{
"group": 0,
"binding": 6,
"type": "read-only-storage",
"varName": "startIndices",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 7,
"type": "read-only-storage",
"varName": "endIndices",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 8,
"type": "uniform",
"varName": "collisionRadius",
"varType": "f32"
},
{
"group": 0,
"binding": 9,
"type": "uniform",
"varName": "deltaTimeSeconds",
"varType": "f32"
},
{
"group": 0,
"binding": 10,
"type": "uniform",
"varName": "gridMax",
"varType": "vec3<f32>"
}
]
},
{
"name": "bitonicSortShader",
"path": "../shaders/bitonicSort.wgsl",
"start": 6436,
"end": 6516,
"bindings": [
{
"group": 0,
"binding": 0,
"type": "storage",
"varName": "compare",
"varType": "array<f32>"
},
{
"group": 0,
"binding": 1,
"type": "storage",
"varName": "indices",
"varType": "array<u32>"
},
{
"group": 0,
"binding": 2,
"type": "uniform",
"varName": "k",
"varType": "u32"
},
{
"group": 0,
"binding": 3,
"type": "uniform",
"varName": "j",
"varType": "u32"
},
{
"group": 0,
"binding": 4,
"type": "uniform",
"varName": "totalCount",
"varType": "u32"
}
]
}
],
"edges": [
{
"from": "gravityShader",
"to": "findGridHashShader",
"buffer": "positions",
"fromBuffer": "positions"
},
{
"from": "findGridHashShader",
"to": "localSortShader",
"buffer": "gridHashes",
"fromBuffer": "gridHashes"
},
{
"from": "findGridHashShader",
"to": "localSortShader",
"buffer": "indices",
"fromBuffer": "indices"
},
{
"from": "localSortShader",
"to": "bitonicSortGridHashShader",
"buffer": "gridHashes",
"fromBuffer": "gridHashes"
},
{
"from": "localSortShader",
"to": "bitonicSortGridHashShader",
"buffer": "indices",
"fromBuffer": "indices"
},
{
"from": "findGridHashShader",
"to": "bitonicSortGridHashShader",
"buffer": "gridHashes",
"fromBuffer": "gridHashes"
},
{
"from": "findGridHashShader",
"to": "bitonicSortGridHashShader",
"buffer": "indices",
"fromBuffer": "indices"
},
{
"from": "gravityShader",
"to": "collisionDetectionShader",
"buffer": "positions",
"fromBuffer": "positions"
},
{
"from": "gravityShader",
"to": "collisionDetectionShader",
"buffer": "velocities",
"fromBuffer": "velocities"
},
{
"from": "gravityShader",
"to": "collisionDetectionShader",
"buffer": "positions",
"fromBuffer": "positions"
},
{
"from": "gravityShader",
"to": "collisionDetectionShader",
"buffer": "velocities",
"fromBuffer": "velocities"
},
{
"from": "findGridHashShader",
"to": "collisionDetectionShader",
"buffer": "gridHashes",
"fromBuffer": "gridHashes"
},
{
"from": "localSortShader",
"to": "collisionDetectionShader",
"buffer": "hashSortedIndices",
"fromBuffer": "indices"
},
{
"from": "bitonicSortGridHashShader",
"to": "collisionDetectionShader",
"buffer": "hashSortedIndices",
"fromBuffer": "indices"
},
{
"from": "findGridHashRangeShader",
"to": "collisionDetectionShader",
"buffer": "startIndices",
"fromBuffer": "startIndices"
},
{
"from": "findGridHashRangeShader",
"to": "collisionDetectionShader",
"buffer": "endIndices",
"fromBuffer": "endIndices"
},
{
"from": "gravityShader",
"to": "bitonicSortShader",
"buffer": "compare",
"fromBuffer": "distances"
},
{
"from": "gravityShader",
"to": "bitonicSortShader",
"buffer": "indices",
"fromBuffer": "indices"
},
{
"from": "bitonicSortGridHashShader",
"to": "findGridHashRangeShader",
"buffer": "gridHashes",
"fromBuffer": "gridHashes"
},
{
"from": "bitonicSortGridHashShader",
"to": "findGridHashRangeShader",
"buffer": "indices",
"fromBuffer": "indices"
},
{
"from": "bitonicSortShader",
"to": "copyIndicesShader",
"buffer": "indices",
"fromBuffer": "indices"
},
{
"from": "gravityShader",
"to": "renderShader",
"buffer": "positions",
"fromBuffer": "positions"
},
{
"from": "copyIndicesShader",
"to": "renderShader",
"buffer": "sortedIndices",
"fromBuffer": "sortedIndices"
}
],
"definitions": [
{
"name": "canvas",
"init": null,
"start": 356,
"end": 363
},
{
"name": "device",
"init": null,
"start": 366,
"end": 373
},
{
"name": "camera",
"init": null,
"start": 376,
"end": 383
},
{
"name": "useLocalSort",
"init": "true",
"start": 386,
"end": 407
},
{
"name": "eventManager",
"init": "new EventManager()",
"start": 410,
"end": 445
},
{
"name": "frameCount",
"init": "0",
"start": 448,
"end": 466
},
{
"name": "context",
"init": "offscreenCanvas.getContext(\"webgpu\")",
"start": 692,
"end": 741
},
{
"name": "adapter",
"init": "await self.navigator.gpu.requestAdapter()",
"start": 881,
"end": 935
},
{
"name": "boundsMinimum",
"init": "new Vector3(-2, -2, -2)",
"start": 1108,
"end": 1151
},
{
"name": "boundsMaximum",
"init": "new Vector3(2, 2, 2)",
"start": 1160,
"end": 1199
},
{
"name": "cellsPerDimension",
"init": "12",
"start": 1208,
"end": 1231
},
{
"name": "gravity",
"init": "-2.3",
"start": 1240,
"end": 1257
},
{
"name": "jArray",
"init": "new Array()",
"start": 1377,
"end": 1401
},
{
"name": "kArray",
"init": "new Array()",
"start": 1410,
"end": 1434
},
{
"name": "startK",
"init": "this.workgroupSize * 2",
"start": 1473,
"end": 1504
},
{
"name": "startK",
"init": "2",
"start": 1563,
"end": 1573
},
{
"name": "k",
"init": "startK",
"start": 1595,
"end": 1605
},
{
"name": "j",
"init": "k >> 1",
"start": 1657,
"end": 1667
},
{
"name": "quadOffsets",
"init": "new Float32Array([\n// Triangle 1\n-1, -1,\n// bottom-left\n1, -1,\n// bottom-right\n1, 1,\n// top-right\n\n// Triangle 2\n-1, -1,\n// bottom-left\n1, 1,\n// top-right\n-1, 1 // top-left\n])",
"start": 2486,
"end": 2715
},
{
"name": "positions",
"init": "new Float32Array(this.particleCount * 3)",
"start": 7719,
"end": 7775
},
{
"name": "velocities",
"init": "new Float32Array(this.particleCount * 3)",
"start": 7786,
"end": 7843
},
{
"name": "i",
"init": "0",
"start": 7858,
"end": 7863
},
{
"name": "now",
"init": "performance.now()",
"start": 8329,
"end": 8355
},
{
"name": "commandEncoder",
"init": "this.device.createCommandEncoder()",
"start": 8488,
"end": 8539
},
{
"name": "passEncoder",
"init": "commandEncoder.beginComputePass()",
"start": 8606,
"end": 8653
},
{
"init": null,
"start": 8733,
"end": 8753
},
{
"name": "threadPassIndices",
"init": "new Uint32Array(this.particleCount)",
"start": 8986,
"end": 9044
},
{
"name": "i",
"init": "0",
"start": 9059,
"end": 9064
},
{
"name": "passEncoder",
"init": "commandEncoder.beginComputePass()",
"start": 9233,
"end": 9280
},
{
"init": null,
"start": 9370,
"end": 9390
},
{
"name": "startK",
"init": "this.workgroupSize * 2",
"start": 9546,
"end": 9577
},
{
"name": "startK",
"init": "2",
"start": 9601,
"end": 9611
},
{
"name": "k",
"init": "startK",
"start": 9632,
"end": 9642
},
{
"name": "j",
"init": "k >> 1",
"start": 9695,
"end": 9705
},
{
"name": "commandBuffer",
"init": "commandEncoder.finish()",
"start": 9839,
"end": 9878
},
{
"name": "k",
"init": "2",
"start": 10153,
"end": 10158
},
{
"name": "j",
"init": "k >> 1",
"start": 10209,
"end": 10219
},
{
"name": "commandBuffers",
"init": "new Array()",
"start": 10250,
"end": 10278
},
{
"name": "commandEncoder",
"init": "this.device.createCommandEncoder()",
"start": 10434,
"end": 10485
},
{
"name": "passEncoder",
"init": "commandEncoder.beginComputePass()",
"start": 10498,
"end": 10545
},
{
"init": null,
"start": 10629,
"end": 10649
},
{
"name": "viewMatrixData",
"init": "this.camera.getViewMatrix()",
"start": 11861,
"end": 11908
},
{
"name": "projectionMatrixData",
"init": "Matrix4.createProjectionMatrix(this.camera, this.canvas)",
"start": 11919,
"end": 12002
},
{
"name": "viewProjectionMatrix",
"init": "Matrix4.multiply(projectionMatrixData, viewMatrixData)",
"start": 12012,
"end": 12093
},
{
"name": "cameraWorldMatrix",
"init": "Matrix4.invert(viewMatrixData)",
"start": 12104,
"end": 12158
},
{
"name": "cameraRight",
"init": "Matrix4.getColumn(cameraWorldMatrix, 0)",
"start": 12171,
"end": 12229
},
{
"name": "cameraUp",
"init": "Matrix4.getColumn(cameraWorldMatrix, 1)",
"start": 12240,
"end": 12296
},
{
"name": "cameraPosition",
"init": "Matrix4.getColumn(cameraWorldMatrix, 3)",
"start": 12307,
"end": 12367
}
]
}

250
package-lock.json generated Normal file
View File

@@ -0,0 +1,250 @@
{
"name": "GraphExplorer",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"dependencies": {
"@babel/generator": "^7.27.5",
"@babel/parser": "^7.27.5",
"@babel/traverse": "^7.27.4",
"esprima": "^4.0.1",
"estraverse": "^5.3.0"
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/generator": {
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz",
"integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==",
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.27.5",
"@babel/types": "^7.27.3",
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25",
"jsesc": "^3.0.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.27.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz",
"integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==",
"license": "MIT",
"dependencies": {
"@babel/types": "^7.27.3"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.27.2",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.27.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz",
"integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==",
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.27.3",
"@babel/parser": "^7.27.4",
"@babel/template": "^7.27.2",
"@babel/types": "^7.27.3",
"debug": "^4.3.1",
"globals": "^11.1.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.27.6",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz",
"integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==",
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz",
"integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==",
"license": "MIT",
"dependencies": {
"@jridgewell/set-array": "^1.2.1",
"@jridgewell/sourcemap-codec": "^1.4.10",
"@jridgewell/trace-mapping": "^0.3.24"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/set-array": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
"integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
"integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.25",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
"integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/debug": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
"integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==",
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/esprima": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
"license": "BSD-2-Clause",
"bin": {
"esparse": "bin/esparse.js",
"esvalidate": "bin/esvalidate.js"
},
"engines": {
"node": ">=4"
}
},
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/globals": {
"version": "11.12.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz",
"integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
"node": ">=6"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"license": "ISC"
}
}
}

10
package.json Normal file
View File

@@ -0,0 +1,10 @@
{
"type": "module",
"dependencies": {
"@babel/generator": "^7.27.5",
"@babel/parser": "^7.27.5",
"@babel/traverse": "^7.27.4",
"esprima": "^4.0.1",
"estraverse": "^5.3.0"
}
}