mirror of
https://github.com/rio-labs/rio.git
synced 2026-05-01 16:39:29 -05:00
graph editor progress
This commit is contained in:
@@ -31,7 +31,7 @@ export class DraggingConnectionStrategy {
|
||||
this.element = element;
|
||||
}
|
||||
|
||||
onDragMove(this_: GraphEditorComponent, event: PointerEvent): void {
|
||||
onDragMove(ge: GraphEditorComponent, event: PointerEvent): void {
|
||||
// Update the latent connection
|
||||
const fixedPortComponent = componentsById[this.fixedPortId] as
|
||||
| NodeInputComponent
|
||||
@@ -55,7 +55,7 @@ export class DraggingConnectionStrategy {
|
||||
updateConnectionFromCoordinates(this.element, x1, y1, x2, y2);
|
||||
}
|
||||
|
||||
onDragEnd(this_: GraphEditorComponent, event: PointerEvent): void {
|
||||
onDragEnd(ge: GraphEditorComponent, event: PointerEvent): void {
|
||||
// Remove the SVG path
|
||||
this.element.remove();
|
||||
|
||||
@@ -101,7 +101,7 @@ export class DraggingConnectionStrategy {
|
||||
|
||||
// Create a real connection between the two ports
|
||||
let connectionElement = makeConnectionElement();
|
||||
this_.svgChild.appendChild(connectionElement);
|
||||
ge.svgChild.appendChild(connectionElement);
|
||||
|
||||
let augmentedConn: AugmentedConnectionState = {
|
||||
fromNode: getNodeFromPort(fromPortComponent).id,
|
||||
@@ -111,7 +111,7 @@ export class DraggingConnectionStrategy {
|
||||
element: connectionElement,
|
||||
};
|
||||
|
||||
this_.graphStore.addConnection(augmentedConn);
|
||||
ge.graphStore.addConnection(augmentedConn);
|
||||
updateConnectionFromObject(augmentedConn);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,38 @@
|
||||
/// A connection that is only connected to a single port, with the other end
|
||||
|
||||
import { componentsByElement } from "../../componentManagement";
|
||||
import { ComponentId } from "../../dataModels";
|
||||
import { GraphEditorComponent } from "./graphEditor";
|
||||
import { NodeInputComponent } from "../nodeInput";
|
||||
import { NodeOutputComponent } from "../nodeOutput";
|
||||
import { pixelsPerRem } from "../../app";
|
||||
import { updateConnectionFromObject } from "./utils";
|
||||
|
||||
/// The user is moving around all selected nodes
|
||||
export class DraggingNodesStrategy {
|
||||
onDragMove(this_: GraphEditorComponent, event: PointerEvent): void {
|
||||
// Update the node state
|
||||
nodeState.left += event.movementX / pixelsPerRem;
|
||||
nodeState.top += event.movementY / pixelsPerRem;
|
||||
onDragMove(ge: GraphEditorComponent, event: PointerEvent): void {
|
||||
// Move all selected nodes
|
||||
let moveX = event.movementX / pixelsPerRem;
|
||||
let moveY = event.movementY / pixelsPerRem;
|
||||
|
||||
// Move its element
|
||||
nodeElement.style.left = `${nodeState.left}rem`;
|
||||
nodeElement.style.top = `${nodeState.top}rem`;
|
||||
for (let nodeState of ge.getSelectedNodes()) {
|
||||
// Update the stored position
|
||||
nodeState.left += moveX;
|
||||
nodeState.top += moveY;
|
||||
|
||||
// Move the HTML element
|
||||
nodeState.element.style.left = `${nodeState.left}rem`;
|
||||
nodeState.element.style.top = `${nodeState.top}rem`;
|
||||
}
|
||||
|
||||
// Update any connections
|
||||
let connections = this.graphStore.getConnectionsForNode(nodeState.id);
|
||||
|
||||
for (let conn of connections) {
|
||||
this._updateConnectionFromObject(conn);
|
||||
for (let nodeState of ge.getSelectedNodes()) {
|
||||
for (let connection of ge.graphStore.getConnectionsForNode(
|
||||
nodeState.id
|
||||
)) {
|
||||
updateConnectionFromObject(connection);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onDragEnd(this_: GraphEditorComponent, event: PointerEvent): void {
|
||||
// The node no longer needs to be on top
|
||||
nodeElement.style.removeProperty("z-index");
|
||||
|
||||
return false;
|
||||
onDragEnd(ge: GraphEditorComponent, event: PointerEvent): void {
|
||||
// The nodes no longer needs to be on top
|
||||
for (let nodeState of ge.getSelectedNodes()) {
|
||||
nodeState.element.style.removeProperty("z-index");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
/// A connection that is only connected to a single port, with the other end
|
||||
|
||||
import { componentsByElement } from "../../componentManagement";
|
||||
import { ComponentId } from "../../dataModels";
|
||||
import { GraphEditorComponent } from "./graphEditor";
|
||||
import { NodeInputComponent } from "../nodeInput";
|
||||
import { NodeOutputComponent } from "../nodeOutput";
|
||||
|
||||
/// The user is selecting nodes by dragging a rectangle
|
||||
export class DraggingSelectionStrategy {
|
||||
startPointX: number;
|
||||
startPointY: number;
|
||||
@@ -15,6 +10,70 @@ export class DraggingSelectionStrategy {
|
||||
this.startPointY = startPointY;
|
||||
}
|
||||
|
||||
onDragMove(this_: GraphEditorComponent, event: PointerEvent): void {}
|
||||
onDragEnd(this_: GraphEditorComponent, event: PointerEvent): void {}
|
||||
/// Returns the [left, top, width, height] of the selection rectangle,
|
||||
/// taking care that width and height are not negative.
|
||||
getSelectedRectangle(
|
||||
event: PointerEvent
|
||||
): [number, number, number, number] {
|
||||
let rectLeft = this.startPointX;
|
||||
let rectTop = this.startPointY;
|
||||
let rectWidth = event.clientX - this.startPointX;
|
||||
let rectHeight = event.clientY - this.startPointY;
|
||||
|
||||
// Avoid negative width and height
|
||||
if (rectWidth < 0) {
|
||||
rectLeft += rectWidth;
|
||||
rectWidth = -rectWidth;
|
||||
}
|
||||
|
||||
if (rectHeight < 0) {
|
||||
rectTop += rectHeight;
|
||||
rectHeight = -rectHeight;
|
||||
}
|
||||
|
||||
// Done
|
||||
return [rectLeft, rectTop, rectWidth, rectHeight];
|
||||
}
|
||||
|
||||
onDragMove(ge: GraphEditorComponent, event: PointerEvent): void {
|
||||
// Get the new selection rectangle
|
||||
let [rectLeft, rectTop, rectWidth, rectHeight] =
|
||||
this.getSelectedRectangle(event);
|
||||
|
||||
// Apply the new values
|
||||
ge.selectionRect.style.left = `${rectLeft}px`;
|
||||
ge.selectionRect.style.top = `${rectTop}px`;
|
||||
ge.selectionRect.style.width = `${rectWidth}px`;
|
||||
ge.selectionRect.style.height = `${rectHeight}px`;
|
||||
}
|
||||
|
||||
onDragEnd(ge: GraphEditorComponent, event: PointerEvent): void {
|
||||
// Hide the selection rectangle
|
||||
ge.selectionRect.style.opacity = "0";
|
||||
|
||||
// Get the new selection rectangle
|
||||
let [rectLeft, rectTop, rectWidth, rectHeight] =
|
||||
this.getSelectedRectangle(event);
|
||||
|
||||
let rectRight = rectLeft + rectWidth;
|
||||
let rectBottom = rectTop + rectHeight;
|
||||
|
||||
// Select all nodes that fall within the selection rectangle
|
||||
for (let node of ge.graphStore.allNodes()) {
|
||||
let nodeElement = node.element;
|
||||
|
||||
// Get the bounding box of the node
|
||||
let nodeBox = nodeElement.getBoundingClientRect();
|
||||
|
||||
// Check if the node is within the selection rectangle
|
||||
if (
|
||||
nodeBox.left >= rectLeft &&
|
||||
nodeBox.right <= rectRight &&
|
||||
nodeBox.top >= rectTop &&
|
||||
nodeBox.bottom <= rectBottom
|
||||
) {
|
||||
ge.selectNode(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,11 +14,13 @@ import { DraggingSelectionStrategy } from "./draggingSelectionStrategy";
|
||||
import { DraggingNodesStrategy } from "./draggingNodesStrategy";
|
||||
import {
|
||||
devel_getComponentByKey,
|
||||
getNodeComponentFromElement,
|
||||
getNodeFromPort,
|
||||
getPortFromCircle,
|
||||
makeConnectionElement,
|
||||
updateConnectionFromObject,
|
||||
} from "./utils";
|
||||
import { componentsByElement, componentsById } from "../../componentManagement";
|
||||
|
||||
export type GraphEditorState = ComponentState & {
|
||||
_type_: "GraphEditor-builtin";
|
||||
@@ -31,13 +33,18 @@ export class GraphEditorComponent extends ComponentBase {
|
||||
private htmlChild: HTMLElement;
|
||||
public svgChild: SVGSVGElement;
|
||||
|
||||
private selectionChild: HTMLElement;
|
||||
public selectionRect: HTMLElement;
|
||||
|
||||
public graphStore: GraphStore = new GraphStore();
|
||||
|
||||
// When clicking & dragging a variety of things can happen based on the
|
||||
// selection, mouse button, position and phase of the moon. This strategy
|
||||
// object is used in lieu of if-else chains.
|
||||
/// All currently selected nodes, if any. This is intentionally private,
|
||||
/// because adding & removing nodes also needs to update their styles
|
||||
/// accordingly.
|
||||
private selectedNodes: Set<ComponentId> = new Set();
|
||||
|
||||
/// When clicking & dragging a variety of things can happen based on the
|
||||
/// selection, mouse button, position and phase of the moon. This strategy
|
||||
/// object is used in lieu of if-else chains.
|
||||
private dragStrategy:
|
||||
| DraggingConnectionStrategy
|
||||
| DraggingSelectionStrategy
|
||||
@@ -58,10 +65,10 @@ export class GraphEditorComponent extends ComponentBase {
|
||||
this.htmlChild = document.createElement("div");
|
||||
element.appendChild(this.htmlChild);
|
||||
|
||||
this.selectionChild = document.createElement("div");
|
||||
this.selectionChild.classList.add("rio-graph-editor-selection");
|
||||
this.selectionChild.style.opacity = "0";
|
||||
this.htmlChild.appendChild(this.selectionChild);
|
||||
this.selectionRect = document.createElement("div");
|
||||
this.selectionRect.classList.add("rio-graph-editor-selection");
|
||||
this.selectionRect.style.opacity = "0";
|
||||
this.htmlChild.appendChild(this.selectionRect);
|
||||
|
||||
// Listen for drag events. The exact nature of the drag event is
|
||||
// determined by the current drag strategy.
|
||||
@@ -158,12 +165,25 @@ export class GraphEditorComponent extends ComponentBase {
|
||||
}
|
||||
|
||||
// Case: Move around the selected nodes
|
||||
if (event.button === 0 && false) {
|
||||
if (
|
||||
event.button === 0 &&
|
||||
targetElement.classList.contains("rio-graph-editor-node-header")
|
||||
) {
|
||||
// Make sure this node is selected
|
||||
let nodeComponent = getNodeComponentFromElement(
|
||||
targetElement.parentElement!
|
||||
);
|
||||
let nodeState = this.graphStore.getNodeById(nodeComponent.id);
|
||||
this.selectNode(nodeState);
|
||||
|
||||
// Make sure all selected nodes are on top
|
||||
//
|
||||
// TODO
|
||||
//
|
||||
// nodeElement.style.zIndex = "1";
|
||||
for (let nodeId of this.selectedNodes) {
|
||||
let node = componentsById[nodeId];
|
||||
|
||||
if (node !== undefined) {
|
||||
node.element.style.zIndex = "1";
|
||||
}
|
||||
}
|
||||
|
||||
// Store the strategy
|
||||
this.dragStrategy = new DraggingNodesStrategy();
|
||||
@@ -173,7 +193,19 @@ export class GraphEditorComponent extends ComponentBase {
|
||||
}
|
||||
|
||||
// Case: Rectangle selection
|
||||
if (event.button === 0) {
|
||||
if (event.button === 0 && targetElement === this.htmlChild) {
|
||||
// Deselect any previously selected nodes
|
||||
for (let node of this.getSelectedNodes()) {
|
||||
this.deselectNode(node);
|
||||
}
|
||||
|
||||
// Initialize the selection rectangle
|
||||
this.selectionRect.style.opacity = "1";
|
||||
this.selectionRect.style.left = `${event.clientX}px`;
|
||||
this.selectionRect.style.top = `${event.clientY}px`;
|
||||
this.selectionRect.style.width = "0";
|
||||
this.selectionRect.style.height = "0";
|
||||
|
||||
// Store the strategy
|
||||
this.dragStrategy = new DraggingSelectionStrategy(
|
||||
event.clientX,
|
||||
@@ -223,7 +255,6 @@ export class GraphEditorComponent extends ComponentBase {
|
||||
): AugmentedNodeState {
|
||||
// Build the node HTML
|
||||
const nodeElement = document.createElement("div");
|
||||
nodeElement.dataset.nodeId = nodeState.id.toString();
|
||||
nodeElement.classList.add(
|
||||
"rio-graph-editor-node",
|
||||
"rio-switcheroo-neutral"
|
||||
@@ -252,4 +283,43 @@ export class GraphEditorComponent extends ComponentBase {
|
||||
|
||||
return augmentedNode;
|
||||
}
|
||||
|
||||
/// Adds the given node to the set of selected nodes and updates its style
|
||||
/// appropriately.
|
||||
///
|
||||
/// Does nothing if the node is already selected.
|
||||
public selectNode(node: AugmentedNodeState): void {
|
||||
// Add to the set of selected nodes
|
||||
this.selectedNodes.add(node.id);
|
||||
|
||||
// Update the CSS
|
||||
node.element.classList.add("rio-graph-editor-selected-node");
|
||||
}
|
||||
|
||||
/// Removes the given node from the set of selected nodes and updates its
|
||||
/// style appropriately.
|
||||
///
|
||||
/// Does nothing if the node is not selected.
|
||||
public deselectNode(node: AugmentedNodeState): void {
|
||||
// Remove from the set of selected nodes
|
||||
this.selectedNodes.delete(node.id);
|
||||
|
||||
// Update the CSS
|
||||
node.element.classList.remove("rio-graph-editor-selected-node");
|
||||
}
|
||||
|
||||
/// Returns an iterable over all selected nodes.
|
||||
public getSelectedNodes(): Iterable<AugmentedNodeState> {
|
||||
let result: AugmentedNodeState[] = [];
|
||||
|
||||
for (let nodeId of this.selectedNodes) {
|
||||
let node = this.graphStore.getNodeById(nodeId);
|
||||
|
||||
if (node !== undefined) {
|
||||
result.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,6 +64,25 @@ export function getNodeFromPort(
|
||||
return nodeComponent;
|
||||
}
|
||||
|
||||
/// Given the HTML element of a node, return the node's component by walking
|
||||
/// the DOM.
|
||||
export function getNodeComponentFromElement(
|
||||
nodeElement: HTMLElement
|
||||
): ComponentBase {
|
||||
console.assert(
|
||||
nodeElement.classList.contains("rio-graph-editor-node"),
|
||||
"Node element does not have the expected class"
|
||||
);
|
||||
|
||||
// The node body contains a Rio component. That component's ID is also the
|
||||
// node's ID.
|
||||
let componentElement = nodeElement.querySelector(
|
||||
".rio-component"
|
||||
) as HTMLElement;
|
||||
|
||||
return componentsByElement.get(componentElement) as ComponentBase;
|
||||
}
|
||||
|
||||
/// Creates a SVG path element representing a connection. Does not add it to
|
||||
/// the DOM.
|
||||
export function makeConnectionElement(): SVGPathElement {
|
||||
|
||||
Reference in New Issue
Block a user