mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 10:42:44 +00:00
minimap (#4520)
Co-authored-by: github-actions <github-actions@github.com> Co-authored-by: Christian Byrne <cbyrne@comfy.org>
This commit is contained in:
@@ -18,6 +18,12 @@
|
||||
/>
|
||||
</div>
|
||||
<GraphCanvasMenu v-if="canvasMenuEnabled" class="pointer-events-auto" />
|
||||
|
||||
<MiniMap
|
||||
v-if="comfyAppReady && minimapEnabled"
|
||||
ref="minimapRef"
|
||||
class="pointer-events-auto"
|
||||
/>
|
||||
</template>
|
||||
</LiteGraphCanvasSplitterOverlay>
|
||||
<GraphCanvasMenu v-if="!betaMenuEnabled && canvasMenuEnabled" />
|
||||
@@ -51,6 +57,7 @@ import LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitter
|
||||
import BottomPanel from '@/components/bottomPanel/BottomPanel.vue'
|
||||
import DomWidgets from '@/components/graph/DomWidgets.vue'
|
||||
import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
|
||||
import MiniMap from '@/components/graph/MiniMap.vue'
|
||||
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
|
||||
import SelectionOverlay from '@/components/graph/SelectionOverlay.vue'
|
||||
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
|
||||
@@ -65,6 +72,7 @@ import { useContextMenuTranslation } from '@/composables/useContextMenuTranslati
|
||||
import { useCopy } from '@/composables/useCopy'
|
||||
import { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'
|
||||
import { useLitegraphSettings } from '@/composables/useLitegraphSettings'
|
||||
import { useMinimap } from '@/composables/useMinimap'
|
||||
import { usePaste } from '@/composables/usePaste'
|
||||
import { useWorkflowAutoSave } from '@/composables/useWorkflowAutoSave'
|
||||
import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
|
||||
@@ -111,6 +119,10 @@ const selectionToolboxEnabled = computed(() =>
|
||||
settingStore.get('Comfy.Canvas.SelectionToolbox')
|
||||
)
|
||||
|
||||
const minimapRef = ref<InstanceType<typeof MiniMap>>()
|
||||
const minimapEnabled = computed(() => settingStore.get('Comfy.Minimap.Visible'))
|
||||
const minimap = useMinimap()
|
||||
|
||||
watchEffect(() => {
|
||||
nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')
|
||||
})
|
||||
@@ -346,6 +358,13 @@ onMounted(async () => {
|
||||
}
|
||||
)
|
||||
|
||||
whenever(
|
||||
() => minimapRef.value,
|
||||
(ref) => {
|
||||
minimap.setMinimapRef(ref)
|
||||
}
|
||||
)
|
||||
|
||||
whenever(
|
||||
() => useCanvasStore().canvas,
|
||||
(canvas) => {
|
||||
|
||||
@@ -56,6 +56,15 @@
|
||||
data-testid="toggle-link-visibility-button"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')"
|
||||
/>
|
||||
<Button
|
||||
v-tooltip.left="t('graphCanvasMenu.toggleMinimap') + ' (Alt + m)'"
|
||||
severity="secondary"
|
||||
:icon="'pi pi-map'"
|
||||
:aria-label="$t('graphCanvasMenu.toggleMinimap')"
|
||||
:class="{ 'minimap-active': minimapVisible }"
|
||||
data-testid="toggle-minimap-button"
|
||||
@click="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')"
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</template>
|
||||
|
||||
@@ -75,6 +84,7 @@ const commandStore = useCommandStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
const settingStore = useSettingStore()
|
||||
|
||||
const minimapVisible = computed(() => settingStore.get('Comfy.Minimap.Visible'))
|
||||
const linkHidden = computed(
|
||||
() => settingStore.get('Comfy.LinkRenderMode') === LiteGraph.HIDDEN_LINK
|
||||
)
|
||||
@@ -107,4 +117,15 @@ const stopRepeat = () => {
|
||||
margin: 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>
|
||||
|
||||
88
src/components/graph/MiniMap.vue
Normal file
88
src/components/graph/MiniMap.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<div
|
||||
v-if="visible && initialized"
|
||||
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>
|
||||
136
src/composables/canvas/useCanvasTransformSync.ts
Normal file
136
src/composables/canvas/useCanvasTransformSync.ts
Normal file
@@ -0,0 +1,136 @@
|
||||
import { LGraphCanvas } from '@comfyorg/litegraph'
|
||||
import { onUnmounted, ref } from 'vue'
|
||||
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
|
||||
interface CanvasTransformSyncOptions {
|
||||
/**
|
||||
* Whether to automatically start syncing when canvas is available
|
||||
* @default true
|
||||
*/
|
||||
autoStart?: boolean
|
||||
/**
|
||||
* Called when sync starts
|
||||
*/
|
||||
onStart?: () => void
|
||||
/**
|
||||
* Called when sync stops
|
||||
*/
|
||||
onStop?: () => void
|
||||
}
|
||||
|
||||
interface CanvasTransform {
|
||||
scale: number
|
||||
offsetX: number
|
||||
offsetY: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Manages requestAnimationFrame-based synchronization with LiteGraph canvas transforms.
|
||||
*
|
||||
* This composable provides a clean way to sync Vue transform state with LiteGraph canvas
|
||||
* on every frame. It handles RAF lifecycle management, and ensures proper cleanup.
|
||||
*
|
||||
* The sync function typically reads canvas.ds properties like offset and scale to keep
|
||||
* Vue components aligned with the canvas coordinate system.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const syncWithCanvas = (canvas: LGraphCanvas) => {
|
||||
* canvas.ds.scale
|
||||
* canvas.ds.offset
|
||||
* }
|
||||
*
|
||||
* const { isActive, startSync, stopSync } = useCanvasTransformSync(
|
||||
* syncWithCanvas,
|
||||
* {
|
||||
* autoStart: false,
|
||||
* onStart: () => emit('rafStatusChange', true),
|
||||
* onStop: () => emit('rafStatusChange', false)
|
||||
* }
|
||||
* )
|
||||
* ```
|
||||
*/
|
||||
export function useCanvasTransformSync(
|
||||
syncFn: (canvas: LGraphCanvas) => void,
|
||||
options: CanvasTransformSyncOptions = {}
|
||||
) {
|
||||
const { onStart, onStop, autoStart = true } = options
|
||||
const { getCanvas } = useCanvasStore()
|
||||
|
||||
const isActive = ref(false)
|
||||
let rafId: number | null = null
|
||||
let lastTransform: CanvasTransform = {
|
||||
scale: 0,
|
||||
offsetX: 0,
|
||||
offsetY: 0
|
||||
}
|
||||
|
||||
const hasTransformChanged = (canvas: LGraphCanvas): boolean => {
|
||||
const ds = canvas.ds
|
||||
return (
|
||||
ds.scale !== lastTransform.scale ||
|
||||
ds.offset[0] !== lastTransform.offsetX ||
|
||||
ds.offset[1] !== lastTransform.offsetY
|
||||
)
|
||||
}
|
||||
|
||||
const sync = () => {
|
||||
if (!isActive.value) return
|
||||
|
||||
const canvas = getCanvas()
|
||||
if (!canvas) return
|
||||
|
||||
try {
|
||||
// Only run sync if transform actually changed
|
||||
if (hasTransformChanged(canvas)) {
|
||||
lastTransform = {
|
||||
scale: canvas.ds.scale,
|
||||
offsetX: canvas.ds.offset[0],
|
||||
offsetY: canvas.ds.offset[1]
|
||||
}
|
||||
|
||||
syncFn(canvas)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Canvas transform sync error:', error)
|
||||
}
|
||||
|
||||
rafId = requestAnimationFrame(sync)
|
||||
}
|
||||
|
||||
const startSync = () => {
|
||||
if (isActive.value) return
|
||||
|
||||
isActive.value = true
|
||||
onStart?.()
|
||||
|
||||
// Reset last transform to force initial sync
|
||||
lastTransform = { scale: 0, offsetX: 0, offsetY: 0 }
|
||||
|
||||
sync()
|
||||
}
|
||||
|
||||
const stopSync = () => {
|
||||
isActive.value = false
|
||||
|
||||
if (rafId !== null) {
|
||||
cancelAnimationFrame(rafId)
|
||||
rafId = null
|
||||
}
|
||||
|
||||
onStop?.()
|
||||
}
|
||||
|
||||
onUnmounted(stopSync)
|
||||
|
||||
if (autoStart) {
|
||||
startSync()
|
||||
}
|
||||
|
||||
return {
|
||||
isActive,
|
||||
startSync,
|
||||
stopSync
|
||||
}
|
||||
}
|
||||
@@ -317,6 +317,19 @@ export function useCoreCommands(): ComfyCommand[] {
|
||||
}
|
||||
})()
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Canvas.ToggleMinimap',
|
||||
icon: 'pi pi-map',
|
||||
label: 'Canvas Toggle Minimap',
|
||||
versionAdded: '1.24.1',
|
||||
function: async () => {
|
||||
const settingStore = useSettingStore()
|
||||
await settingStore.set(
|
||||
'Comfy.Minimap.Visible',
|
||||
!settingStore.get('Comfy.Minimap.Visible')
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'Comfy.QueuePrompt',
|
||||
icon: 'pi pi-play',
|
||||
|
||||
685
src/composables/useMinimap.ts
Normal file
685
src/composables/useMinimap.ts
Normal file
@@ -0,0 +1,685 @@
|
||||
import type { LGraphNode } from '@comfyorg/litegraph'
|
||||
import { useRafFn, useThrottleFn } from '@vueuse/core'
|
||||
import { computed, nextTick, ref, watch } from 'vue'
|
||||
|
||||
import { useCanvasTransformSync } from '@/composables/canvas/useCanvasTransformSync'
|
||||
import type { NodeId } from '@/schemas/comfyWorkflowSchema'
|
||||
import { api } from '@/scripts/api'
|
||||
import { useCanvasStore } from '@/stores/graphStore'
|
||||
import { useSettingStore } from '@/stores/settingStore'
|
||||
|
||||
interface GraphCallbacks {
|
||||
onNodeAdded?: (node: LGraphNode) => void
|
||||
onNodeRemoved?: (node: LGraphNode) => void
|
||||
onConnectionChange?: (node: LGraphNode) => void
|
||||
}
|
||||
|
||||
export function useMinimap() {
|
||||
const settingStore = useSettingStore()
|
||||
const canvasStore = useCanvasStore()
|
||||
|
||||
const containerRef = ref<HTMLDivElement>()
|
||||
const canvasRef = ref<HTMLCanvasElement>()
|
||||
const minimapRef = ref<any>(null)
|
||||
|
||||
const visible = ref(true)
|
||||
|
||||
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 = '#0B8CE999'
|
||||
const linkColor = '#F99614'
|
||||
const slotColor = '#F99614'
|
||||
const viewportColor = '#FFF'
|
||||
const backgroundColor = '#15161C'
|
||||
const borderColor = '#333'
|
||||
|
||||
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}`,
|
||||
borderRadius: '8px'
|
||||
}))
|
||||
|
||||
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])
|
||||
}
|
||||
|
||||
let currentWidth = maxX - minX
|
||||
let currentHeight = maxY - minY
|
||||
|
||||
// Enforce minimum viewport dimensions for better visualization
|
||||
const minViewportWidth = 2500
|
||||
const minViewportHeight = 2000
|
||||
|
||||
if (currentWidth < minViewportWidth) {
|
||||
const padding = (minViewportWidth - currentWidth) / 2
|
||||
minX -= padding
|
||||
maxX += padding
|
||||
currentWidth = minViewportWidth
|
||||
}
|
||||
|
||||
if (currentHeight < minViewportHeight) {
|
||||
const padding = (minViewportHeight - currentHeight) / 2
|
||||
minY -= padding
|
||||
maxY += padding
|
||||
currentHeight = minViewportHeight
|
||||
}
|
||||
|
||||
return {
|
||||
minX,
|
||||
minY,
|
||||
maxX,
|
||||
maxY,
|
||||
width: currentWidth,
|
||||
height: currentHeight
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
// Apply 0.9 factor to provide padding/gap between nodes and minimap borders
|
||||
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
|
||||
|
||||
// Render solid node blocks
|
||||
ctx.fillStyle = nodeColor
|
||||
ctx.fillRect(x, y, w, h)
|
||||
}
|
||||
}
|
||||
|
||||
const renderConnections = (
|
||||
ctx: CanvasRenderingContext2D,
|
||||
offsetX: number,
|
||||
offsetY: number
|
||||
) => {
|
||||
const g = graph.value
|
||||
if (!g) return
|
||||
|
||||
ctx.strokeStyle = linkColor
|
||||
ctx.lineWidth = 1.4
|
||||
|
||||
const slotRadius = 3.7 * Math.max(scale.value, 0.5) // Larger slots that scale
|
||||
const connections: Array<{
|
||||
x1: number
|
||||
y1: number
|
||||
x2: number
|
||||
y2: number
|
||||
}> = []
|
||||
|
||||
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
|
||||
|
||||
const outputX = x1 + node.size[0] * scale.value
|
||||
const outputY = y1 + node.size[1] * scale.value * 0.2
|
||||
const inputX = x2
|
||||
const inputY = y2 + targetNode.size[1] * scale.value * 0.2
|
||||
|
||||
// Draw connection line
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(outputX, outputY)
|
||||
ctx.lineTo(inputX, inputY)
|
||||
ctx.stroke()
|
||||
|
||||
connections.push({ x1: outputX, y1: outputY, x2: inputX, y2: inputY })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Render connection slots on top
|
||||
ctx.fillStyle = slotColor
|
||||
for (const conn of connections) {
|
||||
// Output slot
|
||||
ctx.beginPath()
|
||||
ctx.arc(conn.x1, conn.y1, slotRadius, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
|
||||
// Input slot
|
||||
ctx.beginPath()
|
||||
ctx.arc(conn.x2, conn.y2, slotRadius, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
|
||||
const renderMinimap = () => {
|
||||
if (!canvasRef.value || !graph.value) return
|
||||
|
||||
const ctx = canvasRef.value.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
// Fast path for 0 nodes - just show background
|
||||
if (!graph.value._nodes || graph.value._nodes.length === 0) {
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
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
|
||||
|
||||
renderNodes(ctx, offsetX, offsetY)
|
||||
renderConnections(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 { startSync: startViewportSync, stopSync: stopViewportSync } =
|
||||
useCanvasTransformSync(updateViewport, { autoStart: false })
|
||||
|
||||
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: GraphCallbacks = {}
|
||||
|
||||
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()
|
||||
startViewportSync()
|
||||
}
|
||||
initialized.value = true
|
||||
}
|
||||
}
|
||||
|
||||
const destroy = () => {
|
||||
pauseChangeDetection()
|
||||
stopViewportSync()
|
||||
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()
|
||||
stopViewportSync()
|
||||
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()
|
||||
startViewportSync()
|
||||
} else {
|
||||
pauseChangeDetection()
|
||||
stopViewportSync()
|
||||
}
|
||||
})
|
||||
|
||||
const toggle = async () => {
|
||||
visible.value = !visible.value
|
||||
await settingStore.set('Comfy.Minimap.Visible', visible.value)
|
||||
}
|
||||
|
||||
const setMinimapRef = (ref: any) => {
|
||||
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
|
||||
},
|
||||
commandId: 'Comfy.Graph.ConvertToSubgraph'
|
||||
},
|
||||
{
|
||||
combo: {
|
||||
key: 'm',
|
||||
alt: true
|
||||
},
|
||||
commandId: 'Comfy.Canvas.ToggleMinimap'
|
||||
}
|
||||
]
|
||||
|
||||
@@ -824,6 +824,13 @@ export const CORE_SETTINGS: SettingParams[] = [
|
||||
defaultValue: false,
|
||||
versionAdded: '1.15.12'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Minimap.Visible',
|
||||
name: 'Display minimap on canvas',
|
||||
type: 'hidden',
|
||||
defaultValue: true,
|
||||
versionAdded: '1.25.0'
|
||||
},
|
||||
{
|
||||
id: 'Comfy.Workflow.AutoSaveDelay',
|
||||
name: 'Auto Save Delay (ms)',
|
||||
|
||||
@@ -71,6 +71,9 @@
|
||||
"Comfy_Canvas_ToggleLock": {
|
||||
"label": "Canvas Toggle Lock"
|
||||
},
|
||||
"Comfy_Canvas_ToggleMinimap": {
|
||||
"label": "Canvas Toggle Minimap"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelected_Pin": {
|
||||
"label": "Pin/Unpin Selected Items"
|
||||
},
|
||||
|
||||
@@ -872,7 +872,8 @@
|
||||
"fitView": "Fit View",
|
||||
"selectMode": "Select Mode",
|
||||
"panMode": "Pan Mode",
|
||||
"toggleLinkVisibility": "Toggle Link Visibility"
|
||||
"toggleLinkVisibility": "Toggle Link Visibility",
|
||||
"toggleMinimap": "Toggle Minimap"
|
||||
},
|
||||
"groupNode": {
|
||||
"create": "Create group node",
|
||||
@@ -948,6 +949,7 @@
|
||||
"Resize Selected Nodes": "Resize Selected Nodes",
|
||||
"Canvas Toggle Link Visibility": "Canvas Toggle Link Visibility",
|
||||
"Canvas Toggle Lock": "Canvas Toggle Lock",
|
||||
"Canvas Toggle Minimap": "Canvas Toggle Minimap",
|
||||
"Pin/Unpin Selected Items": "Pin/Unpin Selected Items",
|
||||
"Bypass/Unbypass Selected Nodes": "Bypass/Unbypass Selected Nodes",
|
||||
"Collapse/Expand Selected Nodes": "Collapse/Expand Selected Nodes",
|
||||
|
||||
@@ -71,6 +71,9 @@
|
||||
"Comfy_Canvas_ToggleLock": {
|
||||
"label": "Alternar bloqueo en lienzo"
|
||||
},
|
||||
"Comfy_Canvas_ToggleMinimap": {
|
||||
"label": "Lienzo Alternar Minimapa"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||
"label": "Omitir/No omitir nodos seleccionados"
|
||||
},
|
||||
|
||||
@@ -403,6 +403,7 @@
|
||||
"resetView": "Restablecer vista",
|
||||
"selectMode": "Modo de selección",
|
||||
"toggleLinkVisibility": "Alternar visibilidad de enlace",
|
||||
"toggleMinimap": "Alternar minimapa",
|
||||
"zoomIn": "Acercar",
|
||||
"zoomOut": "Alejar"
|
||||
},
|
||||
@@ -738,6 +739,7 @@
|
||||
"Bypass/Unbypass Selected Nodes": "Evitar/No evitar nodos seleccionados",
|
||||
"Canvas Toggle Link Visibility": "Alternar visibilidad de enlace en lienzo",
|
||||
"Canvas Toggle Lock": "Alternar bloqueo en lienzo",
|
||||
"Canvas Toggle Minimap": "Lienzo: Alternar minimapa",
|
||||
"Check for Updates": "Buscar actualizaciones",
|
||||
"Clear Pending Tasks": "Borrar tareas pendientes",
|
||||
"Clear Workflow": "Borrar flujo de trabajo",
|
||||
|
||||
@@ -71,6 +71,9 @@
|
||||
"Comfy_Canvas_ToggleLock": {
|
||||
"label": "Basculer le verrouillage du canevas"
|
||||
},
|
||||
"Comfy_Canvas_ToggleMinimap": {
|
||||
"label": "Basculer la mini-carte du canevas"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||
"label": "Contourner/Ne pas contourner les nœuds sélectionnés"
|
||||
},
|
||||
|
||||
@@ -403,6 +403,7 @@
|
||||
"resetView": "Réinitialiser la vue",
|
||||
"selectMode": "Mode sélection",
|
||||
"toggleLinkVisibility": "Basculer la visibilité des liens",
|
||||
"toggleMinimap": "Afficher/Masquer la mini-carte",
|
||||
"zoomIn": "Zoom avant",
|
||||
"zoomOut": "Zoom arrière"
|
||||
},
|
||||
@@ -738,6 +739,7 @@
|
||||
"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 Lock": "Basculer le verrouillage de la toile",
|
||||
"Canvas Toggle Minimap": "Basculer la mini-carte du canevas",
|
||||
"Check for Updates": "Vérifier les mises à jour",
|
||||
"Clear Pending Tasks": "Effacer les tâches en attente",
|
||||
"Clear Workflow": "Effacer le flux de travail",
|
||||
|
||||
@@ -71,6 +71,9 @@
|
||||
"Comfy_Canvas_ToggleLock": {
|
||||
"label": "キャンバスのロックを切り替える"
|
||||
},
|
||||
"Comfy_Canvas_ToggleMinimap": {
|
||||
"label": "キャンバス ミニマップ切り替え"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||
"label": "選択したノードのバイパス/バイパス解除"
|
||||
},
|
||||
|
||||
@@ -403,6 +403,7 @@
|
||||
"resetView": "ビューをリセット",
|
||||
"selectMode": "選択モード",
|
||||
"toggleLinkVisibility": "リンクの表示切り替え",
|
||||
"toggleMinimap": "ミニマップの切り替え",
|
||||
"zoomIn": "拡大",
|
||||
"zoomOut": "縮小"
|
||||
},
|
||||
@@ -738,6 +739,7 @@
|
||||
"Bypass/Unbypass Selected Nodes": "選択したノードのバイパス/バイパス解除",
|
||||
"Canvas Toggle Link Visibility": "キャンバスのリンク表示を切り替え",
|
||||
"Canvas Toggle Lock": "キャンバスのロックを切り替え",
|
||||
"Canvas Toggle Minimap": "キャンバス ミニマップの切り替え",
|
||||
"Check for Updates": "更新を確認する",
|
||||
"Clear Pending Tasks": "保留中のタスクをクリア",
|
||||
"Clear Workflow": "ワークフローをクリア",
|
||||
|
||||
@@ -71,6 +71,9 @@
|
||||
"Comfy_Canvas_ToggleLock": {
|
||||
"label": "캔버스 잠금 토글"
|
||||
},
|
||||
"Comfy_Canvas_ToggleMinimap": {
|
||||
"label": "캔버스 미니맵 전환"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||
"label": "선택한 노드 우회/우회 해제"
|
||||
},
|
||||
|
||||
@@ -403,6 +403,7 @@
|
||||
"resetView": "보기 재설정",
|
||||
"selectMode": "선택 모드",
|
||||
"toggleLinkVisibility": "링크 가시성 전환",
|
||||
"toggleMinimap": "미니맵 전환",
|
||||
"zoomIn": "확대",
|
||||
"zoomOut": "축소"
|
||||
},
|
||||
@@ -738,6 +739,7 @@
|
||||
"Bypass/Unbypass Selected Nodes": "선택한 노드 우회/우회 해제",
|
||||
"Canvas Toggle Link Visibility": "캔버스 토글 링크 가시성",
|
||||
"Canvas Toggle Lock": "캔버스 토글 잠금",
|
||||
"Canvas Toggle Minimap": "캔버스 미니맵 전환",
|
||||
"Check for Updates": "업데이트 확인",
|
||||
"Clear Pending Tasks": "보류 중인 작업 제거하기",
|
||||
"Clear Workflow": "워크플로 지우기",
|
||||
|
||||
@@ -71,6 +71,9 @@
|
||||
"Comfy_Canvas_ToggleLock": {
|
||||
"label": "Переключить блокировку холста"
|
||||
},
|
||||
"Comfy_Canvas_ToggleMinimap": {
|
||||
"label": "Полотно: переключить миникарту"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||
"label": "Обход/Необход выбранных нод"
|
||||
},
|
||||
|
||||
@@ -403,6 +403,7 @@
|
||||
"resetView": "Сбросить вид",
|
||||
"selectMode": "Выбрать режим",
|
||||
"toggleLinkVisibility": "Переключить видимость ссылок",
|
||||
"toggleMinimap": "Показать/скрыть миникарту",
|
||||
"zoomIn": "Увеличить",
|
||||
"zoomOut": "Уменьшить"
|
||||
},
|
||||
@@ -738,6 +739,7 @@
|
||||
"Bypass/Unbypass Selected Nodes": "Обойти/восстановить выбранные ноды",
|
||||
"Canvas Toggle Link Visibility": "Переключение видимости ссылки на холст",
|
||||
"Canvas Toggle Lock": "Переключение блокировки холста",
|
||||
"Canvas Toggle Minimap": "Показать/скрыть миникарту на холсте",
|
||||
"Check for Updates": "Проверить наличие обновлений",
|
||||
"Clear Pending Tasks": "Очистить ожидающие задачи",
|
||||
"Clear Workflow": "Очистить рабочий процесс",
|
||||
|
||||
@@ -71,6 +71,9 @@
|
||||
"Comfy_Canvas_ToggleLock": {
|
||||
"label": "畫布切換鎖定"
|
||||
},
|
||||
"Comfy_Canvas_ToggleMinimap": {
|
||||
"label": "畫布切換小地圖"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||
"label": "略過/取消略過選取的節點"
|
||||
},
|
||||
|
||||
@@ -403,6 +403,7 @@
|
||||
"resetView": "重設視圖",
|
||||
"selectMode": "選取模式",
|
||||
"toggleLinkVisibility": "切換連結顯示",
|
||||
"toggleMinimap": "切換小地圖",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "縮小"
|
||||
},
|
||||
@@ -738,6 +739,7 @@
|
||||
"Bypass/Unbypass Selected Nodes": "繞過/取消繞過選取節點",
|
||||
"Canvas Toggle Link Visibility": "切換連結可見性",
|
||||
"Canvas Toggle Lock": "切換畫布鎖定",
|
||||
"Canvas Toggle Minimap": "畫布切換小地圖",
|
||||
"Check for Updates": "檢查更新",
|
||||
"Clear Pending Tasks": "清除待處理任務",
|
||||
"Clear Workflow": "清除工作流程",
|
||||
|
||||
@@ -71,6 +71,9 @@
|
||||
"Comfy_Canvas_ToggleLock": {
|
||||
"label": "锁定视图"
|
||||
},
|
||||
"Comfy_Canvas_ToggleMinimap": {
|
||||
"label": "畫布切換小地圖"
|
||||
},
|
||||
"Comfy_Canvas_ToggleSelectedNodes_Bypass": {
|
||||
"label": "忽略/取消忽略选中节点"
|
||||
},
|
||||
|
||||
@@ -403,6 +403,7 @@
|
||||
"resetView": "重置视图",
|
||||
"selectMode": "选择模式",
|
||||
"toggleLinkVisibility": "切换连线可见性",
|
||||
"toggleMinimap": "切換小地圖",
|
||||
"zoomIn": "放大",
|
||||
"zoomOut": "缩小"
|
||||
},
|
||||
@@ -738,6 +739,7 @@
|
||||
"Bypass/Unbypass Selected Nodes": "忽略/取消忽略选定节点",
|
||||
"Canvas Toggle Link Visibility": "切换连线可见性",
|
||||
"Canvas Toggle Lock": "切换视图锁定",
|
||||
"Canvas Toggle Minimap": "畫布切換小地圖",
|
||||
"Check for Updates": "检查更新",
|
||||
"Clear Pending Tasks": "清除待处理任务",
|
||||
"Clear Workflow": "清除工作流",
|
||||
|
||||
@@ -475,6 +475,7 @@ const zSettings = z.object({
|
||||
'Comfy.TutorialCompleted': z.boolean(),
|
||||
'Comfy.InstalledVersion': z.string().nullable(),
|
||||
'Comfy.Node.AllowImageSizeDraw': z.boolean(),
|
||||
'Comfy.Minimap.Visible': z.boolean(),
|
||||
'Comfy.Canvas.NavigationMode': z.string(),
|
||||
'Comfy-Desktop.AutoUpdate': z.boolean(),
|
||||
'Comfy-Desktop.SendStatistics': z.boolean(),
|
||||
|
||||
Reference in New Issue
Block a user