mirror of
https://github.com/galaxyproject/galaxy.git
synced 2026-09-24 16:30:27 +08:00
implement fit workflow function
This commit is contained in:
@@ -158,6 +158,7 @@
|
||||
</div>
|
||||
<WorkflowGraph
|
||||
v-if="!datatypesMapperLoading"
|
||||
ref="workflowGraph"
|
||||
:steps="steps"
|
||||
:datatypes-mapper="datatypesMapper"
|
||||
:highlight-id="highlightId"
|
||||
@@ -193,7 +194,7 @@
|
||||
import { library } from "@fortawesome/fontawesome-svg-core";
|
||||
import { faArrowLeft, faArrowRight, faCog, faHistory, faSave, faTimes } from "@fortawesome/free-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome";
|
||||
import { whenever } from "@vueuse/core";
|
||||
import { until, whenever } from "@vueuse/core";
|
||||
import { logicAnd, logicNot, logicOr } from "@vueuse/math";
|
||||
import { Toast } from "composables/toast";
|
||||
import { storeToRefs } from "pinia";
|
||||
@@ -297,6 +298,7 @@ export default {
|
||||
whenever(redoKeys, redo);
|
||||
|
||||
const activityBar = ref(null);
|
||||
const workflowGraph = ref(null);
|
||||
const reportActive = computed(() => activityBar.value?.isActiveSideBar("workflow-editor-report"));
|
||||
|
||||
const parameters = ref(null);
|
||||
@@ -483,6 +485,7 @@ export default {
|
||||
id,
|
||||
name,
|
||||
parameters,
|
||||
workflowGraph,
|
||||
ensureParametersSet,
|
||||
showAttributes,
|
||||
setName,
|
||||
@@ -996,6 +999,8 @@ export default {
|
||||
await this.resetStores();
|
||||
this.onWorkflowMessage("Loading workflow...", "progress");
|
||||
|
||||
console.log("loading");
|
||||
|
||||
try {
|
||||
const data = await this.lastQueue.enqueue(loadWorkflow, { id, version });
|
||||
await fromSimple(id, data);
|
||||
@@ -1003,6 +1008,11 @@ export default {
|
||||
} catch (e) {
|
||||
this.onWorkflowError("Loading workflow failed...", e);
|
||||
}
|
||||
|
||||
await until(() => this.datatypesMapperLoading).toBe(false);
|
||||
await nextTick();
|
||||
|
||||
this.workflowGraph.fitWorkflow();
|
||||
}
|
||||
},
|
||||
onLicense(license) {
|
||||
|
||||
@@ -1,10 +1,168 @@
|
||||
<script setup lang="ts">
|
||||
import { useElementBounding, useScroll } from "@vueuse/core";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, type PropType, provide, reactive, type Ref, ref, watch, watchEffect } from "vue";
|
||||
|
||||
import { DatatypesMapperModel } from "@/components/Datatypes/model";
|
||||
import { useWorkflowStores } from "@/composables/workflowStores";
|
||||
import type { TerminalPosition, XYPosition } from "@/stores/workflowEditorStateStore";
|
||||
import type { Step } from "@/stores/workflowStepStore";
|
||||
import { assertDefined } from "@/utils/assertions";
|
||||
|
||||
import { useD3Zoom } from "./composables/d3Zoom";
|
||||
import { useViewportBoundingBox } from "./composables/viewportBoundingBox";
|
||||
import { useWorkflowBoundingBox } from "./composables/workflowBoundingBox";
|
||||
import type { Vector } from "./modules/geometry";
|
||||
import type { OutputTerminals } from "./modules/terminals";
|
||||
import { maxZoom, minZoom } from "./modules/zoomLevels";
|
||||
|
||||
import AdaptiveGrid from "./AdaptiveGrid.vue";
|
||||
import WorkflowComment from "./Comments/WorkflowComment.vue";
|
||||
import BoxSelectPreview from "./Tools/BoxSelectPreview.vue";
|
||||
import InputCatcher from "./Tools/InputCatcher.vue";
|
||||
import ToolBar from "./Tools/ToolBar.vue";
|
||||
import WorkflowNode from "@/components/Workflow/Editor/Node.vue";
|
||||
import WorkflowEdges from "@/components/Workflow/Editor/WorkflowEdges.vue";
|
||||
import WorkflowMinimap from "@/components/Workflow/Editor/WorkflowMinimap.vue";
|
||||
import ZoomControl from "@/components/Workflow/Editor/ZoomControl.vue";
|
||||
|
||||
const emit = defineEmits(["transform", "graph-offset", "onRemove", "scrollTo", "stepClicked"]);
|
||||
const props = defineProps({
|
||||
steps: { type: Object as PropType<{ [index: string]: Step }>, required: true },
|
||||
datatypesMapper: { type: DatatypesMapperModel, required: true },
|
||||
highlightId: { type: Number as PropType<number | null>, default: null },
|
||||
scrollToId: { type: Number as PropType<number | null>, default: null },
|
||||
readonly: { type: Boolean, default: false },
|
||||
initialPosition: { type: Object as PropType<{ x: number; y: number }>, default: () => ({ x: 50, y: 20 }) },
|
||||
isInvocation: { type: Boolean, default: false },
|
||||
showMinimap: { type: Boolean, default: true },
|
||||
showZoomControls: { type: Boolean, default: true },
|
||||
});
|
||||
|
||||
const { stateStore, stepStore } = useWorkflowStores();
|
||||
const { scale, activeNodeId, draggingPosition, draggingTerminal } = storeToRefs(stateStore);
|
||||
const canvas: Ref<HTMLElement | null> = ref(null);
|
||||
|
||||
const elementBounding = useElementBounding(canvas, { windowResize: false, windowScroll: false });
|
||||
const scroll = useScroll(canvas);
|
||||
const { transform, panBy, setZoom, moveTo } = useD3Zoom(
|
||||
scale.value,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
canvas,
|
||||
scroll,
|
||||
props.initialPosition
|
||||
);
|
||||
|
||||
const { viewportBoundingBox, updateViewportBaseBoundingBox } = useViewportBoundingBox(
|
||||
elementBounding,
|
||||
scale,
|
||||
transform
|
||||
);
|
||||
const { getWorkflowBoundingBox } = useWorkflowBoundingBox();
|
||||
|
||||
function fitWorkflow(minimumFitZoom = 0.5, maximumFitZoom = 1.0, padding = 50.0) {
|
||||
const aabb = getWorkflowBoundingBox();
|
||||
aabb.expand(padding);
|
||||
|
||||
const transform = updateViewportBaseBoundingBox().transformTo(aabb);
|
||||
|
||||
const scale = Math.min(transform.scaleX, transform.scaleY);
|
||||
const clampedScale = Math.max(minimumFitZoom, Math.min(maximumFitZoom, scale));
|
||||
|
||||
zoomTo(clampedScale);
|
||||
moveTo({ x: aabb.x, y: aabb.y }, [0, 0]);
|
||||
}
|
||||
|
||||
const isDragging = ref(false);
|
||||
provide("isDragging", isDragging);
|
||||
provide("transform", transform);
|
||||
|
||||
watch(
|
||||
() => props.scrollToId,
|
||||
() => {
|
||||
if (props.scrollToId !== null) {
|
||||
const scrollToPosition = stateStore.stepPosition[props.scrollToId];
|
||||
const step = stepStore.getStep(props.scrollToId);
|
||||
|
||||
assertDefined(scrollToPosition);
|
||||
assertDefined(step);
|
||||
|
||||
const { width: stepWidth, height: stepHeight } = scrollToPosition;
|
||||
const { position: stepPosition } = step;
|
||||
if (stepPosition) {
|
||||
const { width, height } = reactive(elementBounding);
|
||||
const centerScreenX = width / 2;
|
||||
const centerScreenY = height / 2;
|
||||
const offsetX = centerScreenX - (stepPosition.left + stepWidth / 2);
|
||||
const offsetY = centerScreenY - (stepPosition.top + stepHeight / 2);
|
||||
zoomTo(1, { x: offsetX, y: offsetY });
|
||||
} else {
|
||||
console.log("Step has no position");
|
||||
}
|
||||
emit("scrollTo");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function zoomTo(zoomLevel: number, panTo: XYPosition | null = null, origin?: Vector) {
|
||||
setZoom(zoomLevel, origin);
|
||||
if (panTo) {
|
||||
panBy({ x: panTo.x - transform.value.x, y: panTo.y - transform.value.y });
|
||||
}
|
||||
stateStore.scale = zoomLevel;
|
||||
}
|
||||
function onStopDragging() {
|
||||
stateStore.draggingPosition = null;
|
||||
stateStore.draggingTerminal = null;
|
||||
isDragging.value = false;
|
||||
}
|
||||
function onDragConnector(position: TerminalPosition, draggingTerminal: OutputTerminals) {
|
||||
stateStore.draggingPosition = position;
|
||||
stateStore.draggingTerminal = draggingTerminal;
|
||||
isDragging.value = true;
|
||||
}
|
||||
function onActivate(nodeId: number | null) {
|
||||
emit("stepClicked", nodeId);
|
||||
if (activeNodeId.value !== nodeId) {
|
||||
stateStore.activeNodeId = nodeId;
|
||||
}
|
||||
}
|
||||
function onDeactivate() {
|
||||
stateStore.activeNodeId = null;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => transform.value.k,
|
||||
() => (stateStore.scale = transform.value.k)
|
||||
);
|
||||
|
||||
watch(transform, () => emit("transform", transform.value));
|
||||
watchEffect(() => {
|
||||
emit("graph-offset", reactive(elementBounding));
|
||||
});
|
||||
|
||||
const canvasStyle = computed(() => {
|
||||
return { transform: `translate(${transform.value.x}px, ${transform.value.y}px) scale(${transform.value.k})` };
|
||||
});
|
||||
|
||||
const { commentStore } = useWorkflowStores();
|
||||
const { comments } = storeToRefs(commentStore);
|
||||
|
||||
defineExpose({
|
||||
fitWorkflow,
|
||||
setZoom,
|
||||
moveTo,
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div id="workflow-canvas" class="unified-panel-body workflow-canvas">
|
||||
<ZoomControl
|
||||
v-if="props.showZoomControls"
|
||||
:zoom-level="scale"
|
||||
:pan="transform"
|
||||
@onZoom="onZoom"
|
||||
@onZoom="zoomTo"
|
||||
@update:pan="panBy" />
|
||||
<ToolBar v-if="!readonly" />
|
||||
<div
|
||||
@@ -67,142 +225,6 @@
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { useElementBounding, useScroll } from "@vueuse/core";
|
||||
import { storeToRefs } from "pinia";
|
||||
import { computed, type PropType, provide, reactive, type Ref, ref, watch, watchEffect } from "vue";
|
||||
|
||||
import { DatatypesMapperModel } from "@/components/Datatypes/model";
|
||||
import { useWorkflowStores } from "@/composables/workflowStores";
|
||||
import type { TerminalPosition, XYPosition } from "@/stores/workflowEditorStateStore";
|
||||
import type { Step } from "@/stores/workflowStepStore";
|
||||
import { assertDefined } from "@/utils/assertions";
|
||||
|
||||
import { useD3Zoom } from "./composables/d3Zoom";
|
||||
import { useViewportBoundingBox } from "./composables/viewportBoundingBox";
|
||||
import type { OutputTerminals } from "./modules/terminals";
|
||||
import { maxZoom, minZoom } from "./modules/zoomLevels";
|
||||
|
||||
import AdaptiveGrid from "./AdaptiveGrid.vue";
|
||||
import WorkflowComment from "./Comments/WorkflowComment.vue";
|
||||
import BoxSelectPreview from "./Tools/BoxSelectPreview.vue";
|
||||
import InputCatcher from "./Tools/InputCatcher.vue";
|
||||
import ToolBar from "./Tools/ToolBar.vue";
|
||||
import WorkflowNode from "@/components/Workflow/Editor/Node.vue";
|
||||
import WorkflowEdges from "@/components/Workflow/Editor/WorkflowEdges.vue";
|
||||
import WorkflowMinimap from "@/components/Workflow/Editor/WorkflowMinimap.vue";
|
||||
import ZoomControl from "@/components/Workflow/Editor/ZoomControl.vue";
|
||||
|
||||
const emit = defineEmits(["transform", "graph-offset", "onRemove", "scrollTo", "stepClicked"]);
|
||||
const props = defineProps({
|
||||
steps: { type: Object as PropType<{ [index: string]: Step }>, required: true },
|
||||
datatypesMapper: { type: DatatypesMapperModel, required: true },
|
||||
highlightId: { type: Number as PropType<number | null>, default: null },
|
||||
scrollToId: { type: Number as PropType<number | null>, default: null },
|
||||
readonly: { type: Boolean, default: false },
|
||||
initialPosition: { type: Object as PropType<{ x: number; y: number }>, default: () => ({ x: 50, y: 20 }) },
|
||||
isInvocation: { type: Boolean, default: false },
|
||||
showMinimap: { type: Boolean, default: true },
|
||||
showZoomControls: { type: Boolean, default: true },
|
||||
});
|
||||
|
||||
const { stateStore, stepStore } = useWorkflowStores();
|
||||
const { scale, activeNodeId, draggingPosition, draggingTerminal } = storeToRefs(stateStore);
|
||||
const canvas: Ref<HTMLElement | null> = ref(null);
|
||||
|
||||
const elementBounding = useElementBounding(canvas, { windowResize: false, windowScroll: false });
|
||||
const scroll = useScroll(canvas);
|
||||
const { transform, panBy, setZoom, moveTo } = useD3Zoom(
|
||||
scale.value,
|
||||
minZoom,
|
||||
maxZoom,
|
||||
canvas,
|
||||
scroll,
|
||||
props.initialPosition
|
||||
);
|
||||
|
||||
defineExpose({
|
||||
setZoom,
|
||||
moveTo,
|
||||
});
|
||||
|
||||
const { viewportBoundingBox } = useViewportBoundingBox(elementBounding, scale, transform);
|
||||
|
||||
const isDragging = ref(false);
|
||||
provide("isDragging", isDragging);
|
||||
provide("transform", transform);
|
||||
|
||||
watch(
|
||||
() => props.scrollToId,
|
||||
() => {
|
||||
if (props.scrollToId !== null) {
|
||||
const scrollToPosition = stateStore.stepPosition[props.scrollToId];
|
||||
const step = stepStore.getStep(props.scrollToId);
|
||||
|
||||
assertDefined(scrollToPosition);
|
||||
assertDefined(step);
|
||||
|
||||
const { width: stepWidth, height: stepHeight } = scrollToPosition;
|
||||
const { position: stepPosition } = step;
|
||||
if (stepPosition) {
|
||||
const { width, height } = reactive(elementBounding);
|
||||
const centerScreenX = width / 2;
|
||||
const centerScreenY = height / 2;
|
||||
const offsetX = centerScreenX - (stepPosition.left + stepWidth / 2);
|
||||
const offsetY = centerScreenY - (stepPosition.top + stepHeight / 2);
|
||||
onZoom(1, { x: offsetX, y: offsetY });
|
||||
} else {
|
||||
console.log("Step has no position");
|
||||
}
|
||||
emit("scrollTo");
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
function onZoom(zoomLevel: number, panTo: XYPosition | null = null) {
|
||||
setZoom(zoomLevel);
|
||||
if (panTo) {
|
||||
panBy({ x: panTo.x - transform.value.x, y: panTo.y - transform.value.y });
|
||||
}
|
||||
stateStore.scale = zoomLevel;
|
||||
}
|
||||
function onStopDragging() {
|
||||
stateStore.draggingPosition = null;
|
||||
stateStore.draggingTerminal = null;
|
||||
isDragging.value = false;
|
||||
}
|
||||
function onDragConnector(position: TerminalPosition, draggingTerminal: OutputTerminals) {
|
||||
stateStore.draggingPosition = position;
|
||||
stateStore.draggingTerminal = draggingTerminal;
|
||||
isDragging.value = true;
|
||||
}
|
||||
function onActivate(nodeId: number | null) {
|
||||
emit("stepClicked", nodeId);
|
||||
if (activeNodeId.value !== nodeId) {
|
||||
stateStore.activeNodeId = nodeId;
|
||||
}
|
||||
}
|
||||
function onDeactivate() {
|
||||
stateStore.activeNodeId = null;
|
||||
}
|
||||
|
||||
watch(
|
||||
() => transform.value.k,
|
||||
() => (stateStore.scale = transform.value.k)
|
||||
);
|
||||
|
||||
watch(transform, () => emit("transform", transform.value));
|
||||
watchEffect(() => {
|
||||
emit("graph-offset", reactive(elementBounding));
|
||||
});
|
||||
|
||||
const canvasStyle = computed(() => {
|
||||
return { transform: `translate(${transform.value.x}px, ${transform.value.y}px) scale(${transform.value.k})` };
|
||||
});
|
||||
|
||||
const { commentStore } = useWorkflowStores();
|
||||
const { comments } = storeToRefs(commentStore);
|
||||
</script>
|
||||
|
||||
<style scoped land="scss">
|
||||
.workflow-canvas {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { useDraggable, type UseElementBoundingReturn } from "@vueuse/core";
|
||||
import type { Ref } from "vue";
|
||||
import { computed, onMounted, ref, unref, watch } from "vue";
|
||||
import { computed, nextTick, onMounted, ref, unref, watch } from "vue";
|
||||
|
||||
import { useAnimationFrame } from "@/composables/sensors/animationFrame";
|
||||
import { useAnimationFrameThrottle } from "@/composables/throttle";
|
||||
@@ -15,8 +15,9 @@ import type {
|
||||
} from "@/stores/workflowEditorCommentStore";
|
||||
import type { Step, Steps } from "@/stores/workflowStepStore";
|
||||
|
||||
import { useWorkflowBoundingBox } from "./composables/workflowBoundingBox";
|
||||
import { drawBoxComments, drawFreehandComments, drawSteps } from "./modules/canvasDraw";
|
||||
import { AxisAlignedBoundingBox, Transform } from "./modules/geometry";
|
||||
import { type AxisAlignedBoundingBox, Transform } from "./modules/geometry";
|
||||
|
||||
const props = defineProps<{
|
||||
steps: Steps;
|
||||
@@ -47,37 +48,15 @@ watch(
|
||||
{ deep: true }
|
||||
);
|
||||
|
||||
/** bounding box encompassing all nodes in the workflow */
|
||||
const aabb = new AxisAlignedBoundingBox();
|
||||
const { getWorkflowBoundingBox } = useWorkflowBoundingBox();
|
||||
|
||||
let aabbChanged = false;
|
||||
|
||||
/** transform mapping workflow coordinates to minimap coordinates */
|
||||
let canvasTransform = new Transform();
|
||||
|
||||
function recalculateAABB() {
|
||||
aabb.reset();
|
||||
|
||||
Object.values(props.steps).forEach((step) => {
|
||||
const rect = stateStore.stepPosition[step.id];
|
||||
|
||||
if (rect) {
|
||||
aabb.fitRectangle({
|
||||
x: step.position!.left,
|
||||
y: step.position!.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
props.comments.forEach((comment) => {
|
||||
aabb.fitRectangle({
|
||||
x: comment.position[0],
|
||||
y: comment.position[1],
|
||||
width: comment.size[0],
|
||||
height: comment.size[1],
|
||||
});
|
||||
});
|
||||
const aabb = getWorkflowBoundingBox();
|
||||
|
||||
aabb.squareCenter();
|
||||
aabb.expand(120);
|
||||
@@ -119,7 +98,7 @@ const size = {
|
||||
border: 0,
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
onMounted(async () => {
|
||||
const element = canvas.value!;
|
||||
const style = getComputedStyle(element);
|
||||
|
||||
@@ -135,6 +114,8 @@ onMounted(() => {
|
||||
size.padding = parseInt(style.getPropertyValue("--workflow-overview-padding"));
|
||||
size.border = parseInt(style.getPropertyValue("--workflow-overview-border"));
|
||||
|
||||
await nextTick();
|
||||
|
||||
recalculateAABB();
|
||||
redraw = true;
|
||||
});
|
||||
@@ -278,7 +259,7 @@ const scaleFactor = computed(() => size.max / minimapSize.value);
|
||||
let dragViewport = false;
|
||||
|
||||
useDraggable(canvas, {
|
||||
onStart: (position, event) => {
|
||||
onStart: (_position, event) => {
|
||||
// minimap coordinates to global coordinates
|
||||
const [x, y] = canvasTransform
|
||||
.inverse()
|
||||
@@ -289,7 +270,7 @@ useDraggable(canvas, {
|
||||
dragViewport = true;
|
||||
}
|
||||
},
|
||||
onMove: (position, event) => {
|
||||
onMove: (_position, event) => {
|
||||
dragThrottle(() => {
|
||||
if (!dragViewport || Object.values(props.steps).length === 0) {
|
||||
return;
|
||||
@@ -305,7 +286,7 @@ useDraggable(canvas, {
|
||||
emit("panBy", { x, y });
|
||||
});
|
||||
},
|
||||
onEnd(position, event) {
|
||||
onEnd(_position, event) {
|
||||
// minimap coordinates to global coordinates
|
||||
const [x, y] = canvasTransform
|
||||
.inverse()
|
||||
|
||||
@@ -3,6 +3,7 @@ import { select } from "d3-selection";
|
||||
import { type D3ZoomEvent, zoom, zoomIdentity } from "d3-zoom";
|
||||
import { type Ref, ref, watch } from "vue";
|
||||
|
||||
import type { Vector } from "@/components/Workflow/Editor/modules/geometry";
|
||||
import { type XYPosition } from "@/stores/workflowEditorStateStore";
|
||||
|
||||
// if element is draggable it may implement its own drag handler,
|
||||
@@ -58,10 +59,10 @@ export function useD3Zoom(
|
||||
});
|
||||
});
|
||||
|
||||
function setZoom(k: number) {
|
||||
function setZoom(k: number, p?: Vector) {
|
||||
if (targetRef.value) {
|
||||
const d3Selection = select(targetRef.value).call(d3Zoom);
|
||||
d3Zoom.scaleTo(d3Selection, k);
|
||||
d3Zoom.scaleTo(d3Selection, k, p);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,10 +73,10 @@ export function useD3Zoom(
|
||||
}
|
||||
}
|
||||
|
||||
function moveTo(coordinate: XYPosition) {
|
||||
function moveTo(coordinate: XYPosition, p?: Vector) {
|
||||
if (targetRef.value) {
|
||||
const d3Selection = select(targetRef.value).call(d3Zoom);
|
||||
d3Zoom.translateTo(d3Selection, coordinate.x, coordinate.y);
|
||||
d3Zoom.translateTo(d3Selection, coordinate.x, coordinate.y, p);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,39 +10,61 @@ import { AxisAlignedBoundingBox } from "../modules/geometry";
|
||||
* animation frame intervals.
|
||||
*
|
||||
* @param bounds vueuse bounding box
|
||||
* @param scale scale to apply to bounding box
|
||||
* @param zoom zoom to apply to bounding box
|
||||
* @param pan pan to apply to bounding box
|
||||
* @returns Axis Aligned Bounding Box following the viewport
|
||||
*/
|
||||
export function useViewportBoundingBox(
|
||||
bounds: UseElementBoundingReturn,
|
||||
scale: Ref<number>,
|
||||
zoom: Ref<number>,
|
||||
pan: Ref<{ x: number; y: number }>
|
||||
) {
|
||||
const { throttle } = useAnimationFrameThrottle(100);
|
||||
|
||||
const viewportBaseBoundingBox = ref(new AxisAlignedBoundingBox());
|
||||
const viewportBoundingBox = ref(new AxisAlignedBoundingBox());
|
||||
|
||||
const updateViewportBaseBoundingBox = () => {
|
||||
const width = unref(bounds.width);
|
||||
const height = unref(bounds.height);
|
||||
|
||||
viewportBaseBoundingBox.value.x = 0;
|
||||
viewportBaseBoundingBox.value.y = 0;
|
||||
|
||||
viewportBaseBoundingBox.value.width = width;
|
||||
viewportBaseBoundingBox.value.height = height;
|
||||
|
||||
return viewportBaseBoundingBox.value;
|
||||
};
|
||||
|
||||
const updateViewportBoundingBox = () => {
|
||||
const { width, height } = updateViewportBaseBoundingBox();
|
||||
|
||||
const x = unref(pan).x;
|
||||
const y = unref(pan).y;
|
||||
const scaleValue = 1.0 / unref(zoom);
|
||||
|
||||
viewportBoundingBox.value.move([-x * scaleValue, -y * scaleValue]);
|
||||
viewportBoundingBox.value.width = width * scaleValue;
|
||||
viewportBoundingBox.value.height = height * scaleValue;
|
||||
|
||||
return viewportBoundingBox.value;
|
||||
};
|
||||
|
||||
watch(
|
||||
() => ({
|
||||
x: unref(pan).x,
|
||||
y: unref(pan).y,
|
||||
scale: unref(scale),
|
||||
scale: unref(zoom),
|
||||
width: unref(bounds.width),
|
||||
height: unref(bounds.height),
|
||||
}),
|
||||
({ x, y, scale, width, height }) => {
|
||||
() => {
|
||||
throttle(() => {
|
||||
const bounds = viewportBoundingBox.value;
|
||||
|
||||
bounds.x = -x / scale;
|
||||
bounds.y = -y / scale;
|
||||
bounds.width = width / scale;
|
||||
bounds.height = height / scale;
|
||||
|
||||
viewportBoundingBox.value = bounds;
|
||||
updateViewportBoundingBox();
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return { viewportBoundingBox };
|
||||
return { viewportBoundingBox, updateViewportBoundingBox, viewportBaseBoundingBox, updateViewportBaseBoundingBox };
|
||||
}
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { useWorkflowStores } from "@/composables/workflowStores";
|
||||
|
||||
import { AxisAlignedBoundingBox } from "../modules/geometry";
|
||||
|
||||
export function useWorkflowBoundingBox(workflowId?: string) {
|
||||
const { stepStore, stateStore, commentStore } = useWorkflowStores(workflowId);
|
||||
|
||||
const getWorkflowBoundingBox = () => {
|
||||
const aabb = new AxisAlignedBoundingBox();
|
||||
|
||||
Object.values(stepStore.steps).forEach((step) => {
|
||||
const rect = stateStore.stepPosition[step.id];
|
||||
|
||||
if (rect) {
|
||||
aabb.fitRectangle({
|
||||
x: step.position!.left,
|
||||
y: step.position!.top,
|
||||
width: rect.width,
|
||||
height: rect.height,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
commentStore.comments.forEach((comment) => {
|
||||
aabb.fitRectangle({
|
||||
x: comment.position[0],
|
||||
y: comment.position[1],
|
||||
width: comment.size[0],
|
||||
height: comment.size[1],
|
||||
});
|
||||
});
|
||||
|
||||
return aabb;
|
||||
};
|
||||
|
||||
return {
|
||||
getWorkflowBoundingBox,
|
||||
};
|
||||
}
|
||||
@@ -117,6 +117,31 @@ export class AxisAlignedBoundingBox implements Rectangle {
|
||||
this.endY > other.y
|
||||
);
|
||||
}
|
||||
|
||||
move(position: Vector) {
|
||||
const storedWidth = this.width;
|
||||
const storedHeight = this.height;
|
||||
|
||||
this.x = position[0];
|
||||
this.y = position[1];
|
||||
|
||||
this.width = storedWidth;
|
||||
this.height = storedHeight;
|
||||
}
|
||||
|
||||
scale(factor: number) {
|
||||
this.width *= factor;
|
||||
this.height *= factor;
|
||||
}
|
||||
|
||||
transformTo(other: Rectangle): Transform {
|
||||
const scale: Vector = [this.width / other.width, this.height / other.height];
|
||||
const offset: Vector = [other.x - this.x, other.y - this.y];
|
||||
|
||||
const transform = new Transform().scale(scale).translate(offset);
|
||||
|
||||
return transform;
|
||||
}
|
||||
}
|
||||
|
||||
/* Format
|
||||
@@ -218,6 +243,14 @@ export class Transform {
|
||||
get scaleY() {
|
||||
return this.matrix[3];
|
||||
}
|
||||
|
||||
get offsetX() {
|
||||
return this.matrix[4];
|
||||
}
|
||||
|
||||
get offsetY() {
|
||||
return this.matrix[5];
|
||||
}
|
||||
}
|
||||
|
||||
/** returns a vector constructed of both vectors smaller coordinates */
|
||||
|
||||
@@ -318,6 +318,16 @@ export const useWorkflowCommentStore = defineScopedStore("workflowCommentStore",
|
||||
});
|
||||
}
|
||||
|
||||
function allCommentBounds() {
|
||||
const bounds = new AxisAlignedBoundingBox();
|
||||
|
||||
comments.value.forEach((frame) => {
|
||||
bounds.fitRectangle(commentToRectangle(frame));
|
||||
});
|
||||
|
||||
return bounds;
|
||||
}
|
||||
|
||||
return {
|
||||
commentsRecord,
|
||||
comments,
|
||||
@@ -342,5 +352,6 @@ export const useWorkflowCommentStore = defineScopedStore("workflowCommentStore",
|
||||
resolveCommentsInFrames,
|
||||
resolveStepsInFrames,
|
||||
$reset,
|
||||
allCommentBounds,
|
||||
};
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user