Skip to content

Commit

Permalink
Further implementation and bug fixes
Browse files Browse the repository at this point in the history
rubenthoms committed Oct 6, 2023
1 parent 74edfd3 commit 9f4eaaa
Showing 10 changed files with 198 additions and 120 deletions.
2 changes: 2 additions & 0 deletions frontend/src/framework/GuiMessageBroker.ts
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ export enum GuiEvent {
HideDataChannelConnectionsRequest = "hideDataChannelConnectionsRequest",
HighlightDataChannelConnectionRequest = "highlightDataChannelConnectionRequest",
UnhighlightDataChannelConnectionRequest = "unhighlightDataChannelConnectionRequest",
DataChannelPointerUp = "dataChannelPointerUp",
DataChannelOriginPointerDown = "dataChannelOriginPointerDown",
DataChannelConnectionsChange = "dataChannelConnectionsChange",
DataChannelNodeHover = "dataChannelNodeHover",
@@ -62,6 +63,7 @@ export type GuiEventPayloads = {
moduleInstanceId: string;
originElement: HTMLElement;
};
[GuiEvent.DataChannelPointerUp]: {};
[GuiEvent.DataChannelNodeHover]: {
connectionAllowed: boolean;
};
45 changes: 43 additions & 2 deletions frontend/src/framework/internal/GlobalCursor.tsx
Original file line number Diff line number Diff line change
@@ -26,9 +26,50 @@ export class GlobalCursor {
this._cursorStack = [];
}

private getCursorClassName(cursorType: GlobalCursorType) {
switch (cursorType) {
case GlobalCursorType.Default:
return "cursor-default";
case GlobalCursorType.Copy:
return "cursor-copy";
case GlobalCursorType.Pointer:
return "cursor-pointer";
case GlobalCursorType.Grab:
return "cursor-grab";
case GlobalCursorType.Grabbing:
return "cursor-grabbing";
case GlobalCursorType.Crosshair:
return "cursor-crosshair";
case GlobalCursorType.Move:
return "cursor-move";
case GlobalCursorType.Text:
return "cursor-text";
case GlobalCursorType.Wait:
return "cursor-wait";
case GlobalCursorType.Help:
return "cursor-help";
case GlobalCursorType.Progress:
return "cursor-progress";
case GlobalCursorType.Cell:
return "cursor-cell";
case GlobalCursorType.VerticalText:
return "cursor-vertical-text";
case GlobalCursorType.Alias:
return "cursor-alias";
case GlobalCursorType.ContextMenu:
return "cursor-context-menu";
case GlobalCursorType.NoDrop:
return "cursor-no-drop";
case GlobalCursorType.NotAllowed:
return "cursor-not-allowed";
default:
return "cursor-default";
}
}

private updateBodyCursor() {
if (this._lastCursor !== GlobalCursorType.Default) {
const oldCursorClass = `cursor-${this._lastCursor}`;
const oldCursorClass = this.getCursorClassName(this._lastCursor);
document.body.classList.remove(oldCursorClass);
}

@@ -38,7 +79,7 @@ export class GlobalCursor {
}

const newCursor = this._cursorStack[this._cursorStack.length - 1];
const newCursorClass = `cursor-${newCursor}`;
const newCursorClass = this.getCursorClassName(newCursor);
document.body.classList.add(newCursorClass);
this._lastCursor = newCursor;
}
Original file line number Diff line number Diff line change
@@ -46,6 +46,11 @@ export const DataChannelVisualization: React.FC<DataChannelVisualizationProps> =

const guiMessageBroker = props.workbench.getGuiMessageBroker();

const [dataChannelConnectionsLayerVisible, setDataChannelConnectionsLayerVisible] = useGuiState(
guiMessageBroker,
GuiState.DataChannelConnectionLayerVisible
);

React.useEffect(() => {
forceRerender();
}, [boundingRect]);
@@ -66,6 +71,7 @@ export const DataChannelVisualization: React.FC<DataChannelVisualizationProps> =
mousePressed = true;
setCurrentPointerPosition(currentOriginPoint);
setCurrentChannelName(null);
setDataChannelConnectionsLayerVisible(true);

const moduleInstance = props.workbench.getModuleInstance(payload.moduleInstanceId);
if (!moduleInstance) {
@@ -84,6 +90,7 @@ export const DataChannelVisualization: React.FC<DataChannelVisualizationProps> =
setEditDataChannelConnectionsForModuleInstanceId(null);
setShowDataChannelConnections(false);
props.workbench.getGlobalCursor().restoreOverrideCursor();
setDataChannelConnectionsLayerVisible(false);
}

function handlePointerUp() {
@@ -121,9 +128,9 @@ export const DataChannelVisualization: React.FC<DataChannelVisualizationProps> =

function handleNodeHover(payload: GuiEventPayloads[GuiEvent.DataChannelNodeHover]) {
if (payload.connectionAllowed) {
props.workbench.getGlobalCursor().setOverrideCursor(GlobalCursorType.Copy);
props.workbench.getGlobalCursor().changeOverrideCursor(GlobalCursorType.Copy);
} else {
props.workbench.getGlobalCursor().setOverrideCursor(GlobalCursorType.NotAllowed);
props.workbench.getGlobalCursor().changeOverrideCursor(GlobalCursorType.NotAllowed);
}
}

@@ -170,6 +177,10 @@ export const DataChannelVisualization: React.FC<DataChannelVisualizationProps> =
GuiEvent.DataChannelOriginPointerDown,
handleDataChannelOriginPointerDown
);
const removeDataChannelPointerUpHandler = guiMessageBroker.subscribeToEvent(
GuiEvent.DataChannelPointerUp,
handlePointerUp
);
const removeDataChannelDoneHandler = guiMessageBroker.subscribeToEvent(
GuiEvent.HideDataChannelConnectionsRequest,
handleDataChannelDone
@@ -196,6 +207,7 @@ export const DataChannelVisualization: React.FC<DataChannelVisualizationProps> =
removeHighlightDataChannelConnectionRequestHandler();
removeUnhighlightDataChannelConnectionRequestHandler();
removeDataChannelOriginPointerDownHandler();
removeDataChannelPointerUpHandler();
removeDataChannelDoneHandler();
removeConnectionChangeHandler();
removeNodeHoverHandler();
@@ -209,7 +221,7 @@ export const DataChannelVisualization: React.FC<DataChannelVisualizationProps> =

React.useEffect(() => {
function handlePointerUp() {
if (!editDataChannelConnectionsForModuleInstanceId) {
if (!editDataChannelConnectionsForModuleInstanceId || dataChannelConnectionsLayerVisible) {
return;
}
setShowDataChannelConnections(false);
@@ -222,16 +234,24 @@ export const DataChannelVisualization: React.FC<DataChannelVisualizationProps> =
return () => {
document.removeEventListener("pointerup", handlePointerUp);
};
}, [editDataChannelConnectionsForModuleInstanceId]);
}, [editDataChannelConnectionsForModuleInstanceId, dataChannelConnectionsLayerVisible]);

let midPointY = (originPoint.y + currentPointerPosition.y) / 2;

if (currentPointerPosition.y < originPoint.y + 40 && currentPointerPosition.y > originPoint.y) {
midPointY = originPoint.y - 20;
} else if (currentPointerPosition.y > originPoint.y - 40 && currentPointerPosition.y < originPoint.y) {
midPointY = originPoint.y + 20;
}

const midPoint1: Point = {
x: originPoint.x,
y: (originPoint.y + currentPointerPosition.y) / 2,
y: midPointY,
};

const midPoint2: Point = {
x: currentPointerPosition.x,
y: (originPoint.y + currentPointerPosition.y) / 2,
y: midPointY,
};

function makeDataChannelPaths() {
@@ -344,7 +364,7 @@ export const DataChannelVisualization: React.FC<DataChannelVisualizationProps> =
</marker>
<marker
id="arrowhead-right-active"
fill="red"
fill="blue"
markerWidth="20"
markerHeight="14"
refX="0"
@@ -358,7 +378,7 @@ export const DataChannelVisualization: React.FC<DataChannelVisualizationProps> =
</marker>
<marker
id="arrowhead-left-active"
fill="red"
fill="blue"
markerWidth="20"
markerHeight="14"
refX="20"
@@ -417,15 +437,15 @@ export const DataChannelVisualization: React.FC<DataChannelVisualizationProps> =
<path
id={dataChannelPath.key}
d={`M ${dataChannelPath.origin.x} ${dataChannelPath.origin.y} C ${dataChannelPath.midPoint1.x} ${dataChannelPath.midPoint1.y} ${dataChannelPath.midPoint2.x} ${dataChannelPath.midPoint2.y} ${dataChannelPath.destination.x} ${dataChannelPath.destination.y}`}
stroke={dataChannelPath.highlighted ? "red" : "#aaa"}
stroke={dataChannelPath.highlighted ? "blue" : "#aaa"}
fill="transparent"
markerEnd={`url(#arrowhead-right${dataChannelPath.highlighted ? "-active" : ""})`}
/>
) : (
<path
id={dataChannelPath.key}
d={`M ${dataChannelPath.destination.x} ${dataChannelPath.destination.y} C ${dataChannelPath.midPoint2.x} ${dataChannelPath.midPoint2.y} ${dataChannelPath.midPoint1.x} ${dataChannelPath.midPoint1.y} ${dataChannelPath.origin.x} ${dataChannelPath.origin.y}`}
stroke={dataChannelPath.highlighted ? "red" : "#aaa"}
stroke={dataChannelPath.highlighted ? "blue" : "#aaa"}
fill="transparent"
markerStart={`url(#arrowhead-left${dataChannelPath.highlighted ? "-active" : ""})`}
/>
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import React from "react";
import { createPortal } from "react-dom";

import { XMarkIcon } from "@heroicons/react/20/solid";
import { Overlay } from "@lib/components/Overlay";
import { Point } from "@lib/utils/geometry";
import { Close } from "@mui/icons-material";

export type ChannelSelectorProps = {
channelNames: string[];
@@ -35,34 +36,38 @@ export const ChannelSelector: React.FC<ChannelSelectorProps> = (props) => {
}, [props.onCancel]);

return createPortal(
<div
id="channel-selector"
className="absolute bg-white max-h-52 border rounded overflow-auto z-50 shadow"
style={{
left: props.position.x < window.innerWidth / 2 ? props.position.x : undefined,
top: props.position.y < window.innerHeight / 2 ? props.position.y : undefined,
right: props.position.x > window.innerWidth / 2 ? window.innerWidth - props.position.x : undefined,
bottom: props.position.y > window.innerHeight / 2 ? window.innerHeight - props.position.y : undefined,
}}
>
<div id="channel-selector-header" className="p-2 bg-slate-200 font-bold text-xs flex">
<div className="flex-grow">Select a channel</div>
<div className="hover:text-slate-500 cursor-pointer" onClick={props.onCancel}>
<XMarkIcon className="w-4 h-4" />
<>
<Overlay visible />
<div
id="channel-selector"
className="absolute bg-white max-h-52 border rounded overflow-auto z-50 shadow"
style={{
left: props.position.x < window.innerWidth / 2 ? props.position.x : undefined,
top: props.position.y < window.innerHeight / 2 ? props.position.y : undefined,
right: props.position.x > window.innerWidth / 2 ? window.innerWidth - props.position.x : undefined,
bottom:
props.position.y > window.innerHeight / 2 ? window.innerHeight - props.position.y : undefined,
}}
>
<div id="channel-selector-header" className="p-2 bg-slate-200 font-bold text-xs flex">
<div className="flex-grow">Select a channel</div>
<div className="hover:text-slate-500 cursor-pointer" onClick={props.onCancel}>
<Close fontSize="small" />
</div>
</div>
{props.channelNames.map((channelName) => {
return (
<div
key={channelName}
className="p-2 hover:bg-blue-50 cursor-pointer text-xs"
onClick={() => props.onSelectChannel(channelName)}
>
{channelName}
</div>
);
})}
</div>
{props.channelNames.map((channelName) => {
return (
<div
key={channelName}
className="p-2 hover:bg-blue-50 cursor-pointer text-xs"
onClick={() => props.onSelectChannel(channelName)}
>
{channelName}
</div>
);
})}
</div>,
</>,
document.body
);
};
Original file line number Diff line number Diff line change
@@ -7,7 +7,7 @@ import { IconButton } from "@lib/components/IconButton";
import { useElementBoundingRect } from "@lib/hooks/useElementBoundingRect";
import { Point, pointerEventToPoint, rectContainsPoint } from "@lib/utils/geometry";
import { resolveClassNames } from "@lib/utils/resolveClassNames";
import { Close } from "@mui/icons-material";
import { Remove } from "@mui/icons-material";

export type InputChannelNodeProps = {
inputName: string;
@@ -21,6 +21,7 @@ export type InputChannelNodeProps = {

export const InputChannelNode: React.FC<InputChannelNodeProps> = (props) => {
const ref = React.useRef<HTMLDivElement>(null);
const removeButtonRef = React.useRef<HTMLButtonElement>(null);
const [connectable, setConnectable] = React.useState<boolean>(false);
const [hovered, setHovered] = React.useState<boolean>(false);
const [hasConnection, setHasConnection] = React.useState<boolean>(false);
@@ -112,41 +113,43 @@ export const InputChannelNode: React.FC<InputChannelNodeProps> = (props) => {
}

function handlePointerUp(e: PointerEvent) {
console.debug("pointer up", isHovered, isConnectable);
if (isHovered) {
if (isConnectable) {
if (removeButtonRef.current && removeButtonRef.current.contains(e.target as Node)) {
props.onChannelConnectionDisconnect(props.inputName);
setHovered(false);
isHovered = false;
} else if (isConnectable) {
props.onChannelConnect(props.inputName, moduleInstanceId, pointerEventToPoint(e));
} else {
guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {});
setHovered(false);
isHovered = false;
}
setHovered(false);
isHovered = false;
}
guiMessageBroker.publishEvent(GuiEvent.DataChannelPointerUp, {});
e.stopPropagation();
}

function handleEditDataChannelConnectionsRequest() {
setEditDataChannelConnections(true);
}

function handleDataChannelDone() {
setConnectable(false);
isConnectable = false;
setConnectable(false);
setHovered(false);
isHovered = false;
setEditDataChannelConnections(false);
console.debug("done");
}

function handlePointerMove(e: PointerEvent) {
const boundingRect = ref.current?.getBoundingClientRect();
if (boundingRect && rectContainsPoint(boundingRect, pointerEventToPoint(e))) {
setHovered(true);
isHovered = true;
console.debug("hovered");
return;
}
if (isHovered) {
setHovered(false);
isHovered = false;
console.debug("unhovered");
}
}

@@ -164,24 +167,19 @@ export const InputChannelNode: React.FC<InputChannelNodeProps> = (props) => {
handleEditDataChannelConnectionsRequest
);

document.addEventListener("pointerup", handlePointerUp);
ref.current?.addEventListener("pointerup", handlePointerUp, true);
document.addEventListener("pointermove", handlePointerMove);

return () => {
removeDataChannelDoneHandler();
removeDataChannelOriginPointerDownHandler();
removeShowDataChannelConnectionsRequestHandler();

document.removeEventListener("pointerup", handlePointerUp);
ref.current?.removeEventListener("pointerup", handlePointerUp);
document.removeEventListener("pointermove", handlePointerMove);
};
}, [props.onChannelConnect, props.workbench, props.moduleInstanceId, props.inputName, props.channelKeyCategories]);

function handleChannelConnectionRemoveClick(e: React.PointerEvent<HTMLButtonElement>) {
e.stopPropagation();
props.onChannelConnectionDisconnect(props.inputName);
}

function handlePointerEnter() {
guiMessageBroker.publishEvent(GuiEvent.HighlightDataChannelConnectionRequest, {
moduleInstanceId: props.moduleInstanceId,
@@ -195,6 +193,7 @@ export const InputChannelNode: React.FC<InputChannelNodeProps> = (props) => {

function handlePointerLeave() {
guiMessageBroker.publishEvent(GuiEvent.UnhighlightDataChannelConnectionRequest, {});
guiMessageBroker.publishEvent(GuiEvent.DataChannelNodeUnhover, {});
}

return (
@@ -204,36 +203,39 @@ export const InputChannelNode: React.FC<InputChannelNodeProps> = (props) => {
data-channelconnector
className={resolveClassNames(
"flex",
"flex-col",
"items-center",
"justify-center",
"rounded",
"border",
"p-4",
"h-20",
"m-2",
"h-16",
"gap-2",
"text-sm",
{
"hover:border-red-600": !connectable,
"hover:border-green-600": connectable,
"bg-green-600": hovered && connectable,
"bg-red-600": hovered && !connectable,
"bg-green-600 border-green-600": hovered && connectable,
"bg-blue-600 border-blue-600": hovered && !connectable,
"bg-slate-100": !hovered,
"text-white": hovered,
"shadow-md": hasConnection,
}
)}
onPointerEnter={handlePointerEnter}
onPointerLeave={handlePointerLeave}
>
{props.displayName}
{editDataChannelConnections && hasConnection && (
<IconButton
onPointerUp={handleChannelConnectionRemoveClick}
className="ml-2 m-0 text-white"
title="Remove data channel connection"
>
<Close fontSize="small" />
</IconButton>
)}
<IconButton
ref={removeButtonRef}
className={resolveClassNames("m-0 hover:bg-white hover:text-red-600", {
"text-white": hovered,
"text-red-600": !hovered,
hidden: !editDataChannelConnections || !hasConnection,
})}
title="Remove data channel connection"
>
<Remove fontSize="small" />
</IconButton>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -39,6 +39,7 @@ export const InputChannelNodeWrapper: React.FC<InputChannelNodeWrapperProps> = (
props.guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {});
setVisible(false);
}
e.stopPropagation();
}

function handleEditDataChannelConnectionsRequest(
@@ -78,7 +79,7 @@ export const InputChannelNodeWrapper: React.FC<InputChannelNodeWrapperProps> = (

return createPortal(
<div
className={resolveClassNames("absolute", "flex", "items-center", "justify-center", "z-50", {
className={resolveClassNames("absolute flex items-center justify-center z-50", {
invisible: !visible,
})}
style={{
Original file line number Diff line number Diff line change
@@ -76,6 +76,9 @@ export const ViewWrapper: React.FC<ViewWrapperProps> = (props) => {
);

function handleModuleClick() {
if (dataChannelConnectionsLayerVisible) {
return;
}
if (settingsPanelWidth <= 5) {
setSettingsPanelWidth(20);
}
@@ -91,9 +94,6 @@ export const ViewWrapper: React.FC<ViewWrapperProps> = (props) => {
}

function handlePointerUp() {
if (dataChannelConnectionsLayerVisible) {
return;
}
if (drawerContent === DrawerContent.ModulesList) {
if (!timeRef.current || Date.now() - timeRef.current < 800) {
handleModuleClick();
@@ -110,52 +110,58 @@ export const ViewWrapper: React.FC<ViewWrapperProps> = (props) => {
e.stopPropagation();
}

function handleChannelConnect(inputName: string, moduleInstanceId: string, destinationPoint: Point) {
const originModuleInstance = props.workbench.getModuleInstance(moduleInstanceId);
const handleChannelConnect = React.useCallback(
function handleChannelConnect(inputName: string, moduleInstanceId: string, destinationPoint: Point) {
const originModuleInstance = props.workbench.getModuleInstance(moduleInstanceId);

if (!originModuleInstance) {
guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {});
return;
}

const acceptedKeys = props.moduleInstance
.getInputChannelDefs()
.find((channelDef) => channelDef.name === inputName)?.keyCategories;

const channels = Object.values(originModuleInstance.getBroadcastChannels()).filter((channel) => {
if (!acceptedKeys || acceptedKeys.some((key) => channel.getDataDef().key === key)) {
return Object.values(props.moduleInstance.getInputChannels()).every((inputChannel) => {
if (inputChannel.getDataDef().key === channel.getDataDef().key) {
return true;
}
return false;
});
if (!originModuleInstance) {
guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {});
return;
}
return false;
});

if (channels.length === 0) {
guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {});
return;
}
const acceptedKeys = props.moduleInstance
.getInputChannelDefs()
.find((channelDef) => channelDef.name === inputName)?.keyCategories;

if (channels.length > 1) {
setChannelSelectorCenterPoint(destinationPoint);
setSelectableChannels(Object.values(channels).map((channel) => channel.getName()));
setCurrentInputName(inputName);
return;
}
const channels = Object.values(originModuleInstance.getBroadcastChannels()).filter((channel) => {
if (!acceptedKeys || acceptedKeys.some((key) => channel.getDataDef().key === key)) {
return Object.values(props.moduleInstance.getInputChannels()).every((inputChannel) => {
if (inputChannel.getDataDef().key === channel.getDataDef().key) {
return true;
}
return false;
});
}
return false;
});

const channelName = Object.values(channels)[0].getName();
if (channels.length === 0) {
guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {});
return;
}

props.moduleInstance.setInputChannel(inputName, channelName);
guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {});
}
if (channels.length > 1) {
setChannelSelectorCenterPoint(destinationPoint);
setSelectableChannels(Object.values(channels).map((channel) => channel.getName()));
setCurrentInputName(inputName);
return;
}

function handleChannelDisconnect(inputName: string) {
props.moduleInstance.removeInputChannel(inputName);
guiMessageBroker.publishEvent(GuiEvent.DataChannelConnectionsChange, {});
}
const channelName = Object.values(channels)[0].getName();

props.moduleInstance.setInputChannel(inputName, channelName);
guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {});
},
[props.moduleInstance, props.workbench]
);

const handleChannelDisconnect = React.useCallback(
function handleChannelDisconnect(inputName: string) {
props.moduleInstance.removeInputChannel(inputName);
guiMessageBroker.publishEvent(GuiEvent.DataChannelConnectionsChange, {});
},
[props.moduleInstance]
);

function handleCancelChannelSelection() {
setChannelSelectorCenterPoint(null);
@@ -164,14 +170,15 @@ export const ViewWrapper: React.FC<ViewWrapperProps> = (props) => {
}

function handleChannelSelection(channelName: string) {
guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {});

if (!currentInputName) {
return;
}
setChannelSelectorCenterPoint(null);
setSelectableChannels([]);

props.moduleInstance.setInputChannel(currentInputName, channelName);
guiMessageBroker.publishEvent(GuiEvent.HideDataChannelConnectionsRequest, {});
}

const showAsActive =
2 changes: 1 addition & 1 deletion frontend/src/lib/components/Slider/slider.tsx
Original file line number Diff line number Diff line change
@@ -221,7 +221,7 @@ export const Slider = React.forwardRef((props: SliderProps, ref: React.Forwarded
"h-5",
"block",
"bg-blue-600",
"z-50",
"z-30",
"shadow-sm",
"rounded-full",
"transform",
4 changes: 4 additions & 0 deletions frontend/src/modules/DistributionPlot/view.tsx
Original file line number Diff line number Diff line change
@@ -107,6 +107,10 @@ export const view = ({
return;
}

if (plotType !== PlotType.ScatterWithColorMapping) {
setPlotType(PlotType.ScatterWithColorMapping);
}

const handleChannelColorChanged = (data: any | null, metaData: BroadcastChannelMeta | null) => {
setDataColor(data);
setMetaDataColor(metaData);
4 changes: 0 additions & 4 deletions frontend/src/modules/SimulationTimeSeries/channelDefs.ts
Original file line number Diff line number Diff line change
@@ -10,8 +10,4 @@ export const broadcastChannelsDef = {
key: BroadcastChannelKeyCategory.Realization,
value: BroadcastChannelValueType.Numeric,
},
[BroadcastChannelNames.Realization_Value_TEST]: {
key: BroadcastChannelKeyCategory.Realization,
value: BroadcastChannelValueType.Numeric,
},
};

0 comments on commit 9f4eaaa

Please sign in to comment.