Skip to content

Commit

Permalink
Merge pull request #432 from jackgriffiths/zoom-to-fit
Browse files Browse the repository at this point in the history
Add commands for zoom to fit
  • Loading branch information
newcat authored Oct 23, 2024
2 parents 3689272 + eeaccc3 commit 668adb4
Show file tree
Hide file tree
Showing 9 changed files with 176 additions and 0 deletions.
4 changes: 4 additions & 0 deletions docs/visual-editor/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,10 @@ Settings can be changed by accessing the `settings` property of the view model r
| nodes.reverseY | boolean | false |
| contextMenu.enabled | boolean | true |
| contextMenu.additionalItems | ContextMenuItem[] | [] |
| zoomToFit.paddingLeft | number | 300 |
| zoomToFit.paddingRight | number | 50 |
| zoomToFit.paddingTop | number | 110 |
| zoomToFit.paddingBottom | number | 50 |

For example, to enable displaying the value of a node interface on hover:

Expand Down
11 changes: 11 additions & 0 deletions packages/renderer-vue/playground/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
<button @click="saveAndLoad">Save and Load</button>
<button @click="changeSidebarWidth">SidebarWidth</button>
<button @click="clearHistory">Clear History</button>
<button @click="zoomToFitRandomNode">Zoom to Random Node</button>
</div>
</template>

