Skip to content

Commit

Permalink
Merge pull request #695 from writer/feat-autoarrange-blocks
Browse files Browse the repository at this point in the history
feat: Autoarrange nodes
  • Loading branch information
ramedina86 authored Dec 18, 2024
2 parents 22d0248 + 5406097 commit 58be8e0
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 29 deletions.
10 changes: 6 additions & 4 deletions src/ui/src/builder/BuilderHeader.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,25 +5,27 @@
<div class="undoRedo">
<button
class="undo"
:title="
:data-writer-tooltip="
undoRedoSnapshot.isUndoAvailable
? `Undo ${undoRedoSnapshot.undoDesc}`
? `Undo: ${undoRedoSnapshot.undoDesc}`
: 'Nothing to undo'
"
:disabled="!undoRedoSnapshot.isUndoAvailable"
data-writer-tooltip-placement="bottom"
@click="undo()"
>
<i class="material-symbols-outlined"> undo </i>
Undo
</button>
<button
class="redo"
:title="
:data-writer-tooltip="
undoRedoSnapshot.isRedoAvailable
? `Redo ${undoRedoSnapshot.redoDesc}`
? `Redo: ${undoRedoSnapshot.redoDesc}`
: 'Nothing to redo'
"
:disabled="!undoRedoSnapshot.isRedoAvailable"
data-writer-tooltip-placement="bottom"
@click="redo()"
>
<i class="material-symbols-outlined"> redo </i>
Expand Down
29 changes: 27 additions & 2 deletions src/ui/src/builder/useComponentActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -765,7 +765,7 @@ export function useComponentActions(wf: Core, ssbm: BuilderManager) {
const component = wf.getComponentById(componentId);
if (!component) return;
const transactionId = `edit-${componentId}-out-${out.outId}-${out.toNodeId}`;
ssbm.openMutationTransaction(transactionId, `Edit out`, true);
ssbm.openMutationTransaction(transactionId, "Edit out", true);
ssbm.registerPreMutation(component);

component.outs = component.outs.filter(
Expand All @@ -789,7 +789,7 @@ export function useComponentActions(wf: Core, ssbm: BuilderManager) {
if (!component) return;

const transactionId = `change-${componentId}-coordinates`;
ssbm.openMutationTransaction(transactionId, `Change coordinates`, true);
ssbm.openMutationTransaction(transactionId, "Change coordinates", false);
ssbm.registerPreMutation(component);

component.x = Math.floor(x);
Expand All @@ -800,6 +800,30 @@ export function useComponentActions(wf: Core, ssbm: BuilderManager) {
wf.sendComponentUpdate();
}

/***
* Change the coordinates of multiple components.
*/
function changeCoordinatesMultiple(
coordinates: Record<Component["id"], {x: number, y: number}>
) {
const transactionId = "change-multiple-coordinates";
ssbm.openMutationTransaction(transactionId, "Change coordinates", false);

const entries = Object.entries(coordinates);
if (entries.length == 0) return;
entries.forEach(([componentId, {x, y}]) => {
const component = wf.getComponentById(componentId);
if (!component) return;
ssbm.registerPreMutation(component);
component.x = Math.floor(x);
component.y = Math.floor(y);
ssbm.registerPostMutation(component);
});

ssbm.closeMutationTransaction(transactionId);
wf.sendComponentUpdate();
}

/**
* Set the value for a component's visibility.
*/
Expand Down Expand Up @@ -948,6 +972,7 @@ export function useComponentActions(wf: Core, ssbm: BuilderManager) {
addOut,
removeOut,
changeCoordinates,
changeCoordinatesMultiple,
setVisibleValue,
setBinding,
getUndoRedoSnapshot,
Expand Down
143 changes: 127 additions & 16 deletions src/ui/src/components/workflows/WorkflowsWorkflow.vue
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@
<component
:is="renderProxiedComponent(node.id, 0)"
:style="{
top: `${node.y - renderOffset.y}px`,
left: `${node.x - renderOffset.x}px`,
top: `${(temporaryNodeCoordinates?.[node.id]?.y ?? node.y) - renderOffset.y}px`,
left: `${(temporaryNodeCoordinates?.[node.id]?.x ?? node.x) - renderOffset.x}px`,
'border-color':
activeConnection?.liveArrow?.toNodeId == node.id
? activeConnection?.liveArrow?.color
Expand Down Expand Up @@ -72,6 +72,7 @@
:render-offset="renderOffset"
:zoom-level="zoomLevel"
class="navigator"
@auto-arrange="handleAutoArrange"
@change-render-offset="handleChangeRenderOffset"
@change-zoom-level="handleChangeZoomLevel"
@reset-zoom="resetZoom"
Expand Down Expand Up @@ -154,13 +155,24 @@ const isRunning = ref(false);
const selectedArrow = ref(null);
const zoomLevel = ref(ZOOM_SETTINGS.initialLevel);
const arrowRefresherObserver = new MutationObserver(refreshArrows);
const temporaryNodeCoordinates = ref<
Record<Component["id"], { x: number; y: number }>
>({});
const AUTOARRANGE_ROW_GAP_PX = 96;
const AUTOARRANGE_COLUMN_GAP_PX = 128;
const nodes = computed(() =>
wf.getComponents(workflowComponentId, { sortedByPosition: true }),
);
const { createAndInsertComponent, addOut, removeOut, changeCoordinates } =
useComponentActions(wf, wfbm);
const {
createAndInsertComponent,
addOut,
removeOut,
changeCoordinates,
changeCoordinatesMultiple,
} = useComponentActions(wf, wfbm);
const { getComponentInfoFromDrag } = useDragDropComponent(wf);
const activeConnection: Ref<{
Expand Down Expand Up @@ -214,6 +226,101 @@ function handleClick() {
selectedArrow.value = null;
}
function organizeNodesInColumns() {
const columns: Map<number, Set<Component>> = new Map();
function scan(node: Component, layer: number) {
columns.forEach((column) => {
if (column.has(node)) {
column.delete(node);
}
});
if (!columns.has(layer)) {
columns.set(layer, new Set());
}
const column = columns.get(layer);
column.add(node);
node.outs?.forEach((out) => {
const outNode = wf.getComponentById(out.toNodeId);
scan(outNode, layer + 1);
});
}
const dependencies: Map<Component["id"], Set<Component["id"]>> = new Map();
nodes.value.forEach((node) => {
node.outs?.forEach((outNode) => {
if (!dependencies.has(outNode.toNodeId)) {
dependencies.set(outNode.toNodeId, new Set());
}
dependencies.get(outNode.toNodeId).add(node.id);
});
});
nodes.value
.filter((node) => !dependencies.has(node.id))
.forEach((startNode) => {
scan(startNode, 0);
});
return columns;
}
function calculateAutoArrangeDimensions(columns: Map<number, Set<Component>>) {
const columnDimensions: Map<number, { height: number; width: number }> =
new Map();
const nodeDimensions: Map<Component["id"], { height: number }> = new Map();
columns.forEach((nodes, layer) => {
let height = 0;
let width = 0;
nodes.forEach((node) => {
const nodeEl = nodeContainerEl.value.querySelector(
`[data-writer-id="${node.id}"]`,
);
if (!nodeEl) return;
const nodeBCR = nodeEl.getBoundingClientRect();
nodeDimensions.set(node.id, {
height: nodeBCR.height * (1 / zoomLevel.value),
});
height +=
nodeBCR.height * (1 / zoomLevel.value) + AUTOARRANGE_ROW_GAP_PX;
width = Math.max(width, nodeBCR.width * (1 / zoomLevel.value));
});
columnDimensions.set(layer, {
height: height - AUTOARRANGE_ROW_GAP_PX,
width,
});
});
return { columnDimensions, nodeDimensions };
}
function handleAutoArrange() {
const columns = organizeNodesInColumns();
const { columnDimensions, nodeDimensions } =
calculateAutoArrangeDimensions(columns);
const maxColumnHeight = Math.max(
...Array.from(columnDimensions.values()).map(
(dimensions) => dimensions.height,
),
);
const coordinates = {};
let x = AUTOARRANGE_COLUMN_GAP_PX;
for (let i = 0; i < columns.size; i++) {
const nodes = Array.from(columns.get(i)).sort((a, b) =>
a.y > b.y ? 1 : -1,
);
const { width, height } = columnDimensions.get(i);
let y = (maxColumnHeight - height) / 2 + AUTOARRANGE_ROW_GAP_PX;
nodes.forEach((node) => {
coordinates[node.id] = { x, y };
y += nodeDimensions.get(node.id).height + AUTOARRANGE_ROW_GAP_PX;
});
x += width + AUTOARRANGE_COLUMN_GAP_PX;
}
changeCoordinatesMultiple(coordinates);
}
async function handleRun() {
if (isRunning.value) return;
isRunning.value = true;
Expand Down Expand Up @@ -376,26 +483,27 @@ function clearActiveOperations() {
activeNodeMove.value = null;
}
function saveNodeMove() {
const { nodeId } = activeNodeMove.value;
const tempXY = temporaryNodeCoordinates.value?.[nodeId];
if (!tempXY) return;
const { x, y } = tempXY;
changeCoordinates(nodeId, x, y);
temporaryNodeCoordinates.value[nodeId] = null;
}
function moveNode(ev: MouseEvent) {
const { nodeId, offset } = activeNodeMove.value;
activeNodeMove.value.isPerfected = true;
const component = wf.getComponentById(nodeId);
const { x, y } = getAdjustedCoordinates(ev);
const newX = Math.floor(x - offset.x);
const newY = Math.floor(y - offset.y);
if (component.x == newX && component.y == newY) return;
component.x = newX;
component.y = newY;
setTimeout(() => {
// Debouncing
if (component.x !== newX) return;
if (component.y !== newY) return;
changeCoordinates(component.id, newX, newY);
}, 200);
temporaryNodeCoordinates.value[nodeId] = {
x: newX,
y: newY,
};
}
function moveCanvas(ev: MouseEvent) {
Expand Down Expand Up @@ -444,6 +552,9 @@ function handleMousedown(ev: MouseEvent) {
}
async function handleMouseup(ev: MouseEvent) {
if (activeNodeMove.value) {
saveNodeMove();
}
if (activeConnection.value === null) {
return;
}
Expand Down
4 changes: 2 additions & 2 deletions src/ui/src/components/workflows/base/WorkflowMiniMap.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const miniMap = ref({
});
const selector = ref({
width: 230,
width: 260,
height: 1,
top: 0,
left: 0,
Expand Down Expand Up @@ -120,7 +120,7 @@ function render() {
});
miniMap.value = {
width: 230,
width: 260,
height:
((nodeContainerBCR.height / nodeContainerBCR.width) *
nodeContainerBCR.width) /
Expand Down
29 changes: 28 additions & 1 deletion src/ui/src/components/workflows/base/WorkflowNavigator.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@
@change-render-offset="handleRenderOffsetChange"
></WorkflowMiniMap>
<div class="bar">
<div class="autoArranger">
<WdsButton
variant="neutral"
size="smallIcon"
data-writer-tooltip="Auto arrange blocks"
data-writer-tooltip-placement="left"
@click="handleAutoArrange"
>
<i class="material-symbols-outlined">apps</i>
</WdsButton>
</div>
<div class="zoomer">
<WdsButton
variant="neutral"
Expand Down Expand Up @@ -99,6 +110,7 @@ const emit = defineEmits([
"changeRenderOffset",
"changeZoomLevel",
"resetZoom",
"autoArrange",
]);
const isMiniMapShown = ref(true);
const zoomLevelAsText = ref<string>(
Expand All @@ -113,6 +125,10 @@ function toggleMiniMap() {
isMiniMapShown.value = !isMiniMapShown.value;
}
function handleAutoArrange(ev: MouseEvent) {

Check warning on line 128 in src/ui/src/components/workflows/base/WorkflowNavigator.vue

View workflow job for this annotation

GitHub Actions / build (3.9)

'ev' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 128 in src/ui/src/components/workflows/base/WorkflowNavigator.vue

View workflow job for this annotation

GitHub Actions / build (3.9)

'ev' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 128 in src/ui/src/components/workflows/base/WorkflowNavigator.vue

View workflow job for this annotation

GitHub Actions / build (3.9)

'ev' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 128 in src/ui/src/components/workflows/base/WorkflowNavigator.vue

View workflow job for this annotation

GitHub Actions / build (3.10)

'ev' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 128 in src/ui/src/components/workflows/base/WorkflowNavigator.vue

View workflow job for this annotation

GitHub Actions / build (3.10)

'ev' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 128 in src/ui/src/components/workflows/base/WorkflowNavigator.vue

View workflow job for this annotation

GitHub Actions / build (3.10)

'ev' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 128 in src/ui/src/components/workflows/base/WorkflowNavigator.vue

View workflow job for this annotation

GitHub Actions / build (3.11)

'ev' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 128 in src/ui/src/components/workflows/base/WorkflowNavigator.vue

View workflow job for this annotation

GitHub Actions / build (3.11)

'ev' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 128 in src/ui/src/components/workflows/base/WorkflowNavigator.vue

View workflow job for this annotation

GitHub Actions / build (3.11)

'ev' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 128 in src/ui/src/components/workflows/base/WorkflowNavigator.vue

View workflow job for this annotation

GitHub Actions / build (3.12)

'ev' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 128 in src/ui/src/components/workflows/base/WorkflowNavigator.vue

View workflow job for this annotation

GitHub Actions / build (3.12)

'ev' is defined but never used. Allowed unused args must match /^_/u

Check warning on line 128 in src/ui/src/components/workflows/base/WorkflowNavigator.vue

View workflow job for this annotation

GitHub Actions / build (3.12)

'ev' is defined but never used. Allowed unused args must match /^_/u
emit("autoArrange");
}
function handleRenderOffsetChange(offset: typeof props.renderOffset) {
emit("changeRenderOffset", offset);
}
Expand Down Expand Up @@ -190,20 +206,31 @@ onUnmounted(() => {
overflow: hidden;
color: var(--builderSecondaryTextColor);
border: 1px solid var(--builderSeparatorColor);
width: 230px;
width: 260px;
}
.bar {
display: flex;
background: var(--builderBackgroundColor);
}
.autoArranger {
min-height: 36px;
min-width: 40px;
flex: 0 0 40px;
display: flex;
border-right: 1px solid var(--builderSeparatorColor);
align-items: center;
justify-content: center;
}
.zoomer {
flex: 1 0 auto;
display: flex;
padding: 4px;
gap: 4px;
align-items: center;
justify-content: center;
}
.zoomer .zoomLevelInput {
Expand Down
Loading

0 comments on commit 58be8e0

Please sign in to comment.