[refactor] Refactor rendering-related files to DDD organization (#5388)

* refactor rendering-related files to DDD organization

* add to git ignore ignore revs
This commit is contained in:
Christian Byrne
2025-09-06 02:47:37 -07:00
committed by snomiao
parent 348046ec91
commit 3f41509e90
14 changed files with 17 additions and 13 deletions

View File

@@ -89,15 +89,12 @@ 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 SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
import TitleEditor from '@/components/graph/TitleEditor.vue'
import TransformPane from '@/components/graph/TransformPane.vue'
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
import SideToolbar from '@/components/sidebar/SideToolbar.vue'
import SecondRowWorkflowTabs from '@/components/topbar/SecondRowWorkflowTabs.vue'
import { useTransformState } from '@/composables/element/useTransformState'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
import type {
@@ -117,12 +114,15 @@ import { useWorkflowPersistence } from '@/composables/useWorkflowPersistence'
import { CORE_SETTINGS } from '@/constants/coreSettings'
import { i18n, t } from '@/i18n'
import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph'
import TransformPane from '@/renderer/core/layout/TransformPane.vue'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync'
import { useLinkLayoutSync } from '@/renderer/core/layout/sync/useLinkLayoutSync'
import { useSlotLayoutSync } from '@/renderer/core/layout/sync/useSlotLayoutSync'
import { LayoutSource } from '@/renderer/core/layout/types'
import { useTransformState } from '@/renderer/core/layout/useTransformState'
import MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'
import VueGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'
import { UnauthorizedError, api } from '@/scripts/api'
import { app as comfyApp } from '@/scripts/app'

View File

@@ -1,147 +0,0 @@
<template>
<div
v-if="visible && initialized"
ref="minimapRef"
class="minimap-main-container flex absolute bottom-[66px] right-2 md:right-11 z-1000"
>
<MiniMapPanel
v-if="showOptionsPanel"
:panel-styles="panelStyles"
:node-colors="nodeColors"
:show-links="showLinks"
:show-groups="showGroups"
:render-bypass="renderBypass"
:render-error="renderError"
@update-option="updateOption"
/>
<div
ref="containerRef"
class="litegraph-minimap relative"
:style="containerStyles"
>
<Button
class="absolute z-10"
size="small"
text
severity="secondary"
@click.stop="toggleOptionsPanel"
>
<template #icon>
<i-lucide:settings-2 />
</template>
</Button>
<Button
class="absolute z-10 right-0"
size="small"
text
severity="secondary"
data-testid="close-minmap-button"
@click.stop="() => commandStore.execute('Comfy.Canvas.ToggleMinimap')"
>
<template #icon>
<i-lucide:x />
</template>
</Button>
<hr
class="absolute top-5 bg-[#E1DED5] dark-theme:bg-[#262729] h-[1px] border-0"
:style="{
width: containerStyles.width
}"
/>
<canvas
ref="canvasRef"
:width="width"
:height="height"
class="minimap-canvas"
/>
<div class="minimap-viewport" :style="viewportStyles" />
<div
class="absolute inset-0"
@pointerdown="handlePointerDown"
@pointermove="handlePointerMove"
@pointerup="handlePointerUp"
@pointerleave="handlePointerUp"
@wheel="handleWheel"
/>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { onMounted, onUnmounted, ref } from 'vue'
import { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'
import { useCommandStore } from '@/stores/commandStore'
import MiniMapPanel from './MiniMapPanel.vue'
const commandStore = useCommandStore()
const minimapRef = ref<HTMLDivElement>()
const {
initialized,
visible,
containerRef,
canvasRef,
containerStyles,
viewportStyles,
width,
height,
panelStyles,
nodeColors,
showLinks,
showGroups,
renderBypass,
renderError,
updateOption,
destroy,
handlePointerDown,
handlePointerMove,
handlePointerUp,
handleWheel,
setMinimapRef
} = useMinimap()
const showOptionsPanel = ref(false)
const toggleOptionsPanel = () => {
showOptionsPanel.value = !showOptionsPanel.value
}
onMounted(() => {
if (minimapRef.value) {
setMinimapRef(minimapRef.value)
}
})
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>

View File

@@ -1,97 +0,0 @@
<template>
<div
class="minimap-panel p-3 mr-2 flex flex-col gap-3 text-sm"
:style="panelStyles"
>
<div class="flex items-center gap-2">
<Checkbox
input-id="node-colors"
name="node-colors"
:model-value="nodeColors"
binary
@update:model-value="
(value) => $emit('updateOption', 'Comfy.Minimap.NodeColors', value)
"
/>
<i-lucide:palette />
<label for="node-colors">{{ $t('minimap.nodeColors') }}</label>
</div>
<div class="flex items-center gap-2">
<Checkbox
input-id="show-links"
name="show-links"
:model-value="showLinks"
binary
@update:model-value="
(value) => $emit('updateOption', 'Comfy.Minimap.ShowLinks', value)
"
/>
<i-lucide:route />
<label for="show-links">{{ $t('minimap.showLinks') }}</label>
</div>
<div class="flex items-center gap-2">
<Checkbox
input-id="show-groups"
name="show-groups"
:model-value="showGroups"
binary
@update:model-value="
(value) => $emit('updateOption', 'Comfy.Minimap.ShowGroups', value)
"
/>
<i-lucide:frame />
<label for="show-groups">{{ $t('minimap.showGroups') }}</label>
</div>
<div class="flex items-center gap-2">
<Checkbox
input-id="render-bypass"
name="render-bypass"
:model-value="renderBypass"
binary
@update:model-value="
(value) =>
$emit('updateOption', 'Comfy.Minimap.RenderBypassState', value)
"
/>
<i-lucide:circle-slash-2 />
<label for="render-bypass">{{ $t('minimap.renderBypassState') }}</label>
</div>
<div class="flex items-center gap-2">
<Checkbox
input-id="render-error"
name="render-error"
:model-value="renderError"
binary
@update:model-value="
(value) =>
$emit('updateOption', 'Comfy.Minimap.RenderErrorState', value)
"
/>
<i-lucide:message-circle-warning />
<label for="render-error">{{ $t('minimap.renderErrorState') }}</label>
</div>
</div>
</template>
<script setup lang="ts">
import Checkbox from 'primevue/checkbox'
import type { MinimapSettingsKey } from '@/renderer/extensions/minimap/types'
defineProps<{
panelStyles: any
nodeColors: boolean
showLinks: boolean
showGroups: boolean
renderBypass: boolean
renderError: boolean
}>()
defineEmits<{
updateOption: [key: MinimapSettingsKey, value: boolean]
}>()
</script>

View File

@@ -1,350 +0,0 @@
import { mount } from '@vue/test-utils'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick, ref } from 'vue'
import TransformPane from './TransformPane.vue'
// Mock the transform state composable
const mockTransformState = {
camera: ref({ x: 0, y: 0, z: 1 }),
transformStyle: ref({
transform: 'scale(1) translate(0px, 0px)',
transformOrigin: '0 0'
}),
syncWithCanvas: vi.fn(),
canvasToScreen: vi.fn(),
screenToCanvas: vi.fn(),
isNodeInViewport: vi.fn()
}
vi.mock('@/composables/element/useTransformState', () => ({
useTransformState: () => mockTransformState
}))
// Mock requestAnimationFrame/cancelAnimationFrame
global.requestAnimationFrame = vi.fn((cb) => {
setTimeout(cb, 16)
return 1
})
global.cancelAnimationFrame = vi.fn()
describe('TransformPane', () => {
let wrapper: ReturnType<typeof mount>
let mockCanvas: any
beforeEach(() => {
vi.clearAllMocks()
// Create mock canvas with LiteGraph interface
mockCanvas = {
canvas: {
addEventListener: vi.fn(),
removeEventListener: vi.fn()
},
ds: {
offset: [0, 0],
scale: 1
}
}
// Reset mock transform state
mockTransformState.camera.value = { x: 0, y: 0, z: 1 }
mockTransformState.transformStyle.value = {
transform: 'scale(1) translate(0px, 0px)',
transformOrigin: '0 0'
}
})
afterEach(() => {
if (wrapper) {
wrapper.unmount()
}
})
describe('component mounting', () => {
it('should mount successfully with minimal props', () => {
wrapper = mount(TransformPane, {
props: {
canvas: mockCanvas
}
})
expect(wrapper.exists()).toBe(true)
expect(wrapper.find('.transform-pane').exists()).toBe(true)
})
it('should apply transform style from composable', () => {
mockTransformState.transformStyle.value = {
transform: 'scale(2) translate(100px, 50px)',
transformOrigin: '0 0'
}
wrapper = mount(TransformPane, {
props: {
canvas: mockCanvas
}
})
const transformPane = wrapper.find('.transform-pane')
const style = transformPane.attributes('style')
expect(style).toContain('transform: scale(2) translate(100px, 50px)')
})
it('should render slot content', () => {
wrapper = mount(TransformPane, {
props: {
canvas: mockCanvas
},
slots: {
default: '<div class="test-content">Test Node</div>'
}
})
expect(wrapper.find('.test-content').exists()).toBe(true)
expect(wrapper.find('.test-content').text()).toBe('Test Node')
})
})
describe('RAF synchronization', () => {
it('should start RAF sync on mount', async () => {
wrapper = mount(TransformPane, {
props: {
canvas: mockCanvas
}
})
await nextTick()
// Should emit RAF status change to true
expect(wrapper.emitted('rafStatusChange')).toBeTruthy()
expect(wrapper.emitted('rafStatusChange')?.[0]).toEqual([true])
})
it('should call syncWithCanvas during RAF updates', async () => {
wrapper = mount(TransformPane, {
props: {
canvas: mockCanvas
}
})
await nextTick()
// Allow RAF to execute
await new Promise((resolve) => setTimeout(resolve, 20))
expect(mockTransformState.syncWithCanvas).toHaveBeenCalledWith(mockCanvas)
})
it('should emit transform update timing', async () => {
wrapper = mount(TransformPane, {
props: {
canvas: mockCanvas
}
})
await nextTick()
// Allow RAF to execute
await new Promise((resolve) => setTimeout(resolve, 20))
expect(wrapper.emitted('transformUpdate')).toBeTruthy()
const updateEvent = wrapper.emitted('transformUpdate')?.[0]
expect(typeof updateEvent?.[0]).toBe('number')
expect(updateEvent?.[0]).toBeGreaterThanOrEqual(0)
})
it('should stop RAF sync on unmount', async () => {
wrapper = mount(TransformPane, {
props: {
canvas: mockCanvas
}
})
await nextTick()
wrapper.unmount()
expect(wrapper.emitted('rafStatusChange')).toBeTruthy()
const events = wrapper.emitted('rafStatusChange') as any[]
expect(events[events.length - 1]).toEqual([false])
expect(global.cancelAnimationFrame).toHaveBeenCalled()
})
})
describe('canvas event listeners', () => {
it('should add event listeners to canvas on mount', async () => {
wrapper = mount(TransformPane, {
props: {
canvas: mockCanvas
}
})
await nextTick()
expect(mockCanvas.canvas.addEventListener).toHaveBeenCalledWith(
'wheel',
expect.any(Function),
expect.any(Object)
)
expect(mockCanvas.canvas.addEventListener).toHaveBeenCalledWith(
'pointerdown',
expect.any(Function),
expect.any(Object)
)
expect(mockCanvas.canvas.addEventListener).toHaveBeenCalledWith(
'pointerup',
expect.any(Function),
expect.any(Object)
)
expect(mockCanvas.canvas.addEventListener).toHaveBeenCalledWith(
'pointercancel',
expect.any(Function),
expect.any(Object)
)
})
it('should remove event listeners on unmount', async () => {
wrapper = mount(TransformPane, {
props: {
canvas: mockCanvas
}
})
await nextTick()
wrapper.unmount()
expect(mockCanvas.canvas.removeEventListener).toHaveBeenCalledWith(
'wheel',
expect.any(Function),
expect.any(Object)
)
expect(mockCanvas.canvas.removeEventListener).toHaveBeenCalledWith(
'pointerdown',
expect.any(Function),
expect.any(Object)
)
expect(mockCanvas.canvas.removeEventListener).toHaveBeenCalledWith(
'pointerup',
expect.any(Function),
expect.any(Object)
)
expect(mockCanvas.canvas.removeEventListener).toHaveBeenCalledWith(
'pointercancel',
expect.any(Function),
expect.any(Object)
)
})
})
describe('interaction state management', () => {
it('should apply interacting class during interactions', async () => {
wrapper = mount(TransformPane, {
props: {
canvas: mockCanvas
}
})
// Simulate interaction start by checking internal state
// Note: This tests the CSS class application logic
const transformPane = wrapper.find('.transform-pane')
// Initially should not have interacting class
expect(transformPane.classes()).not.toContain(
'transform-pane--interacting'
)
})
it('should handle pointer events for node delegation', async () => {
wrapper = mount(TransformPane, {
props: {
canvas: mockCanvas
}
})
const transformPane = wrapper.find('.transform-pane')
// Simulate pointer down - we can't test the exact delegation logic
// in unit tests due to vue-test-utils limitations, but we can verify
// the event handler is set up correctly
await transformPane.trigger('pointerdown')
// The test passes if no errors are thrown during event handling
expect(transformPane.exists()).toBe(true)
})
})
describe('transform state integration', () => {
it('should provide transform utilities to child components', () => {
wrapper = mount(TransformPane, {
props: {
canvas: mockCanvas
}
})
// The component should provide transform state via Vue's provide/inject
// This is tested indirectly through the composable integration
expect(mockTransformState.syncWithCanvas).toBeDefined()
expect(mockTransformState.canvasToScreen).toBeDefined()
expect(mockTransformState.screenToCanvas).toBeDefined()
})
})
describe('error handling', () => {
it('should handle null canvas gracefully', () => {
wrapper = mount(TransformPane, {
props: {
canvas: undefined
}
})
expect(wrapper.exists()).toBe(true)
expect(wrapper.find('.transform-pane').exists()).toBe(true)
})
it('should handle missing canvas properties', () => {
const incompleteCanvas = {} as any
wrapper = mount(TransformPane, {
props: {
canvas: incompleteCanvas
}
})
expect(wrapper.exists()).toBe(true)
// Should not throw errors during mount
})
})
describe('performance optimizations', () => {
it('should use contain CSS property for layout optimization', () => {
wrapper = mount(TransformPane, {
props: {
canvas: mockCanvas
}
})
const transformPane = wrapper.find('.transform-pane')
// This test verifies the CSS contains the performance optimization
// Note: In JSDOM, computed styles might not reflect all CSS properties
expect(transformPane.element.className).toContain('transform-pane')
})
it('should disable pointer events on container but allow on children', () => {
wrapper = mount(TransformPane, {
props: {
canvas: mockCanvas
},
slots: {
default: '<div data-node-id="test">Test Node</div>'
}
})
const transformPane = wrapper.find('.transform-pane')
// The CSS should handle pointer events optimization
// This is primarily a CSS concern, but we verify the structure
expect(transformPane.exists()).toBe(true)
expect(wrapper.find('[data-node-id="test"]').exists()).toBe(true)
})
})
})

View File

@@ -1,91 +0,0 @@
<template>
<div
class="transform-pane"
:class="{ 'transform-pane--interacting': isInteracting }"
:style="transformStyle"
@pointerdown="handlePointerDown"
>
<!-- Vue nodes will be rendered here -->
<slot />
</div>
</template>
<script setup lang="ts">
import { computed, provide } from 'vue'
import { useTransformState } from '@/composables/element/useTransformState'
import { useCanvasTransformSync } from '@/composables/graph/useCanvasTransformSync'
import { useTransformSettling } from '@/composables/graph/useTransformSettling'
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
interface TransformPaneProps {
canvas?: LGraphCanvas
}
const props = defineProps<TransformPaneProps>()
const {
camera,
transformStyle,
syncWithCanvas,
canvasToScreen,
screenToCanvas,
isNodeInViewport
} = useTransformState()
const canvasElement = computed(() => props.canvas?.canvas)
const { isTransforming: isInteracting } = useTransformSettling(canvasElement, {
settleDelay: 200,
trackPan: true
})
provide('transformState', {
camera,
canvasToScreen,
screenToCanvas,
isNodeInViewport
})
const handlePointerDown = (event: PointerEvent) => {
const target = event.target as HTMLElement
const nodeElement = target.closest('[data-node-id]')
if (nodeElement) {
// TODO: Emit event for node interaction
// Node interaction with nodeId will be handled in future implementation
}
}
const emit = defineEmits<{
rafStatusChange: [active: boolean]
transformUpdate: [time: number]
}>()
useCanvasTransformSync(props.canvas, syncWithCanvas, {
onStart: () => emit('rafStatusChange', true),
onUpdate: (duration) => emit('transformUpdate', duration),
onStop: () => emit('rafStatusChange', false)
})
</script>
<style scoped>
.transform-pane {
position: absolute;
inset: 0;
transform-origin: 0 0;
pointer-events: none;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
.transform-pane--interacting {
will-change: transform;
}
/* Allow pointer events on nodes */
.transform-pane :deep([data-node-id]) {
pointer-events: auto;
}
</style>