Expand Down Expand Up @@ -154,6 +155,16 @@ const changeSidebarWidth = () => {
const clearHistory = () => {
baklavaView.commandHandler.executeCommand<Commands.ClearHistoryCommand>(Commands.CLEAR_HISTORY_COMMAND);
};
const zoomToFitRandomNode = () => {
if (baklavaView.displayedGraph.nodes.length === 0) {
return;
}
const nodes = baklavaView.displayedGraph.nodes;
const node = nodes[Math.floor(Math.random() * nodes.length)];
baklavaView.commandHandler.executeCommand<Commands.ZoomToFitNodesCommand>(Commands.ZOOM_TO_FIT_NODES_COMMAND, true, [node]);
}
</script>

<style>
Expand Down
2 changes: 2 additions & 0 deletions packages/renderer-vue/src/commandList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export type { ClearHistoryCommand, CommitTransactionCommand, StartTransactionCom
export type { ClearClipboardCommand, CopyCommand, PasteCommand } from "./clipboard";
export type { OpenSidebarCommand } from "./sidebar";
export type { StartSelectionBoxCommand } from "./editor/selectionBox";
export type { ZoomToFitRectCommand, ZoomToFitNodesCommand, ZoomToFitGraphCommand } from "./zoomToFit";

export {
CREATE_SUBGRAPH_COMMAND,
Expand All @@ -14,3 +15,4 @@ export { CLEAR_HISTORY_COMMAND, COMMIT_TRANSACTION_COMMAND, START_TRANSACTION_CO
export { CLEAR_CLIPBOARD_COMMAND, COPY_COMMAND, PASTE_COMMAND } from "./clipboard";
export { OPEN_SIDEBAR_COMMAND } from "./sidebar";
export { START_SELECTION_BOX_COMMAND } from "./editor/selectionBox";
export { ZOOM_TO_FIT_RECT_COMMAND, ZOOM_TO_FIT_NODES_COMMAND, ZOOM_TO_FIT_GRAPH_COMMAND } from "./zoomToFit";
22 changes: 22 additions & 0 deletions packages/renderer-vue/src/icons/ZoomScan.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
class="baklava-icon"
width="24"
height="24"
viewBox="0 0 24 24"
stroke-width="2"
stroke="currentColor"
fill="none"
stroke-linecap="round"
stroke-linejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M4 8v-2a2 2 0 0 1 2 -2h2" />
<path d="M4 16v2a2 2 0 0 0 2 2h2" />
<path d="M16 4h2a2 2 0 0 1 2 2v2" />
<path d="M16 20h2a2 2 0 0 0 2 -2v-2" />
<path d="M8 11a3 3 0 1 0 6 0a3 3 0 0 0 -6 0" />
<path d="M16 16l-2.5 -2.5" />
</svg>
</template>
1 change: 1 addition & 0 deletions packages/renderer-vue/src/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ export { default as Hierarchy2 } from "./Hierarchy2.vue";
export { default as SelectAll } from "./SelectAll.vue";
export { default as Trash } from "./Trash.vue";
export { default as VerticalDots } from "./VerticalDots.vue";
export { default as ZoomScan } from "./ZoomScan.vue";
12 changes: 12 additions & 0 deletions packages/renderer-vue/src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ export interface IViewSettings {
enabled: boolean;
additionalItems: ContextMenuItem[];
};
zoomToFit: {
paddingLeft: number;
paddingRight: number;
paddingTop: number;
paddingBottom: number;
};
}

export const DEFAULT_SETTINGS: () => IViewSettings = () => ({
Expand Down Expand Up @@ -101,4 +107,10 @@ export const DEFAULT_SETTINGS: () => IViewSettings = () => ({
enabled: true,
additionalItems: [],
},
zoomToFit: {
paddingLeft: 300,
paddingRight: 50,
paddingTop: 110,
paddingBottom: 50,
},
});
2 changes: 2 additions & 0 deletions packages/renderer-vue/src/toolbar/Toolbar.vue
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import {
START_SELECTION_BOX_COMMAND,
DELETE_NODES_COMMAND,
SWITCH_TO_MAIN_GRAPH_COMMAND,
ZOOM_TO_FIT_GRAPH_COMMAND,
} from "../commandList";
import * as Icons from "../icons";
import ToolbarButton from "./ToolbarButton.vue";
Expand All @@ -44,6 +45,7 @@ export default defineComponent({
{ command: DELETE_NODES_COMMAND, title: "Delete selected nodes", icon: Icons.Trash },
{ command: UNDO_COMMAND, title: "Undo", icon: Icons.ArrowBackUp },
{ command: REDO_COMMAND, title: "Redo", icon: Icons.ArrowForwardUp },
{ command: ZOOM_TO_FIT_GRAPH_COMMAND, title: "Zoom to Fit", icon: Icons.ZoomScan },
{ command: START_SELECTION_BOX_COMMAND, title: "Box Select", icon: Icons.SelectAll },
{ command: CREATE_SUBGRAPH_COMMAND, title: "Create Subgraph", icon: Icons.Hierarchy2 },
];
Expand Down
2 changes: 2 additions & 0 deletions packages/renderer-vue/src/viewModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { IViewNodeState, setViewNodeProperties } from "./node/viewNode";
import { SubgraphInputNode, SubgraphOutputNode } from "./graph/subgraphInterfaceNodes";
import { registerSidebarCommands } from "./sidebar";
import { DEFAULT_SETTINGS, IViewSettings } from "./settings";
import { registerZoomToFitCommands } from "./zoomToFit";
export interface IBaklavaViewModel extends IBaklavaTapable {
editor: Editor;
/** Currently displayed graph */
Expand Down Expand Up @@ -55,6 +56,7 @@ export function useBaklava(existingEditor?: Editor): IBaklavaViewModel {

registerGraphCommands(displayedGraph, commandHandler, switchGraph);
registerSidebarCommands(displayedGraph, commandHandler);
registerZoomToFitCommands(displayedGraph, commandHandler, settings);

watch(
editor,
Expand Down
120 changes: 120 additions & 0 deletions packages/renderer-vue/src/zoomToFit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { Ref } from "vue";
import { AbstractNode, Graph } from "@baklavajs/core";
import { ICommand, ICommandHandler } from "./commands";
import { IViewSettings } from "./settings";

interface IRect {
x1: number;
y1: number;
x2: number;
y2: number;
}

export const ZOOM_TO_FIT_RECT_COMMAND = "ZOOM_TO_FIT_RECT";
export const ZOOM_TO_FIT_NODES_COMMAND = "ZOOM_TO_FIT_NODES";
export const ZOOM_TO_FIT_GRAPH_COMMAND = "ZOOM_TO_FIT_GRAPH";

export type ZoomToFitRectCommand = ICommand<void, [IRect]>;
export type ZoomToFitNodesCommand = ICommand<void, [AbstractNode[]]>;
export type ZoomToFitGraphCommand = ICommand<void>;

export function registerZoomToFitCommands(displayedGraph: Ref<Graph>, handler: ICommandHandler, settings: IViewSettings) {
handler.registerCommand(ZOOM_TO_FIT_RECT_COMMAND, {
canExecute: () => true,
execute: (rect: IRect) => zoomToFitRect(displayedGraph.value, settings, rect),
});
handler.registerCommand(ZOOM_TO_FIT_NODES_COMMAND, {
canExecute: () => true,
execute: (nodes: AbstractNode[]) => zoomToFitNodes(displayedGraph.value, settings, nodes),
});
handler.registerCommand(ZOOM_TO_FIT_GRAPH_COMMAND, {
canExecute: () => displayedGraph.value.nodes.length > 0,
execute: () => zoomToFitGraph(displayedGraph.value, settings),
});
handler.registerHotkey(["f"], ZOOM_TO_FIT_GRAPH_COMMAND);
}

function zoomToFitRect(graph: Graph, settings: IViewSettings, rect: IRect) {
const padding = {
left: settings.zoomToFit.paddingLeft,
right: settings.zoomToFit.paddingRight,
top: settings.zoomToFit.paddingTop,
bottom: settings.zoomToFit.paddingBottom,
};

const editorEl = document.querySelector(".baklava-editor") as Element;
const editorBounding = editorEl.getBoundingClientRect();

const editorWidth = Math.max(0, editorBounding.width - padding.left - padding.right);
const editorHeight = Math.max(0, editorBounding.height - padding.top - padding.bottom);

rect = normalizeRect(rect);

const rectWidth = rect.x2 - rect.x1;
const rectHeight = rect.y2 - rect.y1;

const widthRatio = rectWidth === 0 ? Infinity : editorWidth / rectWidth;
const heightRatio = rectHeight == 0 ? Infinity : editorHeight / rectHeight;

let scale = Math.min(widthRatio, heightRatio);

if (scale === 0 || !Number.isFinite(scale)) {
scale = 1;
}

const remainingEditorWidth = Math.max(0, editorWidth / scale - rectWidth);
const remainingEditorHeight = Math.max(0, editorHeight / scale - rectHeight);

const offsetX = -rect.x1 + padding.left / scale + remainingEditorWidth / 2;
const offsetY = -rect.y1 + padding.top / scale + remainingEditorHeight / 2;

graph.panning.x = offsetX;
graph.panning.y = offsetY;
graph.scaling = scale;
}

function zoomToFitNodes(graph: Graph, settings: IViewSettings, nodes: readonly AbstractNode[]) {
if (nodes.length === 0) {
return;
}

const nodeRects = nodes.map(getNodeRect);

const boundingRect = {
x1: Math.min(...nodeRects.map((i) => i.x1)),
y1: Math.min(...nodeRects.map((i) => i.y1)),
x2: Math.max(...nodeRects.map((i) => i.x2)),
y2: Math.max(...nodeRects.map((i) => i.y2)),
};

zoomToFitRect(graph, settings, boundingRect);
}

function zoomToFitGraph(graph: Graph, settings: IViewSettings) {
zoomToFitNodes(graph, settings, graph.nodes);
}

function getNodeRect(node: AbstractNode): IRect {
const domElement = document.getElementById(node.id);
const width = domElement?.offsetWidth ?? 0;
const height = domElement?.offsetHeight ?? 0;
const posX = node.position?.x ?? 0;
const posY = node.position?.y ?? 0;

return {
x1: posX,
y1: posY,
x2: posX + width,
y2: posY + height,
};
}

function normalizeRect(rect: IRect): IRect {
// Make (x1, y1) the top left corner and (x2, y2) the bottom right corner.
return {
x1: Math.min(rect.x1, rect.x2),
y1: Math.min(rect.y1, rect.y2),
x2: Math.max(rect.x1, rect.x2),
y2: Math.max(rect.y1, rect.y2),
};
}

0 comments on commit 668adb4

Please sign in to comment.