[refactor] Improve renderer domain organization (#5552)

* [refactor] Improve renderer architecture organization

Building on PR #5388, this refines the renderer domain structure:

**Key improvements:**
- Group all transform utilities in `transform/` subdirectory for better cohesion
- Move canvas state to dedicated `renderer/core/canvas/` domain
- Consolidate coordinate system logic (TransformPane, useTransformState, sync utilities)

**File organization:**
- `renderer/core/canvas/canvasStore.ts` (was `stores/graphStore.ts`)
- `renderer/core/layout/transform/` contains all coordinate system utilities
- Transform sync utilities co-located with core transform logic

This creates clearer domain boundaries and groups related functionality
while building on the foundation established in PR #5388.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Clean up linter-modified files

* Fix import paths and clean up unused imports after rebase

- Update all remaining @/stores/graphStore references to @/renderer/core/canvas/canvasStore
- Remove unused imports from selection toolbox components
- All tests pass, only reka-ui upstream issue remains in typecheck

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* [auto-fix] Apply ESLint and Prettier fixes

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Christian Byrne
2025-09-14 21:28:08 -07:00
committed by GitHub
parent 8c6ee026c0
commit 6349ceee6c
65 changed files with 89 additions and 72 deletions

View File

@@ -0,0 +1,110 @@
import { defineStore } from 'pinia'
import { type Raw, computed, markRaw, ref, shallowRef } from 'vue'
import type { Point, Positionable } from '@/lib/litegraph/src/interfaces'
import type {
LGraphCanvas,
LGraphGroup,
LGraphNode
} from '@/lib/litegraph/src/litegraph'
import { app } from '@/scripts/app'
import { isLGraphGroup, isLGraphNode, isReroute } from '@/utils/litegraphUtil'
export const useTitleEditorStore = defineStore('titleEditor', () => {
const titleEditorTarget = shallowRef<LGraphNode | LGraphGroup | null>(null)
return {
titleEditorTarget
}
})
export const useCanvasStore = defineStore('canvas', () => {
/**
* The LGraphCanvas instance.
*
* The root LGraphCanvas object is a shallow ref.
*/
const canvas = shallowRef<LGraphCanvas | null>(null)
/**
* The selected items on the canvas. All stored items are raw.
*/
const selectedItems = ref<Raw<Positionable>[]>([])
const updateSelectedItems = () => {
const items = Array.from(canvas.value?.selectedItems ?? [])
selectedItems.value = items.map((item) => markRaw(item))
}
// Reactive scale percentage that syncs with app.canvas.ds.scale
const appScalePercentage = ref(100)
// Set up scale synchronization when canvas is available
let originalOnChanged: ((scale: number, offset: Point) => void) | undefined =
undefined
const initScaleSync = () => {
if (app.canvas?.ds) {
// Initial sync
originalOnChanged = app.canvas.ds.onChanged
appScalePercentage.value = Math.round(app.canvas.ds.scale * 100)
// Set up continuous sync
app.canvas.ds.onChanged = () => {
if (app.canvas?.ds?.scale) {
appScalePercentage.value = Math.round(app.canvas.ds.scale * 100)
}
// Call original handler if exists
originalOnChanged?.(app.canvas.ds.scale, app.canvas.ds.offset)
}
}
}
const cleanupScaleSync = () => {
if (app.canvas?.ds) {
app.canvas.ds.onChanged = originalOnChanged
originalOnChanged = undefined
}
}
const nodeSelected = computed(() => selectedItems.value.some(isLGraphNode))
const groupSelected = computed(() => selectedItems.value.some(isLGraphGroup))
const rerouteSelected = computed(() => selectedItems.value.some(isReroute))
const getCanvas = () => {
if (!canvas.value) throw new Error('getCanvas: canvas is null')
return canvas.value
}
/**
* Sets the canvas zoom level from a percentage value
* @param percentage - Zoom percentage value (1-1000, where 1000 = 1000% zoom)
*/
const setAppZoomFromPercentage = (percentage: number) => {
if (!app.canvas?.ds || percentage <= 0) return
// Convert percentage to scale (1000% = 10.0 scale)
const newScale = percentage / 100
const ds = app.canvas.ds
ds.changeScale(
newScale,
ds.element ? [ds.element.width / 2, ds.element.height / 2] : undefined
)
app.canvas.setDirty(true, true)
// Update reactive value immediately for UI consistency
appScalePercentage.value = Math.round(newScale * 100)
}
return {
canvas,
selectedItems,
nodeSelected,
groupSelected,
rerouteSelected,
appScalePercentage,
updateSelectedItems,
getCanvas,
setAppZoomFromPercentage,
initScaleSync,
cleanupScaleSync
}
})

View File

@@ -2,7 +2,7 @@ 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'
import TransformPane from '../transform/TransformPane.vue'
// Mock the transform state composable
const mockTransformState = {

View File

@@ -13,10 +13,10 @@
<script setup lang="ts">
import { computed, provide } from 'vue'
import { useCanvasTransformSync } from '@/composables/graph/useCanvasTransformSync'
import { useTransformSettling } from '@/composables/graph/useTransformSettling'
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import { useTransformState } from '@/renderer/core/layout/useTransformState'
import { useCanvasTransformSync } from '@/renderer/core/layout/transform/useCanvasTransformSync'
import { useTransformSettling } from '@/renderer/core/layout/transform/useTransformSettling'
import { useTransformState } from '@/renderer/core/layout/transform/useTransformState'
interface TransformPaneProps {
canvas?: LGraphCanvas

View File

@@ -0,0 +1,115 @@
import { onUnmounted, ref } from 'vue'
import type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
interface CanvasTransformSyncOptions {
/**
* Whether to automatically start syncing when canvas is available
* @default true
*/
autoStart?: boolean
}
interface CanvasTransformSyncCallbacks {
/**
* Called when sync starts
*/
onStart?: () => void
/**
* Called after each sync update with timing information
*/
onUpdate?: (duration: number) => void
/**
* Called when sync stops
*/
onStop?: () => void
}
/**
* 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, provides performance timing,
* and ensures proper cleanup.
*
* The sync function typically reads canvas.ds (draw state) properties like offset and scale
* to keep Vue components aligned with the canvas coordinate system.
*
* @example
* ```ts
* const { isActive, startSync, stopSync } = useCanvasTransformSync(
* canvas,
* (canvas) => syncWithCanvas(canvas),
* {
* onStart: () => emit('rafStatusChange', true),
* onUpdate: (time) => emit('transformUpdate', time),
* onStop: () => emit('rafStatusChange', false)
* }
* )
* ```
*/
export function useCanvasTransformSync(
canvas: LGraphCanvas | undefined | null,
syncFn: (canvas: LGraphCanvas) => void,
callbacks: CanvasTransformSyncCallbacks = {},
options: CanvasTransformSyncOptions = {}
) {
const { autoStart = true } = options
const { onStart, onUpdate, onStop } = callbacks
const isActive = ref(false)
let rafId: number | null = null
const startSync = () => {
if (isActive.value || !canvas) return
isActive.value = true
onStart?.()
const sync = () => {
if (!isActive.value || !canvas) return
try {
const startTime = performance.now()
syncFn(canvas)
const endTime = performance.now()
onUpdate?.(endTime - startTime)
} catch (error) {
console.warn('Canvas transform sync error:', error)
}
rafId = requestAnimationFrame(sync)
}
sync()
}
const stopSync = () => {
if (!isActive.value) return
if (rafId !== null) {
cancelAnimationFrame(rafId)
rafId = null
}
isActive.value = false
onStop?.()
}
// Auto-start if canvas is available and autoStart is enabled
if (autoStart && canvas) {
startSync()
}
// Clean up on unmount
onUnmounted(() => {
stopSync()
})
return {
isActive,
startSync,
stopSync
}
}

View File

@@ -0,0 +1,151 @@
import { useDebounceFn, useEventListener, useThrottleFn } from '@vueuse/core'
import { ref } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
interface TransformSettlingOptions {
/**
* Delay in ms before transform is considered "settled" after last interaction
* @default 200
*/
settleDelay?: number
/**
* Whether to track both zoom (wheel) and pan (pointer drag) interactions
* @default false
*/
trackPan?: boolean
/**
* Throttle delay for high-frequency pointermove events (only used when trackPan is true)
* @default 16 (~60fps)
*/
pointerMoveThrottle?: number
/**
* Whether to use passive event listeners (better performance but can't preventDefault)
* @default true
*/
passive?: boolean
}
/**
* Tracks when canvas transforms (zoom/pan) are actively changing vs settled.
*
* This composable helps optimize rendering quality during transformations.
* When the user is actively zooming or panning, we can reduce rendering quality
* for better performance. Once the transform "settles" (stops changing), we can
* trigger high-quality re-rasterization.
*
* The settling concept prevents constant quality switching during interactions
* by waiting for a period of inactivity before considering the transform complete.
*
* Uses VueUse's useEventListener for automatic cleanup and useDebounceFn for
* efficient settle detection.
*
* @example
* ```ts
* const { isTransforming } = useTransformSettling(canvasRef, {
* settleDelay: 200,
* trackPan: true
* })
*
* // Use in CSS classes or rendering logic
* const cssClass = computed(() => ({
* 'low-quality': isTransforming.value,
* 'high-quality': !isTransforming.value
* }))
* ```
*/
export function useTransformSettling(
target: MaybeRefOrGetter<HTMLElement | null | undefined>,
options: TransformSettlingOptions = {}
) {
const {
settleDelay = 200,
trackPan = false,
pointerMoveThrottle = 16,
passive = true
} = options
const isTransforming = ref(false)
let isPanning = false
/**
* Mark transform as active
*/
const markTransformActive = () => {
isTransforming.value = true
}
/**
* Mark transform as settled (debounced)
*/
const markTransformSettled = useDebounceFn(() => {
isTransforming.value = false
}, settleDelay)
/**
* Handle any transform event - mark active then queue settle
*/
const handleTransformEvent = () => {
markTransformActive()
void markTransformSettled()
}
// Wheel handler
const handleWheel = () => {
handleTransformEvent()
}
// Pointer handlers for panning
const handlePointerDown = () => {
if (trackPan) {
isPanning = true
handleTransformEvent()
}
}
// Throttled pointer move handler for performance
const handlePointerMove = trackPan
? useThrottleFn(() => {
if (isPanning) {
handleTransformEvent()
}
}, pointerMoveThrottle)
: undefined
const handlePointerEnd = () => {
if (trackPan) {
isPanning = false
// Don't immediately stop - let the debounced settle handle it
}
}
// Register event listeners with auto-cleanup
useEventListener(target, 'wheel', handleWheel, {
capture: true,
passive
})
if (trackPan) {
useEventListener(target, 'pointerdown', handlePointerDown, {
capture: true
})
if (handlePointerMove) {
useEventListener(target, 'pointermove', handlePointerMove, {
capture: true,
passive
})
}
useEventListener(target, 'pointerup', handlePointerEnd, {
capture: true
})
useEventListener(target, 'pointercancel', handlePointerEnd, {
capture: true
})
}
return {
isTransforming
}
}

View File

@@ -2,7 +2,7 @@ import { useRafFn } from '@vueuse/core'
import { computed, nextTick, ref, watch } from 'vue'
import type { LGraph } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/stores/graphStore'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useSettingStore } from '@/stores/settingStore'
import { useWorkflowStore } from '@/stores/workflowStore'

View File

@@ -11,8 +11,8 @@
import type { Ref } from 'vue'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'
import { useCanvasStore } from '@/stores/graphStore'
interface NodeManager {
getNode: (id: string) => any

View File

@@ -8,11 +8,11 @@ import type {
IBaseWidget,
IWidgetOptions
} from '@/lib/litegraph/src/types/widgets'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
import { app } from '@/scripts/app'
import { calculateImageGrid } from '@/scripts/ui/imagePreview'
import { ComfyWidgetConstructorV2 } from '@/scripts/widgets'
import { useCanvasStore } from '@/stores/graphStore'
import { useSettingStore } from '@/stores/settingStore'
import { is_all_same_aspect_ratio } from '@/utils/imageUtil'

View File

@@ -1,9 +1,9 @@
import type { LGraph } from '@/lib/litegraph/src/litegraph'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import {
calculateMinimapScale,
calculateNodeBounds
} from '@/renderer/core/spatial/boundsCalculator'
import { useCanvasStore } from '@/stores/graphStore'
import { useWorkflowStore } from '@/stores/workflowStore'
import { renderMinimapToCanvas } from '../extensions/minimap/minimapCanvasRenderer'