@woosh/meep-engine
Version:
Pure JavaScript game engine. Fully featured and production ready.
454 lines (335 loc) • 14.7 kB
JavaScript
import { clamp } from "../../../src/core/math/clamp.js";
import { lerp } from "../../../src/core/math/lerp.js";
import { max2 } from "../../../src/core/math/max2.js";
import { min2 } from "../../../src/core/math/min2.js";
import View from "../../../src/view/View.js";
import { AutoCanvasView } from "../ecs/components/common/AutoCanvasView.js";
import Vector2 from "../../../src/core/geom/Vector2.js";
import Vector1 from "../../../src/core/geom/Vector1.js";
import ListView from "../../../src/view/common/ListView.js";
import { NodeView } from "./NodeView.js";
import List from "../../../src/core/collection/list/List.js";
import { DraggableAspect } from "../../../src/engine/ui/DraggableAspect.js";
import { PortDirection } from "../../../src/core/model/node-graph/node/PortDirection.js";
import { NodeInstancePortReference } from "../../../src/core/model/node-graph/node/NodeInstancePortReference.js";
export class NodeGraphCamera {
constructor() {
this.position = new Vector2();
/**
*
* @type {Vector1}
*/
this.scale = new Vector1(1);
}
}
const v2 = new Vector2();
const CONNECTION_WIDTH = 3;
const PORT_BEND_OFFSET_X = 100;
export class NodeGraphView extends View {
/**
*
* @param {NodeGraph} graph
* @param {NodeGraphVisualData} visual
* @param {NodeGraphCamera} camera
* @param {NodeDescriptionVisualRegistry} nodeVisualRegistry
*/
constructor({ graph, visual, camera, nodeVisualRegistry }) {
super();
/**
*
* @type {NodeGraphCamera}
*/
this.camera = camera;
/**
*
* @type {NodeGraph}
*/
this.graph = graph;
/**
*
* @type {NodeGraphVisualData}
*/
this.visual = visual;
/**
*
* @type {List<number>}
*/
this.selection = new List();
this.el = document.createElement('div');
this.addClass('ui-node-graph-view');
const tempConnection = {
enabled: false,
anchor: new Vector2(),
endpoint: null
};
const vBlockCanvas = new ListView(graph.nodes, {
classList: ['block-canvas'],
/**
*
* @param {NodeInstance} node
* @returns {View}
*/
elementFactory(node) {
let nodeVisualData = visual.getNode(node.id);
if (nodeVisualData === undefined) {
const vd0 = nodeVisualRegistry.get(node.description.id);
if (vd0 === undefined) {
throw new Error(`Node (name='${node.description.name}', id=${node.description.id}) not found in registry`);
}
const vd1 = vd0.clone();
vd1.id = node.id;
visual.addNode(node.id, vd1);
nodeVisualData = vd1;
}
const nodeView = new NodeView({
node,
visual: nodeVisualData,
visualData: visual,
portCreationCallback(portView, port) {
const draggableAspect = new DraggableAspect({
el: portView.el,
dragStart(p) {
const endpoint = new NodeInstancePortReference();
endpoint.set(node, port);
tempConnection.enabled = true;
tempConnection.endpoint = endpoint;
},
dragEnd() {
tempConnection.enabled = false;
vConnectionCanvas.render();
},
drag(p, o) {
const scale = camera.scale.getValue();
tempConnection.anchor.set(
(p.x / scale + camera.position.x),
(p.y / scale + camera.position.y)
);
vConnectionCanvas.render();
}
});
draggableAspect.getPointer().on.up.add(() => {
//pointer released above the port
if (tempConnection.enabled) {
/**
*
* @type {NodeInstancePortReference}
*/
const endpoint = tempConnection.endpoint;
const endpointPort = endpoint.port;
if (endpointPort === port) {
//can't connect port to itself
return;
}
if (endpointPort.direction === port.direction) {
//can't connect ports of teh same directionality
return;
}
const dataType = endpointPort.dataType;
//check that the port types match
if (port.dataType === dataType) {
if (endpointPort.direction === PortDirection.Out) {
graph.createConnection(endpoint.instance.id, endpointPort.id, node.id, port.id);
} else {
graph.createConnection(node.id, port.id, endpoint.instance.id, endpointPort.id);
}
}
}
});
portView.on.linked.add(draggableAspect.start, draggableAspect);
portView.on.unlinked.add(draggableAspect.stop, draggableAspect);
}
});
nodeView.bindSignal(nodeView.position.onChanged, vConnectionCanvas.render, vConnectionCanvas);
return nodeView;
}
});
this.addChild(vBlockCanvas);
//create canvas to draw connections on
const vConnectionCanvas = new AutoCanvasView({
classList: ['connection-canvas']
});
const v2_source = new Vector2();
const v2_target = new Vector2();
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param width
* @param height
*/
vConnectionCanvas.draw = (ctx, width, height) => {
ctx.clearRect(0, 0, width, height);
ctx.lineWidth = max2(1, CONNECTION_WIDTH * camera.scale.getValue());
ctx.lineCap = 'round';
//draw connections
graph.connections.forEach(connection => {
const source = connection.source;
const target = connection.target;
this.getEndpointGraphPosition(source, v2_source);
this.getEndpointGraphPosition(target, v2_target);
this.drawConnection(ctx, v2_source, v2_target, source.port.dataType);
});
//draw temp connection
if (tempConnection.enabled) {
/**
*
* @type {NodeInstancePortReference}
*/
const endpoint = tempConnection.endpoint;
this.getEndpointGraphPosition(endpoint, v2_source);
const dataType = endpoint.port.dataType;
if (endpoint.port.direction === PortDirection.Out) {
this.drawConnection(ctx, v2_source, tempConnection.anchor, dataType);
} else {
this.drawConnection(ctx, tempConnection.anchor, v2_source, dataType);
}
}
};
this.addChild(vConnectionCanvas);
this.size.onChanged.add((x, y) => {
vConnectionCanvas.size.set(x, y);
});
function onCameraChange() {
const scale = camera.scale.getValue();
const p = camera.position;
vBlockCanvas.scale.setScalar(scale);
vBlockCanvas.position.set(-p.x * scale, -p.y * scale);
vConnectionCanvas.render();
}
this.initializeNavigationControls();
this.on.linked.add(onCameraChange);
this.bindSignal(camera.position.onChanged, onCameraChange);
this.bindSignal(camera.scale.onChanged, onCameraChange);
}
initializeNavigationControls() {
const cameraPosition = new Vector2();
const camera = this.camera;
const canvasSize = this.size;
const draggableAspect = new DraggableAspect({
el: this.el,
dragStart() {
cameraPosition.copy(camera.position)
},
drag(p, o) {
const d = o.clone();
d.sub(p);
d.multiplyScalar(1 / camera.scale.getValue());
d.add(cameraPosition);
camera.position.copy(d);
}
});
draggableAspect.getPointer().on.wheel.add((delta, position) => {
const scaleDelta = -delta.y / 20;
const oldScale = camera.scale.getValue();
const v = (1 + scaleDelta) * oldScale;
const newScale = clamp(v, .03, 4);
const actualScaleDelta = newScale / oldScale - 1;
camera.scale.set(newScale);
const canvasSizeInGraphSpace = new Vector2();
canvasSizeInGraphSpace.copy(canvasSize);
canvasSizeInGraphSpace.divideScalar(newScale);
const selectionSizeDelta = new Vector2();
selectionSizeDelta.copy(canvasSizeInGraphSpace);
selectionSizeDelta.multiplyScalar(actualScaleDelta);
const graphCameraPosition = new Vector2();
this.transformPointGraph2Canvas(camera.position, graphCameraPosition);
const localOffset = position.clone().sub(graphCameraPosition);
const normalizedOffset = localOffset.clone();
normalizedOffset.divide(canvasSize);
const positionDelta = selectionSizeDelta.clone();
positionDelta._multiply(normalizedOffset.x, normalizedOffset.y);
camera.position.add(positionDelta);
});
this.on.linked.add(draggableAspect.start, draggableAspect);
this.on.unlinked.add(draggableAspect.stop, draggableAspect);
}
/**
*
* @param x0
* @param y0
* @param x1
* @param y1
* @returns {boolean}
*/
isGraphAABBVisible(x0, y0, x1, y1) {
const scale = this.camera.scale.getValue();
const cp = this.camera.position;
//transform graph-space to canvas-space
const _x0 = (x0 - cp.x) * scale;
const _y0 = (y0 - cp.y) * scale;
const _x1 = (x1 - cp.x) * scale;
const _y1 = (y1 - cp.y) * scale;
return _x0 < this.size.x && _x1 > 0 && _y0 < this.size.y && _y1 > 0;
}
/**
*
* @param {CanvasRenderingContext2D} ctx
* @param {Vector2} source
* @param {Vector2} target
* @param {BinaryDataType} dataType
*/
drawConnection(ctx, source, target, dataType) {
//check that the connection is visible
const cw_2 = (CONNECTION_WIDTH / 2);
const x0 = min2(source.x, target.x) - (cw_2 + PORT_BEND_OFFSET_X);
const y0 = min2(source.y, target.y) - cw_2;
const x1 = max2(source.x, target.x) + cw_2 + PORT_BEND_OFFSET_X;
const y1 = max2(source.y, target.y) + cw_2;
if (!this.isGraphAABBVisible(x0, y0, x1, y1)) {
//connection is not visible, don't draw
return;
}
const cameraScale = this.camera.scale.getValue();
const alpha = clamp(lerp(0, 1, cameraScale * CONNECTION_WIDTH), 0, 1);
const dataColor = this.visual.getDataColor(dataType.id);
ctx.strokeStyle = `rgba(${dataColor.r * 255}, ${dataColor.g * 255}, ${dataColor.b * 255}, ${alpha})`;
ctx.beginPath();
this.transformPointGraph2Canvas(source, v2);
ctx.moveTo(v2.x, v2.y);
const cp1x = v2.x + PORT_BEND_OFFSET_X * cameraScale;
const cp1y = v2.y;
this.transformPointGraph2Canvas(target, v2);
const cp2x = v2.x - PORT_BEND_OFFSET_X * cameraScale;
const cp2y = v2.y;
ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, v2.x, v2.y);
ctx.stroke();
}
/**
*
* @param {NodeInstancePortReference} endpoint
* @param {Vector2} output
*/
getEndpointGraphPosition(endpoint, output) {
const instance = endpoint.instance;
const nodeVisualData = this.visual.getNode(instance.id);
const nodePosition = nodeVisualData.dimensions.position;
const portVisualData = nodeVisualData.getPort(endpoint.port.id);
const portPosition = portVisualData.position;
const x = nodePosition.x + portPosition.x;
const y = nodePosition.y + portPosition.y;
output.set(x, y);
}
/**
* Convert point from Graph coordinate space to Canvas coordinate space
* @param {Vector2} input
* @param {Vector2} output
*/
transformPointGraph2Canvas(input, output) {
const scale = this.camera.scale.getValue();
const p = this.camera.position;
const x = (input.x - p.x) * scale;
const y = (input.y - p.y) * scale;
output.set(x, y);
}
/**
* Convert point from Canvas coordinate space to Graph coordinate space
* @param {Vector2} input
* @param {Vector2} output
*/
transformPointCanvas2Graph(input, output) {
const scale = this.camera.scale.getValue();
const p = this.camera.position;
const x = (input.x / scale) + p.x;
const y = (input.y / scale) + p.y;
output.set(x, y);
}
}