Compare commits
10 Commits
sno-licens
...
minimap2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e4085bdeb4 | ||
|
|
0066fdf0b7 | ||
|
|
d55c68eb9c | ||
|
|
7d5ab8faa4 | ||
|
|
38934f3915 | ||
|
|
2ffde2c4e1 | ||
|
|
b744d4d133 | ||
|
|
f3def8373a | ||
|
|
09088c773a | ||
|
|
0e7bdcf209 |
|
Before Width: | Height: | Size: 83 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 98 KiB After Width: | Height: | Size: 98 KiB |
@@ -9,6 +9,10 @@ import {
|
|||||||
import { type NodeReference } from '../fixtures/utils/litegraphUtils'
|
import { type NodeReference } from '../fixtures/utils/litegraphUtils'
|
||||||
|
|
||||||
test.describe('Item Interaction', () => {
|
test.describe('Item Interaction', () => {
|
||||||
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
|
await comfyPage.setSetting('Comfy.Minimap.Visible', false)
|
||||||
|
})
|
||||||
|
|
||||||
test('Can select/delete all items', async ({ comfyPage }) => {
|
test('Can select/delete all items', async ({ comfyPage }) => {
|
||||||
await comfyPage.loadWorkflow('mixed_graph_items')
|
await comfyPage.loadWorkflow('mixed_graph_items')
|
||||||
await comfyPage.canvas.press('Control+a')
|
await comfyPage.canvas.press('Control+a')
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 80 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 68 KiB |
146
browser_tests/tests/minimap.spec.ts
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
import { expect } from '@playwright/test'
|
||||||
|
|
||||||
|
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
|
||||||
|
|
||||||
|
test.describe('Minimap', () => {
|
||||||
|
test.beforeEach(async ({ comfyPage }) => {
|
||||||
|
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Validate minimap is visible by default', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.loadWorkflow('default')
|
||||||
|
|
||||||
|
await comfyPage.page.waitForFunction(
|
||||||
|
() => window['app'] && window['app'].canvas
|
||||||
|
)
|
||||||
|
|
||||||
|
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
||||||
|
|
||||||
|
await expect(minimapContainer).toBeVisible()
|
||||||
|
|
||||||
|
const minimapCanvas = minimapContainer.locator('.minimap-canvas')
|
||||||
|
await expect(minimapCanvas).toBeVisible()
|
||||||
|
|
||||||
|
const minimapViewport = minimapContainer.locator('.minimap-viewport')
|
||||||
|
await expect(minimapViewport).toBeVisible()
|
||||||
|
|
||||||
|
await expect(minimapContainer).toHaveCSS('position', 'absolute')
|
||||||
|
await expect(minimapContainer).toHaveCSS('z-index', '1000')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Validate minimap toggle button state', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.loadWorkflow('default')
|
||||||
|
|
||||||
|
await comfyPage.page.waitForFunction(
|
||||||
|
() => window['app'] && window['app'].canvas
|
||||||
|
)
|
||||||
|
|
||||||
|
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
|
||||||
|
|
||||||
|
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
|
||||||
|
|
||||||
|
await expect(toggleButton).toBeVisible()
|
||||||
|
|
||||||
|
await expect(toggleButton).toHaveClass(/minimap-active/)
|
||||||
|
|
||||||
|
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
||||||
|
await expect(minimapContainer).toBeVisible()
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Validate minimap can be toggled off and on', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.loadWorkflow('default')
|
||||||
|
|
||||||
|
await comfyPage.page.waitForFunction(
|
||||||
|
() => window['app'] && window['app'].canvas
|
||||||
|
)
|
||||||
|
|
||||||
|
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
||||||
|
await comfyPage.setSetting('Comfy.Graph.CanvasMenu', true)
|
||||||
|
const toggleButton = comfyPage.page.getByTestId('toggle-minimap-button')
|
||||||
|
|
||||||
|
await expect(minimapContainer).toBeVisible()
|
||||||
|
await expect(toggleButton).toHaveClass(/minimap-active/)
|
||||||
|
|
||||||
|
await toggleButton.click()
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
|
await expect(minimapContainer).not.toBeVisible()
|
||||||
|
await expect(toggleButton).not.toHaveClass(/minimap-active/)
|
||||||
|
|
||||||
|
await toggleButton.click()
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
|
await expect(minimapContainer).toBeVisible()
|
||||||
|
await expect(toggleButton).toHaveClass(/minimap-active/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Validate minimap position and size', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.loadWorkflow('default')
|
||||||
|
|
||||||
|
await comfyPage.page.waitForFunction(
|
||||||
|
() => window['app'] && window['app'].canvas
|
||||||
|
)
|
||||||
|
|
||||||
|
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
||||||
|
await expect(minimapContainer).toBeVisible()
|
||||||
|
|
||||||
|
const boundingBox = await minimapContainer.boundingBox()
|
||||||
|
expect(boundingBox).not.toBeNull()
|
||||||
|
|
||||||
|
if (boundingBox) {
|
||||||
|
expect(boundingBox.width).toBeGreaterThan(0)
|
||||||
|
expect(boundingBox.height).toBeGreaterThan(0)
|
||||||
|
|
||||||
|
const viewportSize = comfyPage.page.viewportSize()!
|
||||||
|
expect(boundingBox.x + boundingBox.width).toBeCloseTo(
|
||||||
|
viewportSize.width - 90,
|
||||||
|
50
|
||||||
|
)
|
||||||
|
expect(boundingBox.y + boundingBox.height).toBeCloseTo(
|
||||||
|
viewportSize.height - 20,
|
||||||
|
50
|
||||||
|
)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Validate minimap canvas dimensions', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.loadWorkflow('default')
|
||||||
|
|
||||||
|
await comfyPage.page.waitForFunction(
|
||||||
|
() => window['app'] && window['app'].canvas
|
||||||
|
)
|
||||||
|
|
||||||
|
const minimapCanvas = comfyPage.page.locator(
|
||||||
|
'.litegraph-minimap .minimap-canvas'
|
||||||
|
)
|
||||||
|
await expect(minimapCanvas).toBeVisible()
|
||||||
|
|
||||||
|
const width = await minimapCanvas.getAttribute('width')
|
||||||
|
const height = await minimapCanvas.getAttribute('height')
|
||||||
|
|
||||||
|
expect(parseInt(width || '0')).toBeGreaterThan(0)
|
||||||
|
expect(parseInt(height || '0')).toBeGreaterThan(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('Validate minimap keyboard shortcut Alt+M', async ({ comfyPage }) => {
|
||||||
|
await comfyPage.loadWorkflow('default')
|
||||||
|
|
||||||
|
await comfyPage.page.waitForFunction(
|
||||||
|
() => window['app'] && window['app'].canvas
|
||||||
|
)
|
||||||
|
|
||||||
|
const minimapContainer = comfyPage.page.locator('.litegraph-minimap')
|
||||||
|
|
||||||
|
await expect(minimapContainer).toBeVisible()
|
||||||
|
|
||||||
|
await comfyPage.page.keyboard.press('Alt+KeyM')
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
|
await expect(minimapContainer).not.toBeVisible()
|
||||||
|
|
||||||
|
await comfyPage.page.keyboard.press('Alt+KeyM')
|
||||||
|
await comfyPage.nextFrame()
|
||||||
|
|
||||||
|
await expect(minimapContainer).toBeVisible()
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -19,6 +19,12 @@
|
|||||||
<SubgraphBreadcrumb />
|
<SubgraphBreadcrumb />
|
||||||
</div>
|
</div>
|
||||||
<GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
|
<GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
|
||||||
|
|
||||||
|
<MiniMap
|
||||||
|
v-if="comfyAppReady"
|
||||||
|
ref="minimapRef"
|
||||||
|
class="pointer-events-auto"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</LiteGraphCanvasSplitterOverlay>
|
</LiteGraphCanvasSplitterOverlay>
|
||||||
<GraphCanvasMenu v-if="!betaMenuEnabled && canvasMenuEnabled" />
|
<GraphCanvasMenu v-if="!betaMenuEnabled && canvasMenuEnabled" />
|
||||||
@@ -53,6 +59,7 @@ import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
|
|||||||
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
|
||||||
import DomWidgets from '@/components/graph/DomWidgets.vue'
|
import DomWidgets from '@/components/graph/DomWidgets.vue'
|
||||||
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
||||||
|
import MiniMap from '@/components/graph/MiniMap.vue'
|
||||||
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||||
import SelectionOverlay from '@/components/graph/SelectionOverlay.vue'
|
import SelectionOverlay from '@/components/graph/SelectionOverlay.vue'
|
||||||
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
|
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
|
||||||
@@ -67,6 +74,7 @@ import { useContextMenuTranslation } from '@/composables/useContextMenuTranslati
|
|||||||
import { useCopy } from '@/composables/useCopy'
|
import { useCopy } from '@/composables/useCopy'
|
||||||
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
|
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
|
||||||
import { useLitegraphSettings } from '@/composables/useLitegraphSettings'
|
import { useLitegraphSettings } from '@/composables/useLitegraphSettings'
|
||||||
|
import { useMinimap } from '@/composables/useMinimap'
|
||||||
import { usePaste } from '@/composables/usePaste'
|
import { usePaste } from '@/composables/usePaste'
|
||||||
import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
|
import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
|
||||||
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
||||||
@@ -114,6 +122,9 @@ const selectionToolboxEnabled = computed(() =>
|
|||||||
settingStore.get('Comfy.Canvas.SelectionToolbox')
|
settingStore.get('Comfy.Canvas.SelectionToolbox')
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const minimapRef = ref<InstanceType<typeof MiniMap>>()
|
||||||
|
const minimap = useMinimap()
|
||||||
|
|
||||||
watchEffect(() => {
|
watchEffect(() => {
|
||||||
nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')
|
nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')
|
||||||
})
|
})
|
||||||
@@ -345,6 +356,13 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
whenever(
|
||||||
|
() => minimapRef.value,
|
||||||
|
(ref) => {
|
||||||
|
minimap.setMinimapRef(ref)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
whenever(
|
whenever(
|
||||||
() => useCanvasStore().canvas,
|
() => useCanvasStore().canvas,
|
||||||
(canvas) => {
|
(canvas) => {
|
||||||
|
|||||||
@@ -56,6 +56,15 @@
|
|||||||
data-testid="toggle-link-visibility-button"
|
data-testid="toggle-link-visibility-button"
|
||||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
|
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
|
||||||
/>
|
/>
|
||||||
|
<Button
|
||||||
|
v-tooltip.left="t('graphCanvasMenu.toggleMinimap') + ' [Atl + M]'"
|
||||||
|
severity="secondary"
|
||||||
|
:icon="'pi pi-map'"
|
||||||
|
:aria-label="$t('graphCanvasMenu.toggleMinimap')"
|
||||||
|
:class="{ 'minimap-active': minimap.visible.value }"
|
||||||
|
data-testid="toggle-minimap-button"
|
||||||
|
@click="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')"
|
||||||
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -66,6 +75,7 @@ import ButtonGroup from 'primevue/buttongroup'
|
|||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { useI18n } from 'vue-i18n'
|
import { useI18n } from 'vue-i18n'
|
||||||
|
|
||||||
|
import { useMinimap } from '@/composables/useMinimap'
|
||||||
import { useCommandStore } from '@/stores/commandStore'
|
import { useCommandStore } from '@/stores/commandStore'
|
||||||
import { useCanvasStore } from '@/stores/graphStore'
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
import { useSettingStore } from '@/stores/settingStore'
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
@@ -74,6 +84,7 @@ const { t } = useI18n()
|
|||||||
const commandStore = useCommandStore()
|
const commandStore = useCommandStore()
|
||||||
const canvasStore = useCanvasStore()
|
const canvasStore = useCanvasStore()
|
||||||
const settingStore = useSettingStore()
|
const settingStore = useSettingStore()
|
||||||
|
const minimap = useMinimap()
|
||||||
|
|
||||||
const linkHidden = computed(
|
const linkHidden = computed(
|
||||||
() => settingStore.get('Comfy.LinkRenderMode') === LiteGraph.HIDDEN_LINK
|
() => settingStore.get('Comfy.LinkRenderMode') === LiteGraph.HIDDEN_LINK
|
||||||
@@ -107,4 +118,15 @@ const stopRepeat = () => {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.p-button.minimap-active {
|
||||||
|
background-color: var(--p-button-primary-background);
|
||||||
|
border-color: var(--p-button-primary-border-color);
|
||||||
|
color: var(--p-button-primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-button.minimap-active:hover {
|
||||||
|
background-color: var(--p-button-primary-hover-background);
|
||||||
|
border-color: var(--p-button-primary-hover-border-color);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
88
src/components/graph/MiniMap.vue
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
v-if="visible"
|
||||||
|
ref="containerRef"
|
||||||
|
class="litegraph-minimap absolute bottom-[20px] right-[90px] z-[1000]"
|
||||||
|
:style="containerStyles"
|
||||||
|
@mousedown="handleMouseDown"
|
||||||
|
@mousemove="handleMouseMove"
|
||||||
|
@mouseup="handleMouseUp"
|
||||||
|
@mouseleave="handleMouseUp"
|
||||||
|
@wheel="handleWheel"
|
||||||
|
>
|
||||||
|
<canvas
|
||||||
|
ref="canvasRef"
|
||||||
|
:width="width"
|
||||||
|
:height="height"
|
||||||
|
class="minimap-canvas"
|
||||||
|
/>
|
||||||
|
<div class="minimap-viewport" :style="viewportStyles" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { onMounted, onUnmounted, watch } from 'vue'
|
||||||
|
|
||||||
|
import { useMinimap } from '@/composables/useMinimap'
|
||||||
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
|
|
||||||
|
const minimap = useMinimap()
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
|
|
||||||
|
const {
|
||||||
|
initialized,
|
||||||
|
visible,
|
||||||
|
containerRef,
|
||||||
|
canvasRef,
|
||||||
|
containerStyles,
|
||||||
|
viewportStyles,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
init,
|
||||||
|
destroy,
|
||||||
|
handleMouseDown,
|
||||||
|
handleMouseMove,
|
||||||
|
handleMouseUp,
|
||||||
|
handleWheel
|
||||||
|
} = minimap
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => canvasStore.canvas,
|
||||||
|
async (canvas) => {
|
||||||
|
if (canvas && !initialized.value) {
|
||||||
|
await init()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
if (canvasStore.canvas) {
|
||||||
|
await init()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
destroy()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.litegraph-minimap {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-canvas {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.minimap-viewport {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
import { Point } from '@comfyorg/litegraph'
|
import { Point } from '@comfyorg/litegraph'
|
||||||
|
|
||||||
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
|
||||||
|
import { useMinimap } from '@/composables/useMinimap'
|
||||||
import {
|
import {
|
||||||
DEFAULT_DARK_COLOR_PALETTE,
|
DEFAULT_DARK_COLOR_PALETTE,
|
||||||
DEFAULT_LIGHT_COLOR_PALETTE
|
DEFAULT_LIGHT_COLOR_PALETTE
|
||||||
@@ -313,6 +314,15 @@ export function useCoreCommands(): ComfyCommand[] {
|
|||||||
}
|
}
|
||||||
})()
|
})()
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'Comfy.Canvas.ToggleMinimap',
|
||||||
|
icon: 'pi pi-map',
|
||||||
|
label: 'Canvas Toggle Minimap',
|
||||||
|
versionAdded: '1.24.1',
|
||||||
|
function: async () => {
|
||||||
|
await useMinimap().toggle()
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'Comfy.QueuePrompt',
|
id: 'Comfy.QueuePrompt',
|
||||||
icon: 'pi pi-play',
|
icon: 'pi pi-play',
|
||||||
|
|||||||
631
src/composables/useMinimap.ts
Normal file
@@ -0,0 +1,631 @@
|
|||||||
|
import { useRafFn, useThrottleFn } from '@vueuse/core'
|
||||||
|
import { computed, nextTick, ref, watch } from 'vue'
|
||||||
|
|
||||||
|
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||||
|
import { api } from '@/scripts/api'
|
||||||
|
import { useCanvasStore } from '@/stores/graphStore'
|
||||||
|
import { useSettingStore } from '@/stores/settingStore'
|
||||||
|
|
||||||
|
const globalState = {
|
||||||
|
visible: ref(true),
|
||||||
|
minimapRef: ref<any>(null),
|
||||||
|
initialized: false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useMinimap() {
|
||||||
|
const settingStore = useSettingStore()
|
||||||
|
const canvasStore = useCanvasStore()
|
||||||
|
|
||||||
|
const containerRef = ref<HTMLDivElement>()
|
||||||
|
const canvasRef = ref<HTMLCanvasElement>()
|
||||||
|
|
||||||
|
const visible = globalState.visible
|
||||||
|
|
||||||
|
const initialized = ref(false)
|
||||||
|
const bounds = ref({
|
||||||
|
minX: 0,
|
||||||
|
minY: 0,
|
||||||
|
maxX: 0,
|
||||||
|
maxY: 0,
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
})
|
||||||
|
const scale = ref(1)
|
||||||
|
const isDragging = ref(false)
|
||||||
|
const viewportTransform = ref({ x: 0, y: 0, width: 0, height: 0 })
|
||||||
|
|
||||||
|
const needsFullRedraw = ref(true)
|
||||||
|
const needsBoundsUpdate = ref(true)
|
||||||
|
const lastNodeCount = ref(0)
|
||||||
|
const nodeStatesCache = new Map<NodeId, string>()
|
||||||
|
const linksCache = ref<string>('')
|
||||||
|
|
||||||
|
const updateFlags = ref({
|
||||||
|
bounds: false,
|
||||||
|
nodes: false,
|
||||||
|
connections: false,
|
||||||
|
viewport: false
|
||||||
|
})
|
||||||
|
|
||||||
|
const width = 250
|
||||||
|
const height = 200
|
||||||
|
const nodeColor = '#666'
|
||||||
|
const viewportColor = '#FFF'
|
||||||
|
const backgroundColor = '#1e1e1e'
|
||||||
|
const borderColor = '#444'
|
||||||
|
|
||||||
|
const containerRect = ref({
|
||||||
|
left: 0,
|
||||||
|
top: 0,
|
||||||
|
width: width,
|
||||||
|
height: height
|
||||||
|
})
|
||||||
|
|
||||||
|
const canvasDimensions = ref({
|
||||||
|
width: 0,
|
||||||
|
height: 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateContainerRect = () => {
|
||||||
|
if (!containerRef.value) return
|
||||||
|
|
||||||
|
const rect = containerRef.value.getBoundingClientRect()
|
||||||
|
containerRect.value = {
|
||||||
|
left: rect.left,
|
||||||
|
top: rect.top,
|
||||||
|
width: rect.width,
|
||||||
|
height: rect.height
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCanvasDimensions = () => {
|
||||||
|
const c = canvas.value
|
||||||
|
if (!c) return
|
||||||
|
|
||||||
|
const canvasEl = c.canvas
|
||||||
|
const dpr = window.devicePixelRatio || 1
|
||||||
|
|
||||||
|
canvasDimensions.value = {
|
||||||
|
width: canvasEl.clientWidth || canvasEl.width / dpr,
|
||||||
|
height: canvasEl.clientHeight || canvasEl.height / dpr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = computed(() => canvasStore.canvas)
|
||||||
|
const graph = computed(() => canvas.value?.graph)
|
||||||
|
|
||||||
|
const containerStyles = computed(() => ({
|
||||||
|
width: `${width}px`,
|
||||||
|
height: `${height}px`,
|
||||||
|
backgroundColor: backgroundColor,
|
||||||
|
border: `1px solid ${borderColor}`
|
||||||
|
}))
|
||||||
|
|
||||||
|
const viewportStyles = computed(() => ({
|
||||||
|
transform: `translate(${viewportTransform.value.x}px, ${viewportTransform.value.y}px)`,
|
||||||
|
width: `${viewportTransform.value.width}px`,
|
||||||
|
height: `${viewportTransform.value.height}px`,
|
||||||
|
border: `2px solid ${viewportColor}`,
|
||||||
|
backgroundColor: `${viewportColor}33`,
|
||||||
|
willChange: 'transform',
|
||||||
|
backfaceVisibility: 'hidden' as const,
|
||||||
|
perspective: '1000px',
|
||||||
|
pointerEvents: 'none' as const
|
||||||
|
}))
|
||||||
|
|
||||||
|
const calculateGraphBounds = () => {
|
||||||
|
const g = graph.value
|
||||||
|
if (!g?._nodes || g._nodes.length === 0) {
|
||||||
|
return { minX: 0, minY: 0, maxX: 100, maxY: 100, width: 100, height: 100 }
|
||||||
|
}
|
||||||
|
|
||||||
|
let minX = Infinity
|
||||||
|
let minY = Infinity
|
||||||
|
let maxX = -Infinity
|
||||||
|
let maxY = -Infinity
|
||||||
|
|
||||||
|
for (const node of g._nodes) {
|
||||||
|
minX = Math.min(minX, node.pos[0])
|
||||||
|
minY = Math.min(minY, node.pos[1])
|
||||||
|
maxX = Math.max(maxX, node.pos[0] + node.size[0])
|
||||||
|
maxY = Math.max(maxY, node.pos[1] + node.size[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
minX,
|
||||||
|
minY,
|
||||||
|
maxX,
|
||||||
|
maxY,
|
||||||
|
width: maxX - minX,
|
||||||
|
height: maxY - minY
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const calculateScale = () => {
|
||||||
|
if (bounds.value.width === 0 || bounds.value.height === 0) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
const scaleX = width / bounds.value.width
|
||||||
|
const scaleY = height / bounds.value.height
|
||||||
|
|
||||||
|
return Math.min(scaleX, scaleY) * 0.9
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderNodes = (
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
offsetX: number,
|
||||||
|
offsetY: number
|
||||||
|
) => {
|
||||||
|
const g = graph.value
|
||||||
|
if (!g || !g._nodes || g._nodes.length === 0) return
|
||||||
|
|
||||||
|
for (const node of g._nodes) {
|
||||||
|
const x = (node.pos[0] - bounds.value.minX) * scale.value + offsetX
|
||||||
|
const y = (node.pos[1] - bounds.value.minY) * scale.value + offsetY
|
||||||
|
const w = node.size[0] * scale.value
|
||||||
|
const h = node.size[1] * scale.value
|
||||||
|
|
||||||
|
ctx.fillStyle = node.color || node.constructor.color || nodeColor
|
||||||
|
ctx.fillRect(x, y, w, h)
|
||||||
|
|
||||||
|
ctx.strokeStyle = 'rgba(0,0,0,0.2)'
|
||||||
|
ctx.lineWidth = 0.5
|
||||||
|
ctx.strokeRect(x, y, w, h)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderConnections = (
|
||||||
|
ctx: CanvasRenderingContext2D,
|
||||||
|
offsetX: number,
|
||||||
|
offsetY: number
|
||||||
|
) => {
|
||||||
|
const g = graph.value
|
||||||
|
if (!g) return
|
||||||
|
|
||||||
|
ctx.strokeStyle = 'rgba(255,255,255,0.2)'
|
||||||
|
ctx.lineWidth = 2
|
||||||
|
|
||||||
|
for (const node of g._nodes) {
|
||||||
|
if (!node.outputs) continue
|
||||||
|
|
||||||
|
const x1 = (node.pos[0] - bounds.value.minX) * scale.value + offsetX
|
||||||
|
const y1 = (node.pos[1] - bounds.value.minY) * scale.value + offsetY
|
||||||
|
|
||||||
|
for (const output of node.outputs) {
|
||||||
|
if (!output.links) continue
|
||||||
|
|
||||||
|
for (const linkId of output.links) {
|
||||||
|
const link = g.links[linkId]
|
||||||
|
if (!link) continue
|
||||||
|
|
||||||
|
const targetNode = g.getNodeById(link.target_id)
|
||||||
|
if (!targetNode) continue
|
||||||
|
|
||||||
|
const x2 =
|
||||||
|
(targetNode.pos[0] - bounds.value.minX) * scale.value + offsetX
|
||||||
|
const y2 =
|
||||||
|
(targetNode.pos[1] - bounds.value.minY) * scale.value + offsetY
|
||||||
|
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(
|
||||||
|
x1 + node.size[0] * scale.value,
|
||||||
|
y1 + node.size[1] * scale.value * 0.2
|
||||||
|
)
|
||||||
|
ctx.lineTo(x2, y2 + targetNode.size[1] * scale.value * 0.2)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const renderMinimap = () => {
|
||||||
|
if (!canvasRef.value || !graph.value) return
|
||||||
|
|
||||||
|
const ctx = canvasRef.value.getContext('2d')
|
||||||
|
if (!ctx) return
|
||||||
|
|
||||||
|
const needsRedraw =
|
||||||
|
needsFullRedraw.value ||
|
||||||
|
updateFlags.value.nodes ||
|
||||||
|
updateFlags.value.connections
|
||||||
|
|
||||||
|
if (needsRedraw) {
|
||||||
|
ctx.clearRect(0, 0, width, height)
|
||||||
|
|
||||||
|
const offsetX = (width - bounds.value.width * scale.value) / 2
|
||||||
|
const offsetY = (height - bounds.value.height * scale.value) / 2
|
||||||
|
|
||||||
|
renderConnections(ctx, offsetX, offsetY)
|
||||||
|
renderNodes(ctx, offsetX, offsetY)
|
||||||
|
|
||||||
|
needsFullRedraw.value = false
|
||||||
|
updateFlags.value.nodes = false
|
||||||
|
updateFlags.value.connections = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateViewport = () => {
|
||||||
|
const c = canvas.value
|
||||||
|
if (!c) return
|
||||||
|
|
||||||
|
if (
|
||||||
|
canvasDimensions.value.width === 0 ||
|
||||||
|
canvasDimensions.value.height === 0
|
||||||
|
) {
|
||||||
|
updateCanvasDimensions()
|
||||||
|
}
|
||||||
|
|
||||||
|
const ds = c.ds
|
||||||
|
|
||||||
|
const viewportWidth = canvasDimensions.value.width / ds.scale
|
||||||
|
const viewportHeight = canvasDimensions.value.height / ds.scale
|
||||||
|
|
||||||
|
const worldX = -ds.offset[0]
|
||||||
|
const worldY = -ds.offset[1]
|
||||||
|
|
||||||
|
const centerOffsetX = (width - bounds.value.width * scale.value) / 2
|
||||||
|
const centerOffsetY = (height - bounds.value.height * scale.value) / 2
|
||||||
|
|
||||||
|
viewportTransform.value = {
|
||||||
|
x: (worldX - bounds.value.minX) * scale.value + centerOffsetX,
|
||||||
|
y: (worldY - bounds.value.minY) * scale.value + centerOffsetY,
|
||||||
|
width: viewportWidth * scale.value,
|
||||||
|
height: viewportHeight * scale.value
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFlags.value.viewport = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateMinimap = () => {
|
||||||
|
if (needsBoundsUpdate.value || updateFlags.value.bounds) {
|
||||||
|
bounds.value = calculateGraphBounds()
|
||||||
|
scale.value = calculateScale()
|
||||||
|
needsBoundsUpdate.value = false
|
||||||
|
updateFlags.value.bounds = false
|
||||||
|
needsFullRedraw.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
needsFullRedraw.value ||
|
||||||
|
updateFlags.value.nodes ||
|
||||||
|
updateFlags.value.connections
|
||||||
|
) {
|
||||||
|
renderMinimap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkForChanges = useThrottleFn(() => {
|
||||||
|
const g = graph.value
|
||||||
|
if (!g) return
|
||||||
|
|
||||||
|
let structureChanged = false
|
||||||
|
let positionChanged = false
|
||||||
|
let connectionChanged = false
|
||||||
|
|
||||||
|
if (g._nodes.length !== lastNodeCount.value) {
|
||||||
|
structureChanged = true
|
||||||
|
lastNodeCount.value = g._nodes.length
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const node of g._nodes) {
|
||||||
|
const key = node.id
|
||||||
|
const currentState = `${node.pos[0]},${node.pos[1]},${node.size[0]},${node.size[1]}`
|
||||||
|
|
||||||
|
if (nodeStatesCache.get(key) !== currentState) {
|
||||||
|
positionChanged = true
|
||||||
|
nodeStatesCache.set(key, currentState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentLinks = JSON.stringify(g.links || {})
|
||||||
|
if (currentLinks !== linksCache.value) {
|
||||||
|
connectionChanged = true
|
||||||
|
linksCache.value = currentLinks
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentNodeIds = new Set(g._nodes.map((n) => n.id))
|
||||||
|
for (const [nodeId] of nodeStatesCache) {
|
||||||
|
if (!currentNodeIds.has(nodeId)) {
|
||||||
|
nodeStatesCache.delete(nodeId)
|
||||||
|
structureChanged = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structureChanged || positionChanged) {
|
||||||
|
updateFlags.value.bounds = true
|
||||||
|
updateFlags.value.nodes = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (connectionChanged) {
|
||||||
|
updateFlags.value.connections = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if (structureChanged || positionChanged || connectionChanged) {
|
||||||
|
updateMinimap()
|
||||||
|
}
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
const { pause: pauseChangeDetection, resume: resumeChangeDetection } =
|
||||||
|
useRafFn(
|
||||||
|
async () => {
|
||||||
|
if (visible.value) {
|
||||||
|
await checkForChanges()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: false }
|
||||||
|
)
|
||||||
|
|
||||||
|
const { pause: pauseViewportUpdate, resume: resumeViewportUpdate } = useRafFn(
|
||||||
|
() => {
|
||||||
|
updateViewport()
|
||||||
|
},
|
||||||
|
{ immediate: false, fpsLimit: 60 }
|
||||||
|
)
|
||||||
|
|
||||||
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
|
isDragging.value = true
|
||||||
|
updateContainerRect()
|
||||||
|
handleMouseMove(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
if (!isDragging.value || !canvasRef.value || !canvas.value) return
|
||||||
|
|
||||||
|
const x = e.clientX - containerRect.value.left
|
||||||
|
const y = e.clientY - containerRect.value.top
|
||||||
|
|
||||||
|
const offsetX = (width - bounds.value.width * scale.value) / 2
|
||||||
|
const offsetY = (height - bounds.value.height * scale.value) / 2
|
||||||
|
|
||||||
|
const worldX = (x - offsetX) / scale.value + bounds.value.minX
|
||||||
|
const worldY = (y - offsetY) / scale.value + bounds.value.minY
|
||||||
|
|
||||||
|
centerViewOn(worldX, worldY)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMouseUp = () => {
|
||||||
|
isDragging.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleWheel = (e: WheelEvent) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
const c = canvas.value
|
||||||
|
if (!c) return
|
||||||
|
|
||||||
|
if (
|
||||||
|
containerRect.value.left === 0 &&
|
||||||
|
containerRect.value.top === 0 &&
|
||||||
|
containerRef.value
|
||||||
|
) {
|
||||||
|
updateContainerRect()
|
||||||
|
}
|
||||||
|
|
||||||
|
const ds = c.ds
|
||||||
|
const delta = e.deltaY > 0 ? 0.9 : 1.1
|
||||||
|
|
||||||
|
const newScale = ds.scale * delta
|
||||||
|
|
||||||
|
const MIN_SCALE = 0.1
|
||||||
|
const MAX_SCALE = 10
|
||||||
|
|
||||||
|
if (newScale < MIN_SCALE || newScale > MAX_SCALE) return
|
||||||
|
|
||||||
|
const x = e.clientX - containerRect.value.left
|
||||||
|
const y = e.clientY - containerRect.value.top
|
||||||
|
|
||||||
|
const offsetX = (width - bounds.value.width * scale.value) / 2
|
||||||
|
const offsetY = (height - bounds.value.height * scale.value) / 2
|
||||||
|
|
||||||
|
const worldX = (x - offsetX) / scale.value + bounds.value.minX
|
||||||
|
const worldY = (y - offsetY) / scale.value + bounds.value.minY
|
||||||
|
|
||||||
|
ds.scale = newScale
|
||||||
|
|
||||||
|
centerViewOn(worldX, worldY)
|
||||||
|
}
|
||||||
|
|
||||||
|
const centerViewOn = (worldX: number, worldY: number) => {
|
||||||
|
const c = canvas.value
|
||||||
|
if (!c) return
|
||||||
|
|
||||||
|
if (
|
||||||
|
canvasDimensions.value.width === 0 ||
|
||||||
|
canvasDimensions.value.height === 0
|
||||||
|
) {
|
||||||
|
updateCanvasDimensions()
|
||||||
|
}
|
||||||
|
|
||||||
|
const ds = c.ds
|
||||||
|
|
||||||
|
const viewportWidth = canvasDimensions.value.width / ds.scale
|
||||||
|
const viewportHeight = canvasDimensions.value.height / ds.scale
|
||||||
|
|
||||||
|
ds.offset[0] = -(worldX - viewportWidth / 2)
|
||||||
|
ds.offset[1] = -(worldY - viewportHeight / 2)
|
||||||
|
|
||||||
|
updateFlags.value.viewport = true
|
||||||
|
|
||||||
|
c.setDirty(true, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
let originalCallbacks: any = {}
|
||||||
|
|
||||||
|
const handleGraphChanged = useThrottleFn(() => {
|
||||||
|
needsFullRedraw.value = true
|
||||||
|
updateFlags.value.bounds = true
|
||||||
|
updateFlags.value.nodes = true
|
||||||
|
updateFlags.value.connections = true
|
||||||
|
updateMinimap()
|
||||||
|
}, 500)
|
||||||
|
|
||||||
|
const setupEventListeners = () => {
|
||||||
|
const g = graph.value
|
||||||
|
if (!g) return
|
||||||
|
|
||||||
|
originalCallbacks = {
|
||||||
|
onNodeAdded: g.onNodeAdded,
|
||||||
|
onNodeRemoved: g.onNodeRemoved,
|
||||||
|
onConnectionChange: g.onConnectionChange
|
||||||
|
}
|
||||||
|
|
||||||
|
g.onNodeAdded = function (node) {
|
||||||
|
originalCallbacks.onNodeAdded?.call(this, node)
|
||||||
|
|
||||||
|
void handleGraphChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
g.onNodeRemoved = function (node) {
|
||||||
|
originalCallbacks.onNodeRemoved?.call(this, node)
|
||||||
|
nodeStatesCache.delete(node.id)
|
||||||
|
void handleGraphChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
g.onConnectionChange = function (node) {
|
||||||
|
originalCallbacks.onConnectionChange?.call(this, node)
|
||||||
|
|
||||||
|
void handleGraphChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanupEventListeners = () => {
|
||||||
|
const g = graph.value
|
||||||
|
if (!g) return
|
||||||
|
|
||||||
|
if (originalCallbacks.onNodeAdded !== undefined) {
|
||||||
|
g.onNodeAdded = originalCallbacks.onNodeAdded
|
||||||
|
}
|
||||||
|
if (originalCallbacks.onNodeRemoved !== undefined) {
|
||||||
|
g.onNodeRemoved = originalCallbacks.onNodeRemoved
|
||||||
|
}
|
||||||
|
if (originalCallbacks.onConnectionChange !== undefined) {
|
||||||
|
g.onConnectionChange = originalCallbacks.onConnectionChange
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const init = async () => {
|
||||||
|
if (initialized.value) return
|
||||||
|
|
||||||
|
visible.value = settingStore.get('Comfy.Minimap.Visible')
|
||||||
|
|
||||||
|
if (canvas.value && graph.value) {
|
||||||
|
setupEventListeners()
|
||||||
|
|
||||||
|
api.addEventListener('graphChanged', handleGraphChanged)
|
||||||
|
|
||||||
|
if (containerRef.value) {
|
||||||
|
updateContainerRect()
|
||||||
|
}
|
||||||
|
updateCanvasDimensions()
|
||||||
|
|
||||||
|
window.addEventListener('resize', updateContainerRect)
|
||||||
|
window.addEventListener('scroll', updateContainerRect)
|
||||||
|
window.addEventListener('resize', updateCanvasDimensions)
|
||||||
|
|
||||||
|
needsFullRedraw.value = true
|
||||||
|
updateFlags.value.bounds = true
|
||||||
|
updateFlags.value.nodes = true
|
||||||
|
updateFlags.value.connections = true
|
||||||
|
updateFlags.value.viewport = true
|
||||||
|
|
||||||
|
updateMinimap()
|
||||||
|
updateViewport()
|
||||||
|
|
||||||
|
if (visible.value) {
|
||||||
|
resumeChangeDetection()
|
||||||
|
resumeViewportUpdate()
|
||||||
|
}
|
||||||
|
initialized.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const destroy = () => {
|
||||||
|
pauseChangeDetection()
|
||||||
|
pauseViewportUpdate()
|
||||||
|
cleanupEventListeners()
|
||||||
|
|
||||||
|
api.removeEventListener('graphChanged', handleGraphChanged)
|
||||||
|
|
||||||
|
window.removeEventListener('resize', updateContainerRect)
|
||||||
|
window.removeEventListener('scroll', updateContainerRect)
|
||||||
|
window.removeEventListener('resize', updateCanvasDimensions)
|
||||||
|
|
||||||
|
nodeStatesCache.clear()
|
||||||
|
initialized.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
canvas,
|
||||||
|
async (newCanvas, oldCanvas) => {
|
||||||
|
if (oldCanvas) {
|
||||||
|
cleanupEventListeners()
|
||||||
|
pauseChangeDetection()
|
||||||
|
pauseViewportUpdate()
|
||||||
|
api.removeEventListener('graphChanged', handleGraphChanged)
|
||||||
|
window.removeEventListener('resize', updateContainerRect)
|
||||||
|
window.removeEventListener('scroll', updateContainerRect)
|
||||||
|
window.removeEventListener('resize', updateCanvasDimensions)
|
||||||
|
}
|
||||||
|
if (newCanvas && !initialized.value) {
|
||||||
|
await init()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true }
|
||||||
|
)
|
||||||
|
|
||||||
|
watch(visible, async (isVisible) => {
|
||||||
|
if (isVisible) {
|
||||||
|
if (containerRef.value) {
|
||||||
|
updateContainerRect()
|
||||||
|
}
|
||||||
|
updateCanvasDimensions()
|
||||||
|
|
||||||
|
needsFullRedraw.value = true
|
||||||
|
updateFlags.value.bounds = true
|
||||||
|
updateFlags.value.nodes = true
|
||||||
|
updateFlags.value.connections = true
|
||||||
|
updateFlags.value.viewport = true
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
updateMinimap()
|
||||||
|
updateViewport()
|
||||||
|
resumeChangeDetection()
|
||||||
|
resumeViewportUpdate()
|
||||||
|
} else {
|
||||||
|
pauseChangeDetection()
|
||||||
|
pauseViewportUpdate()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggle = async () => {
|
||||||
|
visible.value = !visible.value
|
||||||
|
await settingStore.set('Comfy.Minimap.Visible', visible.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const setMinimapRef = (ref: any) => {
|
||||||
|
globalState.minimapRef.value = ref
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
visible: computed(() => visible.value),
|
||||||
|
initialized: computed(() => initialized.value),
|
||||||
|
|
||||||
|
containerRef,
|
||||||
|
canvasRef,
|
||||||
|
containerStyles,
|
||||||
|
viewportStyles,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
|
||||||
|
init,
|
||||||
|
destroy,
|
||||||
|
toggle,
|
||||||
|
handleMouseDown,
|
||||||
|
handleMouseMove,
|
||||||
|
handleMouseUp,
|
||||||
|
handleWheel,
|
||||||
|
setMinimapRef
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -181,5 +181,12 @@ export const CORE_KEYBINDINGS: Keybinding[] = [
|
|||||||
shift: true
|
shift: true
|
||||||
},
|
},
|
||||||
commandId: 'Comfy.Graph.ConvertToSubgraph'
|
commandId: 'Comfy.Graph.ConvertToSubgraph'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
combo: {
|
||||||
|
key: 'm',
|
||||||
|
alt: true
|
||||||
|
},
|
||||||
|
commandId: 'Comfy.Canvas.ToggleMinimap'
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -812,6 +812,13 @@ export const CORE_SETTINGS: SettingParams[] = [
|
|||||||
defaultValue: false,
|
defaultValue: false,
|
||||||
versionAdded: '1.15.12'
|
versionAdded: '1.15.12'
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: 'Comfy.Minimap.Visible',
|
||||||
|
name: 'Display minimap on canvas',
|
||||||
|
type: 'hidden',
|
||||||
|
defaultValue: true,
|
||||||
|
versionAdded: '1.24.1'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
id: 'Comfy.Workflow.AutoSaveDelay',
|
id: 'Comfy.Workflow.AutoSaveDelay',
|
||||||
name: 'Auto Save Delay (ms)',
|
name: 'Auto Save Delay (ms)',
|
||||||
|
|||||||
@@ -71,6 +71,9 @@
|
|||||||
"Comfy_Canvas_ToggleLock": {
|
"Comfy_Canvas_ToggleLock": {
|
||||||
"label": "Canvas Toggle Lock"
|
"label": "Canvas Toggle Lock"
|
||||||
},
|
},
|
||||||
|
"Comfy_Canvas_ToggleMinimap": {
|
||||||
|
"label": "Canvas Toggle Minimap"
|
||||||
|
},
|
||||||
"Comfy_Canvas_ToggleSelected_Pin": {
|
"Comfy_Canvas_ToggleSelected_Pin": {
|
||||||
"label": "Pin/Unpin Selected Items"
|
"label": "Pin/Unpin Selected Items"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -862,7 +862,8 @@
|
|||||||
"fitView": "Fit View",
|
"fitView": "Fit View",
|
||||||
"selectMode": "Select Mode",
|
"selectMode": "Select Mode",
|
||||||
"panMode": "Pan Mode",
|
"panMode": "Pan Mode",
|
||||||
"toggleLinkVisibility": "Toggle Link Visibility"
|
"toggleLinkVisibility": "Toggle Link Visibility",
|
||||||
|
"toggleMinimap": "Toggle Minimap"
|
||||||
},
|
},
|
||||||
"groupNode": {
|
"groupNode": {
|
||||||
"create": "Create group node",
|
"create": "Create group node",
|
||||||
@@ -938,6 +939,7 @@
|
|||||||
"Resize Selected Nodes": "Resize Selected Nodes",
|
"Resize Selected Nodes": "Resize Selected Nodes",
|
||||||
"Canvas Toggle Link Visibility": "Canvas Toggle Link Visibility",
|
"Canvas Toggle Link Visibility": "Canvas Toggle Link Visibility",
|
||||||
"Canvas Toggle Lock": "Canvas Toggle Lock",
|
"Canvas Toggle Lock": "Canvas Toggle Lock",
|
||||||
|
"Canvas Toggle Minimap": "Canvas Toggle Minimap",
|
||||||
"Pin/Unpin Selected Items": "Pin/Unpin Selected Items",
|
"Pin/Unpin Selected Items": "Pin/Unpin Selected Items",
|
||||||
"Bypass/Unbypass Selected Nodes": "Bypass/Unbypass Selected Nodes",
|
"Bypass/Unbypass Selected Nodes": "Bypass/Unbypass Selected Nodes",
|
||||||
"Collapse/Expand Selected Nodes": "Collapse/Expand Selected Nodes",
|
"Collapse/Expand Selected Nodes": "Collapse/Expand Selected Nodes",
|
||||||
|
|||||||
@@ -71,6 +71,9 @@
|
|||||||
"Comfy_Canvas_ToggleLock": {
|
"Comfy_Canvas_ToggleLock": {
|
||||||
"label": "Alternar bloqueo en lienzo"
|
"label": "Alternar bloqueo en lienzo"
|
||||||
},
|
},
|
||||||
|
"Comfy_Canvas_ToggleMinimap": {
|
||||||
|
"label": "Lienzo Alternar Minimapa"
|
||||||
|
},
|
||||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||||
"label": "Omitir/No omitir nodos seleccionados"
|
"label": "Omitir/No omitir nodos seleccionados"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -392,6 +392,7 @@
|
|||||||
"resetView": "Restablecer vista",
|
"resetView": "Restablecer vista",
|
||||||
"selectMode": "Modo de selección",
|
"selectMode": "Modo de selección",
|
||||||
"toggleLinkVisibility": "Alternar visibilidad de enlace",
|
"toggleLinkVisibility": "Alternar visibilidad de enlace",
|
||||||
|
"toggleMinimap": "Alternar minimapa",
|
||||||
"zoomIn": "Acercar",
|
"zoomIn": "Acercar",
|
||||||
"zoomOut": "Alejar"
|
"zoomOut": "Alejar"
|
||||||
},
|
},
|
||||||
@@ -721,6 +722,7 @@
|
|||||||
"Bypass/Unbypass Selected Nodes": "Evitar/No evitar nodos seleccionados",
|
"Bypass/Unbypass Selected Nodes": "Evitar/No evitar nodos seleccionados",
|
||||||
"Canvas Toggle Link Visibility": "Alternar visibilidad de enlace en lienzo",
|
"Canvas Toggle Link Visibility": "Alternar visibilidad de enlace en lienzo",
|
||||||
"Canvas Toggle Lock": "Alternar bloqueo en lienzo",
|
"Canvas Toggle Lock": "Alternar bloqueo en lienzo",
|
||||||
|
"Canvas Toggle Minimap": "Lienzo: Alternar minimapa",
|
||||||
"Check for Updates": "Buscar actualizaciones",
|
"Check for Updates": "Buscar actualizaciones",
|
||||||
"Clear Pending Tasks": "Borrar tareas pendientes",
|
"Clear Pending Tasks": "Borrar tareas pendientes",
|
||||||
"Clear Workflow": "Borrar flujo de trabajo",
|
"Clear Workflow": "Borrar flujo de trabajo",
|
||||||
|
|||||||
@@ -71,6 +71,9 @@
|
|||||||
"Comfy_Canvas_ToggleLock": {
|
"Comfy_Canvas_ToggleLock": {
|
||||||
"label": "Basculer le verrouillage du canevas"
|
"label": "Basculer le verrouillage du canevas"
|
||||||
},
|
},
|
||||||
|
"Comfy_Canvas_ToggleMinimap": {
|
||||||
|
"label": "Basculer la mini-carte du canevas"
|
||||||
|
},
|
||||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||||
"label": "Contourner/Ne pas contourner les nœuds sélectionnés"
|
"label": "Contourner/Ne pas contourner les nœuds sélectionnés"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -392,6 +392,7 @@
|
|||||||
"resetView": "Réinitialiser la vue",
|
"resetView": "Réinitialiser la vue",
|
||||||
"selectMode": "Mode sélection",
|
"selectMode": "Mode sélection",
|
||||||
"toggleLinkVisibility": "Basculer la visibilité des liens",
|
"toggleLinkVisibility": "Basculer la visibilité des liens",
|
||||||
|
"toggleMinimap": "Afficher/masquer la mini-carte",
|
||||||
"zoomIn": "Zoom avant",
|
"zoomIn": "Zoom avant",
|
||||||
"zoomOut": "Zoom arrière"
|
"zoomOut": "Zoom arrière"
|
||||||
},
|
},
|
||||||
@@ -721,6 +722,7 @@
|
|||||||
"Bypass/Unbypass Selected Nodes": "Contourner/Ne pas contourner les nœuds sélectionnés",
|
"Bypass/Unbypass Selected Nodes": "Contourner/Ne pas contourner les nœuds sélectionnés",
|
||||||
"Canvas Toggle Link Visibility": "Basculer la visibilité du lien de la toile",
|
"Canvas Toggle Link Visibility": "Basculer la visibilité du lien de la toile",
|
||||||
"Canvas Toggle Lock": "Basculer le verrouillage de la toile",
|
"Canvas Toggle Lock": "Basculer le verrouillage de la toile",
|
||||||
|
"Canvas Toggle Minimap": "Canvas : basculer la mini-carte",
|
||||||
"Check for Updates": "Vérifier les mises à jour",
|
"Check for Updates": "Vérifier les mises à jour",
|
||||||
"Clear Pending Tasks": "Effacer les tâches en attente",
|
"Clear Pending Tasks": "Effacer les tâches en attente",
|
||||||
"Clear Workflow": "Effacer le flux de travail",
|
"Clear Workflow": "Effacer le flux de travail",
|
||||||
|
|||||||
@@ -71,6 +71,9 @@
|
|||||||
"Comfy_Canvas_ToggleLock": {
|
"Comfy_Canvas_ToggleLock": {
|
||||||
"label": "キャンバスのロックを切り替える"
|
"label": "キャンバスのロックを切り替える"
|
||||||
},
|
},
|
||||||
|
"Comfy_Canvas_ToggleMinimap": {
|
||||||
|
"label": "キャンバス ミニマップの切り替え"
|
||||||
|
},
|
||||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||||
"label": "選択したノードのバイパス/バイパス解除"
|
"label": "選択したノードのバイパス/バイパス解除"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -392,6 +392,7 @@
|
|||||||
"resetView": "ビューをリセット",
|
"resetView": "ビューをリセット",
|
||||||
"selectMode": "選択モード",
|
"selectMode": "選択モード",
|
||||||
"toggleLinkVisibility": "リンクの表示切り替え",
|
"toggleLinkVisibility": "リンクの表示切り替え",
|
||||||
|
"toggleMinimap": "ミニマップの切り替え",
|
||||||
"zoomIn": "拡大",
|
"zoomIn": "拡大",
|
||||||
"zoomOut": "縮小"
|
"zoomOut": "縮小"
|
||||||
},
|
},
|
||||||
@@ -721,6 +722,7 @@
|
|||||||
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
|
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
|
||||||
"Canvas Toggle Link Visibility": "キャンバスのリンク表示を切り替え",
|
"Canvas Toggle Link Visibility": "キャンバスのリンク表示を切り替え",
|
||||||
"Canvas Toggle Lock": "キャンバスのロックを切り替え",
|
"Canvas Toggle Lock": "キャンバスのロックを切り替え",
|
||||||
|
"Canvas Toggle Minimap": "キャンバス ミニマップの切り替え",
|
||||||
"Check for Updates": "更新を確認する",
|
"Check for Updates": "更新を確認する",
|
||||||
"Clear Pending Tasks": "保留中のタスクをクリア",
|
"Clear Pending Tasks": "保留中のタスクをクリア",
|
||||||
"Clear Workflow": "ワークフローをクリア",
|
"Clear Workflow": "ワークフローをクリア",
|
||||||
|
|||||||
@@ -71,6 +71,9 @@
|
|||||||
"Comfy_Canvas_ToggleLock": {
|
"Comfy_Canvas_ToggleLock": {
|
||||||
"label": "캔버스 잠금 토글"
|
"label": "캔버스 잠금 토글"
|
||||||
},
|
},
|
||||||
|
"Comfy_Canvas_ToggleMinimap": {
|
||||||
|
"label": "캔버스 미니맵 토글"
|
||||||
|
},
|
||||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||||
"label": "선택한 노드 우회/우회 해제"
|
"label": "선택한 노드 우회/우회 해제"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -392,6 +392,7 @@
|
|||||||
"resetView": "보기 재설정",
|
"resetView": "보기 재설정",
|
||||||
"selectMode": "선택 모드",
|
"selectMode": "선택 모드",
|
||||||
"toggleLinkVisibility": "링크 가시성 전환",
|
"toggleLinkVisibility": "링크 가시성 전환",
|
||||||
|
"toggleMinimap": "미니맵 전환",
|
||||||
"zoomIn": "확대",
|
"zoomIn": "확대",
|
||||||
"zoomOut": "축소"
|
"zoomOut": "축소"
|
||||||
},
|
},
|
||||||
@@ -721,6 +722,7 @@
|
|||||||
"Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제",
|
"Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제",
|
||||||
"Canvas Toggle Link Visibility": "캔버스 토글 링크 가시성",
|
"Canvas Toggle Link Visibility": "캔버스 토글 링크 가시성",
|
||||||
"Canvas Toggle Lock": "캔버스 토글 잠금",
|
"Canvas Toggle Lock": "캔버스 토글 잠금",
|
||||||
|
"Canvas Toggle Minimap": "캔버스 미니맵 전환",
|
||||||
"Check for Updates": "업데이트 확인",
|
"Check for Updates": "업데이트 확인",
|
||||||
"Clear Pending Tasks": "보류 중인 작업 제거하기",
|
"Clear Pending Tasks": "보류 중인 작업 제거하기",
|
||||||
"Clear Workflow": "워크플로 지우기",
|
"Clear Workflow": "워크플로 지우기",
|
||||||
|
|||||||
@@ -71,6 +71,9 @@
|
|||||||
"Comfy_Canvas_ToggleLock": {
|
"Comfy_Canvas_ToggleLock": {
|
||||||
"label": "Переключить блокировку холста"
|
"label": "Переключить блокировку холста"
|
||||||
},
|
},
|
||||||
|
"Comfy_Canvas_ToggleMinimap": {
|
||||||
|
"label": "Полотно: Показать/скрыть миникарту"
|
||||||
|
},
|
||||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||||
"label": "Обход/Необход выбранных нод"
|
"label": "Обход/Необход выбранных нод"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -392,6 +392,7 @@
|
|||||||
"resetView": "Сбросить вид",
|
"resetView": "Сбросить вид",
|
||||||
"selectMode": "Выбрать режим",
|
"selectMode": "Выбрать режим",
|
||||||
"toggleLinkVisibility": "Переключить видимость ссылок",
|
"toggleLinkVisibility": "Переключить видимость ссылок",
|
||||||
|
"toggleMinimap": "Показать/скрыть миникарту",
|
||||||
"zoomIn": "Увеличить",
|
"zoomIn": "Увеличить",
|
||||||
"zoomOut": "Уменьшить"
|
"zoomOut": "Уменьшить"
|
||||||
},
|
},
|
||||||
@@ -721,6 +722,7 @@
|
|||||||
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
|
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
|
||||||
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
|
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
|
||||||
"Canvas Toggle Lock": "Переключение блокировки холста",
|
"Canvas Toggle Lock": "Переключение блокировки холста",
|
||||||
|
"Canvas Toggle Minimap": "Показать/скрыть миникарту холста",
|
||||||
"Check for Updates": "Проверить наличие обновлений",
|
"Check for Updates": "Проверить наличие обновлений",
|
||||||
"Clear Pending Tasks": "Очистить ожидающие задачи",
|
"Clear Pending Tasks": "Очистить ожидающие задачи",
|
||||||
"Clear Workflow": "Очистить рабочий процесс",
|
"Clear Workflow": "Очистить рабочий процесс",
|
||||||
|
|||||||
@@ -71,6 +71,9 @@
|
|||||||
"Comfy_Canvas_ToggleLock": {
|
"Comfy_Canvas_ToggleLock": {
|
||||||
"label": "畫布切換鎖定"
|
"label": "畫布切換鎖定"
|
||||||
},
|
},
|
||||||
|
"Comfy_Canvas_ToggleMinimap": {
|
||||||
|
"label": "畫布切換小地圖"
|
||||||
|
},
|
||||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||||
"label": "略過/取消略過選取的節點"
|
"label": "略過/取消略過選取的節點"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -392,6 +392,7 @@
|
|||||||
"resetView": "重設視圖",
|
"resetView": "重設視圖",
|
||||||
"selectMode": "選取模式",
|
"selectMode": "選取模式",
|
||||||
"toggleLinkVisibility": "切換連結顯示",
|
"toggleLinkVisibility": "切換連結顯示",
|
||||||
|
"toggleMinimap": "切換小地圖",
|
||||||
"zoomIn": "放大",
|
"zoomIn": "放大",
|
||||||
"zoomOut": "縮小"
|
"zoomOut": "縮小"
|
||||||
},
|
},
|
||||||
@@ -721,6 +722,7 @@
|
|||||||
"Bypass/Unbypass Selected Nodes": "繞過/取消繞過選取節點",
|
"Bypass/Unbypass Selected Nodes": "繞過/取消繞過選取節點",
|
||||||
"Canvas Toggle Link Visibility": "切換連結可見性",
|
"Canvas Toggle Link Visibility": "切換連結可見性",
|
||||||
"Canvas Toggle Lock": "切換畫布鎖定",
|
"Canvas Toggle Lock": "切換畫布鎖定",
|
||||||
|
"Canvas Toggle Minimap": "畫布切換小地圖",
|
||||||
"Check for Updates": "檢查更新",
|
"Check for Updates": "檢查更新",
|
||||||
"Clear Pending Tasks": "清除待處理任務",
|
"Clear Pending Tasks": "清除待處理任務",
|
||||||
"Clear Workflow": "清除工作流程",
|
"Clear Workflow": "清除工作流程",
|
||||||
|
|||||||
@@ -71,6 +71,9 @@
|
|||||||
"Comfy_Canvas_ToggleLock": {
|
"Comfy_Canvas_ToggleLock": {
|
||||||
"label": "锁定视图"
|
"label": "锁定视图"
|
||||||
},
|
},
|
||||||
|
"Comfy_Canvas_ToggleMinimap": {
|
||||||
|
"label": "畫布切換小地圖"
|
||||||
|
},
|
||||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||||
"label": "忽略/取消忽略选中节点"
|
"label": "忽略/取消忽略选中节点"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -392,6 +392,7 @@
|
|||||||
"resetView": "重置视图",
|
"resetView": "重置视图",
|
||||||
"selectMode": "选择模式",
|
"selectMode": "选择模式",
|
||||||
"toggleLinkVisibility": "切换连线可见性",
|
"toggleLinkVisibility": "切换连线可见性",
|
||||||
|
"toggleMinimap": "切換小地圖",
|
||||||
"zoomIn": "放大",
|
"zoomIn": "放大",
|
||||||
"zoomOut": "缩小"
|
"zoomOut": "缩小"
|
||||||
},
|
},
|
||||||
@@ -721,6 +722,7 @@
|
|||||||
"Bypass/Unbypass Selected Nodes": "忽略/取消忽略选定节点",
|
"Bypass/Unbypass Selected Nodes": "忽略/取消忽略选定节点",
|
||||||
"Canvas Toggle Link Visibility": "切换连线可见性",
|
"Canvas Toggle Link Visibility": "切换连线可见性",
|
||||||
"Canvas Toggle Lock": "切换视图锁定",
|
"Canvas Toggle Lock": "切换视图锁定",
|
||||||
|
"Canvas Toggle Minimap": "畫布切換小地圖",
|
||||||
"Check for Updates": "检查更新",
|
"Check for Updates": "检查更新",
|
||||||
"Clear Pending Tasks": "清除待处理任务",
|
"Clear Pending Tasks": "清除待处理任务",
|
||||||
"Clear Workflow": "清除工作流",
|
"Clear Workflow": "清除工作流",
|
||||||
|
|||||||
@@ -457,6 +457,7 @@ const zSettings = z.object({
|
|||||||
'Comfy.TutorialCompleted': z.boolean(),
|
'Comfy.TutorialCompleted': z.boolean(),
|
||||||
'Comfy.InstalledVersion': z.string().nullable(),
|
'Comfy.InstalledVersion': z.string().nullable(),
|
||||||
'Comfy.Node.AllowImageSizeDraw': z.boolean(),
|
'Comfy.Node.AllowImageSizeDraw': z.boolean(),
|
||||||
|
'Comfy.Minimap.Visible': z.boolean(),
|
||||||
'Comfy-Desktop.AutoUpdate': z.boolean(),
|
'Comfy-Desktop.AutoUpdate': z.boolean(),
|
||||||
'Comfy-Desktop.SendStatistics': z.boolean(),
|
'Comfy-Desktop.SendStatistics': z.boolean(),
|
||||||
'Comfy-Desktop.WindowStyle': z.string(),
|
'Comfy-Desktop.WindowStyle': z.string(),
|
||||||
|
|||||||
811
tests-ui/tests/composables/useMinimap.test.ts
Normal file
@@ -0,0 +1,811 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||||
|
import { nextTick } from 'vue'
|
||||||
|
|
||||||
|
const flushPromises = () => new Promise((resolve) => setTimeout(resolve, 0))
|
||||||
|
|
||||||
|
const mockPause = vi.fn()
|
||||||
|
const mockResume = vi.fn()
|
||||||
|
|
||||||
|
vi.mock('@vueuse/core', () => {
|
||||||
|
const callbacks: Record<string, () => void> = {}
|
||||||
|
let callbackId = 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
useRafFn: vi.fn((callback, options) => {
|
||||||
|
const id = callbackId++
|
||||||
|
callbacks[id] = callback
|
||||||
|
|
||||||
|
if (options?.immediate !== false) {
|
||||||
|
void Promise.resolve().then(() => callback())
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
pause: mockPause,
|
||||||
|
resume: vi.fn(() => {
|
||||||
|
mockResume()
|
||||||
|
void Promise.resolve().then(() => callbacks[id]?.())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
useThrottleFn: vi.fn((callback) => {
|
||||||
|
return (...args: any[]) => {
|
||||||
|
return callback(...args)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let mockCanvas: any
|
||||||
|
let mockGraph: any
|
||||||
|
|
||||||
|
const setupMocks = () => {
|
||||||
|
const mockNodes = [
|
||||||
|
{
|
||||||
|
id: 'node1',
|
||||||
|
pos: [0, 0],
|
||||||
|
size: [100, 50],
|
||||||
|
color: '#ff0000',
|
||||||
|
constructor: { color: '#666' },
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
links: ['link1']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node2',
|
||||||
|
pos: [200, 100],
|
||||||
|
size: [150, 75],
|
||||||
|
constructor: { color: '#666' },
|
||||||
|
outputs: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
mockGraph = {
|
||||||
|
_nodes: mockNodes,
|
||||||
|
links: {
|
||||||
|
link1: {
|
||||||
|
id: 'link1',
|
||||||
|
target_id: 'node2'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getNodeById: vi.fn((id) => mockNodes.find((n) => n.id === id)),
|
||||||
|
setDirtyCanvas: vi.fn(),
|
||||||
|
onNodeAdded: null,
|
||||||
|
onNodeRemoved: null,
|
||||||
|
onConnectionChange: null
|
||||||
|
}
|
||||||
|
|
||||||
|
mockCanvas = {
|
||||||
|
graph: mockGraph,
|
||||||
|
canvas: {
|
||||||
|
width: 1000,
|
||||||
|
height: 800,
|
||||||
|
clientWidth: 1000,
|
||||||
|
clientHeight: 800
|
||||||
|
},
|
||||||
|
ds: {
|
||||||
|
scale: 1,
|
||||||
|
offset: [0, 0]
|
||||||
|
},
|
||||||
|
setDirty: vi.fn()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setupMocks()
|
||||||
|
|
||||||
|
const defaultCanvasStore = {
|
||||||
|
canvas: mockCanvas
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultSettingStore = {
|
||||||
|
get: vi.fn().mockReturnValue(true),
|
||||||
|
set: vi.fn().mockResolvedValue(undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
vi.mock('@/stores/graphStore', () => ({
|
||||||
|
useCanvasStore: vi.fn(() => defaultCanvasStore)
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/stores/settingStore', () => ({
|
||||||
|
useSettingStore: vi.fn(() => defaultSettingStore)
|
||||||
|
}))
|
||||||
|
|
||||||
|
vi.mock('@/scripts/api', () => ({
|
||||||
|
api: {
|
||||||
|
addEventListener: vi.fn(),
|
||||||
|
removeEventListener: vi.fn()
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
|
||||||
|
const { useMinimap } = await import('@/composables/useMinimap')
|
||||||
|
const { api } = await import('@/scripts/api')
|
||||||
|
|
||||||
|
describe('useMinimap', () => {
|
||||||
|
let mockCanvas: any
|
||||||
|
let mockGraph: any
|
||||||
|
let mockCanvasElement: any
|
||||||
|
let mockContainerElement: any
|
||||||
|
let mockContext2D: any
|
||||||
|
|
||||||
|
const createAndInitializeMinimap = async () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
await minimap.init()
|
||||||
|
await nextTick()
|
||||||
|
await flushPromises()
|
||||||
|
return minimap
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks()
|
||||||
|
|
||||||
|
mockPause.mockClear()
|
||||||
|
mockResume.mockClear()
|
||||||
|
|
||||||
|
mockContext2D = {
|
||||||
|
clearRect: vi.fn(),
|
||||||
|
fillRect: vi.fn(),
|
||||||
|
strokeRect: vi.fn(),
|
||||||
|
beginPath: vi.fn(),
|
||||||
|
moveTo: vi.fn(),
|
||||||
|
lineTo: vi.fn(),
|
||||||
|
stroke: vi.fn(),
|
||||||
|
fillStyle: '',
|
||||||
|
strokeStyle: '',
|
||||||
|
lineWidth: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
mockCanvasElement = {
|
||||||
|
getContext: vi.fn().mockReturnValue(mockContext2D),
|
||||||
|
width: 250,
|
||||||
|
height: 200,
|
||||||
|
clientWidth: 250,
|
||||||
|
clientHeight: 200
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockRect = {
|
||||||
|
left: 100,
|
||||||
|
top: 100,
|
||||||
|
width: 250,
|
||||||
|
height: 200,
|
||||||
|
right: 350,
|
||||||
|
bottom: 300,
|
||||||
|
x: 100,
|
||||||
|
y: 100
|
||||||
|
}
|
||||||
|
|
||||||
|
mockContainerElement = {
|
||||||
|
getBoundingClientRect: vi.fn(() => ({ ...mockRect }))
|
||||||
|
}
|
||||||
|
|
||||||
|
const mockNodes = [
|
||||||
|
{
|
||||||
|
id: 'node1',
|
||||||
|
pos: [0, 0],
|
||||||
|
size: [100, 50],
|
||||||
|
color: '#ff0000',
|
||||||
|
constructor: { color: '#666' },
|
||||||
|
outputs: [
|
||||||
|
{
|
||||||
|
links: ['link1']
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'node2',
|
||||||
|
pos: [200, 100],
|
||||||
|
size: [150, 75],
|
||||||
|
constructor: { color: '#666' },
|
||||||
|
outputs: []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
mockGraph = {
|
||||||
|
_nodes: mockNodes,
|
||||||
|
links: {
|
||||||
|
link1: {
|
||||||
|
id: 'link1',
|
||||||
|
target_id: 'node2'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getNodeById: vi.fn((id) => mockNodes.find((n) => n.id === id)),
|
||||||
|
setDirtyCanvas: vi.fn(),
|
||||||
|
onNodeAdded: null,
|
||||||
|
onNodeRemoved: null,
|
||||||
|
onConnectionChange: null
|
||||||
|
}
|
||||||
|
|
||||||
|
mockCanvas = {
|
||||||
|
graph: mockGraph,
|
||||||
|
canvas: {
|
||||||
|
width: 1000,
|
||||||
|
height: 800,
|
||||||
|
clientWidth: 1000,
|
||||||
|
clientHeight: 800
|
||||||
|
},
|
||||||
|
ds: {
|
||||||
|
scale: 1,
|
||||||
|
offset: [0, 0]
|
||||||
|
},
|
||||||
|
setDirty: vi.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultCanvasStore.canvas = mockCanvas
|
||||||
|
|
||||||
|
defaultSettingStore.get = vi.fn().mockReturnValue(true)
|
||||||
|
defaultSettingStore.set = vi.fn().mockResolvedValue(undefined)
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'devicePixelRatio', {
|
||||||
|
writable: true,
|
||||||
|
configurable: true,
|
||||||
|
value: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
window.addEventListener = vi.fn()
|
||||||
|
window.removeEventListener = vi.fn()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('initialization', () => {
|
||||||
|
it('should initialize with default values', () => {
|
||||||
|
const originalCanvas = defaultCanvasStore.canvas
|
||||||
|
defaultCanvasStore.canvas = null
|
||||||
|
|
||||||
|
const minimap = useMinimap()
|
||||||
|
|
||||||
|
expect(minimap.width).toBe(250)
|
||||||
|
expect(minimap.height).toBe(200)
|
||||||
|
expect(minimap.visible.value).toBe(true)
|
||||||
|
expect(minimap.initialized.value).toBe(false)
|
||||||
|
|
||||||
|
defaultCanvasStore.canvas = originalCanvas
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should initialize minimap when canvas is available', async () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
|
||||||
|
expect(minimap.initialized.value).toBe(true)
|
||||||
|
expect(defaultSettingStore.get).toHaveBeenCalledWith(
|
||||||
|
'Comfy.Minimap.Visible'
|
||||||
|
)
|
||||||
|
expect(api.addEventListener).toHaveBeenCalledWith(
|
||||||
|
'graphChanged',
|
||||||
|
expect.any(Function)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (minimap.visible.value) {
|
||||||
|
expect(mockResume).toHaveBeenCalled()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not initialize without canvas and graph', async () => {
|
||||||
|
const originalCanvas = defaultCanvasStore.canvas
|
||||||
|
defaultCanvasStore.canvas = null
|
||||||
|
|
||||||
|
const minimap = useMinimap()
|
||||||
|
await minimap.init()
|
||||||
|
|
||||||
|
expect(minimap.initialized.value).toBe(false)
|
||||||
|
expect(api.addEventListener).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
defaultCanvasStore.canvas = originalCanvas
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should setup event listeners on graph', async () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
|
||||||
|
expect(mockGraph.onNodeAdded).toBeDefined()
|
||||||
|
expect(mockGraph.onNodeRemoved).toBeDefined()
|
||||||
|
expect(mockGraph.onConnectionChange).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle visibility from settings', async () => {
|
||||||
|
defaultSettingStore.get.mockReturnValue(false)
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
|
||||||
|
expect(minimap.visible.value).toBe(false)
|
||||||
|
expect(mockResume).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('destroy', () => {
|
||||||
|
it('should cleanup all resources', async () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
minimap.destroy()
|
||||||
|
|
||||||
|
expect(mockPause).toHaveBeenCalled()
|
||||||
|
expect(api.removeEventListener).toHaveBeenCalledWith(
|
||||||
|
'graphChanged',
|
||||||
|
expect.any(Function)
|
||||||
|
)
|
||||||
|
expect(window.removeEventListener).toHaveBeenCalled()
|
||||||
|
expect(minimap.initialized.value).toBe(false)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should restore original graph callbacks', async () => {
|
||||||
|
const originalCallbacks = {
|
||||||
|
onNodeAdded: vi.fn(),
|
||||||
|
onNodeRemoved: vi.fn(),
|
||||||
|
onConnectionChange: vi.fn()
|
||||||
|
}
|
||||||
|
|
||||||
|
mockGraph.onNodeAdded = originalCallbacks.onNodeAdded
|
||||||
|
mockGraph.onNodeRemoved = originalCallbacks.onNodeRemoved
|
||||||
|
mockGraph.onConnectionChange = originalCallbacks.onConnectionChange
|
||||||
|
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
minimap.destroy()
|
||||||
|
|
||||||
|
expect(mockGraph.onNodeAdded).toBe(originalCallbacks.onNodeAdded)
|
||||||
|
expect(mockGraph.onNodeRemoved).toBe(originalCallbacks.onNodeRemoved)
|
||||||
|
expect(mockGraph.onConnectionChange).toBe(
|
||||||
|
originalCallbacks.onConnectionChange
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('toggle', () => {
|
||||||
|
it('should toggle visibility and save to settings', async () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
const initialVisibility = minimap.visible.value
|
||||||
|
|
||||||
|
await minimap.toggle()
|
||||||
|
|
||||||
|
expect(minimap.visible.value).toBe(!initialVisibility)
|
||||||
|
expect(defaultSettingStore.set).toHaveBeenCalledWith(
|
||||||
|
'Comfy.Minimap.Visible',
|
||||||
|
!initialVisibility
|
||||||
|
)
|
||||||
|
|
||||||
|
await minimap.toggle()
|
||||||
|
|
||||||
|
expect(minimap.visible.value).toBe(initialVisibility)
|
||||||
|
expect(defaultSettingStore.set).toHaveBeenCalledWith(
|
||||||
|
'Comfy.Minimap.Visible',
|
||||||
|
initialVisibility
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('rendering', () => {
|
||||||
|
it('should verify context is obtained during render', async () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
const getContextSpy = vi.spyOn(mockCanvasElement, 'getContext')
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
|
||||||
|
expect(getContextSpy).toHaveBeenCalled()
|
||||||
|
expect(getContextSpy).toHaveBeenCalledWith('2d')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should render at least once after initialization', async () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
|
||||||
|
const renderingOccurred =
|
||||||
|
mockContext2D.clearRect.mock.calls.length > 0 ||
|
||||||
|
mockContext2D.fillRect.mock.calls.length > 0
|
||||||
|
|
||||||
|
if (!renderingOccurred) {
|
||||||
|
console.log('Minimap visible:', minimap.visible.value)
|
||||||
|
console.log('Minimap initialized:', minimap.initialized.value)
|
||||||
|
console.log('Canvas exists:', !!defaultCanvasStore.canvas)
|
||||||
|
console.log('Graph exists:', !!defaultCanvasStore.canvas?.graph)
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(renderingOccurred).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not render when context is null', async () => {
|
||||||
|
mockCanvasElement.getContext = vi.fn().mockReturnValue(null)
|
||||||
|
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
|
||||||
|
expect(mockContext2D.clearRect).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
mockCanvasElement.getContext = vi.fn().mockReturnValue(mockContext2D)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle empty graph', async () => {
|
||||||
|
const originalNodes = [...mockGraph._nodes]
|
||||||
|
mockGraph._nodes = []
|
||||||
|
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 100))
|
||||||
|
|
||||||
|
expect(minimap.initialized.value).toBe(true)
|
||||||
|
|
||||||
|
expect(mockContext2D.fillRect).not.toHaveBeenCalled()
|
||||||
|
|
||||||
|
mockGraph._nodes = originalNodes
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('mouse interactions', () => {
|
||||||
|
it('should handle mouse down and start dragging', async () => {
|
||||||
|
const minimap = await createAndInitializeMinimap()
|
||||||
|
|
||||||
|
const mouseEvent = new MouseEvent('mousedown', {
|
||||||
|
clientX: 150,
|
||||||
|
clientY: 150
|
||||||
|
})
|
||||||
|
|
||||||
|
minimap.handleMouseDown(mouseEvent)
|
||||||
|
|
||||||
|
expect(mockContainerElement.getBoundingClientRect).toHaveBeenCalled()
|
||||||
|
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle mouse move while dragging', async () => {
|
||||||
|
const minimap = await createAndInitializeMinimap()
|
||||||
|
|
||||||
|
const mouseDownEvent = new MouseEvent('mousedown', {
|
||||||
|
clientX: 150,
|
||||||
|
clientY: 150
|
||||||
|
})
|
||||||
|
minimap.handleMouseDown(mouseDownEvent)
|
||||||
|
|
||||||
|
const mouseMoveEvent = new MouseEvent('mousemove', {
|
||||||
|
clientX: 200,
|
||||||
|
clientY: 200
|
||||||
|
})
|
||||||
|
minimap.handleMouseMove(mouseMoveEvent)
|
||||||
|
|
||||||
|
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||||
|
expect(mockCanvas.ds.offset).toBeDefined()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should not move when not dragging', async () => {
|
||||||
|
const minimap = await createAndInitializeMinimap()
|
||||||
|
|
||||||
|
mockCanvas.setDirty.mockClear()
|
||||||
|
|
||||||
|
const mouseMoveEvent = new MouseEvent('mousemove', {
|
||||||
|
clientX: 200,
|
||||||
|
clientY: 200
|
||||||
|
})
|
||||||
|
minimap.handleMouseMove(mouseMoveEvent)
|
||||||
|
|
||||||
|
expect(mockCanvas.setDirty).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle mouse up to stop dragging', async () => {
|
||||||
|
const minimap = await createAndInitializeMinimap()
|
||||||
|
|
||||||
|
const mouseDownEvent = new MouseEvent('mousedown', {
|
||||||
|
clientX: 150,
|
||||||
|
clientY: 150
|
||||||
|
})
|
||||||
|
minimap.handleMouseDown(mouseDownEvent)
|
||||||
|
|
||||||
|
minimap.handleMouseUp()
|
||||||
|
|
||||||
|
mockCanvas.setDirty.mockClear()
|
||||||
|
|
||||||
|
const mouseMoveEvent = new MouseEvent('mousemove', {
|
||||||
|
clientX: 200,
|
||||||
|
clientY: 200
|
||||||
|
})
|
||||||
|
minimap.handleMouseMove(mouseMoveEvent)
|
||||||
|
|
||||||
|
expect(mockCanvas.setDirty).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('wheel interactions', () => {
|
||||||
|
it('should handle wheel zoom in', async () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
|
||||||
|
const wheelEvent = new WheelEvent('wheel', {
|
||||||
|
deltaY: -100,
|
||||||
|
clientX: 150,
|
||||||
|
clientY: 150
|
||||||
|
})
|
||||||
|
|
||||||
|
const preventDefault = vi.fn()
|
||||||
|
Object.defineProperty(wheelEvent, 'preventDefault', {
|
||||||
|
value: preventDefault
|
||||||
|
})
|
||||||
|
|
||||||
|
minimap.handleWheel(wheelEvent)
|
||||||
|
|
||||||
|
expect(preventDefault).toHaveBeenCalled()
|
||||||
|
expect(mockCanvas.ds.scale).toBeCloseTo(1.1)
|
||||||
|
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle wheel zoom out', async () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
|
||||||
|
const wheelEvent = new WheelEvent('wheel', {
|
||||||
|
deltaY: 100,
|
||||||
|
clientX: 150,
|
||||||
|
clientY: 150
|
||||||
|
})
|
||||||
|
|
||||||
|
const preventDefault = vi.fn()
|
||||||
|
Object.defineProperty(wheelEvent, 'preventDefault', {
|
||||||
|
value: preventDefault
|
||||||
|
})
|
||||||
|
|
||||||
|
minimap.handleWheel(wheelEvent)
|
||||||
|
|
||||||
|
expect(preventDefault).toHaveBeenCalled()
|
||||||
|
expect(mockCanvas.ds.scale).toBeCloseTo(0.9)
|
||||||
|
expect(mockCanvas.setDirty).toHaveBeenCalledWith(true, true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should respect zoom limits', async () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
|
||||||
|
mockCanvas.ds.scale = 0.1
|
||||||
|
|
||||||
|
const wheelEvent = new WheelEvent('wheel', {
|
||||||
|
deltaY: 100,
|
||||||
|
clientX: 150,
|
||||||
|
clientY: 150
|
||||||
|
})
|
||||||
|
|
||||||
|
const preventDefault = vi.fn()
|
||||||
|
Object.defineProperty(wheelEvent, 'preventDefault', {
|
||||||
|
value: preventDefault
|
||||||
|
})
|
||||||
|
|
||||||
|
minimap.handleWheel(wheelEvent)
|
||||||
|
|
||||||
|
expect(mockCanvas.ds.scale).toBe(0.1)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should update container rect if needed', async () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
|
||||||
|
const wheelEvent = new WheelEvent('wheel', {
|
||||||
|
deltaY: -100,
|
||||||
|
clientX: 150,
|
||||||
|
clientY: 150
|
||||||
|
})
|
||||||
|
|
||||||
|
const preventDefault = vi.fn()
|
||||||
|
Object.defineProperty(wheelEvent, 'preventDefault', {
|
||||||
|
value: preventDefault
|
||||||
|
})
|
||||||
|
|
||||||
|
minimap.handleWheel(wheelEvent)
|
||||||
|
|
||||||
|
expect(mockContainerElement.getBoundingClientRect).toHaveBeenCalled()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('viewport updates', () => {
|
||||||
|
it('should update viewport transform correctly', async () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
const viewportStyles = minimap.viewportStyles.value
|
||||||
|
|
||||||
|
expect(viewportStyles).toBeDefined()
|
||||||
|
expect(viewportStyles.transform).toMatch(
|
||||||
|
/translate\(-?\d+(\.\d+)?px, -?\d+(\.\d+)?px\)/
|
||||||
|
)
|
||||||
|
expect(viewportStyles.width).toMatch(/\d+(\.\d+)?px/)
|
||||||
|
expect(viewportStyles.height).toMatch(/\d+(\.\d+)?px/)
|
||||||
|
expect(viewportStyles.border).toBe('2px solid #FFF')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle canvas dimension updates', async () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
|
||||||
|
mockCanvas.canvas.clientWidth = 1200
|
||||||
|
mockCanvas.canvas.clientHeight = 900
|
||||||
|
|
||||||
|
const resizeHandler = (window.addEventListener as any).mock.calls.find(
|
||||||
|
(call: any) => call[0] === 'resize'
|
||||||
|
)?.[1]
|
||||||
|
|
||||||
|
if (resizeHandler) {
|
||||||
|
resizeHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
expect(minimap.viewportStyles.value).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('graph change handling', () => {
|
||||||
|
it('should handle node addition', async () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
|
||||||
|
const newNode = {
|
||||||
|
id: 'node3',
|
||||||
|
pos: [300, 200],
|
||||||
|
size: [100, 100],
|
||||||
|
constructor: { color: '#666' }
|
||||||
|
}
|
||||||
|
|
||||||
|
mockGraph._nodes.push(newNode)
|
||||||
|
if (mockGraph.onNodeAdded) {
|
||||||
|
mockGraph.onNodeAdded(newNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle node removal', async () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
|
||||||
|
const removedNode = mockGraph._nodes[0]
|
||||||
|
mockGraph._nodes.splice(0, 1)
|
||||||
|
|
||||||
|
if (mockGraph.onNodeRemoved) {
|
||||||
|
mockGraph.onNodeRemoved(removedNode)
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle connection changes', async () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
|
||||||
|
if (mockGraph.onConnectionChange) {
|
||||||
|
mockGraph.onConnectionChange(mockGraph._nodes[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 600))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('container styles', () => {
|
||||||
|
it('should provide correct container styles', () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
const styles = minimap.containerStyles.value
|
||||||
|
|
||||||
|
expect(styles.width).toBe('250px')
|
||||||
|
expect(styles.height).toBe('200px')
|
||||||
|
expect(styles.backgroundColor).toBe('#1e1e1e')
|
||||||
|
expect(styles.border).toBe('1px solid #444')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('edge cases', () => {
|
||||||
|
it('should handle missing node outputs', async () => {
|
||||||
|
mockGraph._nodes[0].outputs = null
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await expect(minimap.init()).resolves.not.toThrow()
|
||||||
|
expect(minimap.initialized.value).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle invalid link references', async () => {
|
||||||
|
mockGraph.links.link1.target_id = 'invalid-node'
|
||||||
|
mockGraph.getNodeById.mockReturnValue(null)
|
||||||
|
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await expect(minimap.init()).resolves.not.toThrow()
|
||||||
|
expect(minimap.initialized.value).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle high DPI displays', async () => {
|
||||||
|
window.devicePixelRatio = 2
|
||||||
|
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
|
||||||
|
expect(minimap.initialized.value).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('should handle nodes without color', async () => {
|
||||||
|
mockGraph._nodes[0].color = undefined
|
||||||
|
|
||||||
|
const minimap = useMinimap()
|
||||||
|
minimap.containerRef.value = mockContainerElement
|
||||||
|
minimap.canvasRef.value = mockCanvasElement
|
||||||
|
|
||||||
|
await minimap.init()
|
||||||
|
|
||||||
|
const renderMinimap = (minimap as any).renderMinimap
|
||||||
|
if (renderMinimap) {
|
||||||
|
renderMinimap()
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(mockContext2D.fillRect).toHaveBeenCalled()
|
||||||
|
expect(mockContext2D.fillStyle).toBeDefined()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('setMinimapRef', () => {
|
||||||
|
it('should set minimap reference', () => {
|
||||||
|
const minimap = useMinimap()
|
||||||
|
const ref = { value: 'test-ref' }
|
||||||
|
|
||||||
|
minimap.setMinimapRef(ref)
|
||||||
|
|
||||||
|
expect(() => minimap.setMinimapRef(ref)).not.toThrow()
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||