Files
ComfyUI_frontend/src/composables/maskeditor/useCanvasHistory.ts
Terry Jia 1a6913c466 fully refactor mask editor into vue-based (#6629)
## Summary

This PR refactors the mask editor from a vanilla JavaScript
implementation to Vue 3 + Composition API, aligning it with the ComfyUI
frontend's modern architecture. This is a structural refactor without UI
changes - all visual appearances and user interactions remain identical.

Net change: +1,700 lines (mostly tests)

## Changes

- Converted from class-based managers to Vue 3 Composition API
- Migrated state management to Pinia stores (maskEditorStore,
maskEditorDataStore)
- Split monolithic managers into focused composables:
    - useBrushDrawing - Brush rendering and drawing logic
    - useCanvasManager - Canvas lifecycle and operations
    - useCanvasTools - Tool-specific canvas operations
    - usePanAndZoom - Pan and zoom functionality
    - useToolManager - Tool selection and coordination
    - useKeyboard - Keyboard shortcuts
    - useMaskEditorLoader/Saver - Data loading and saving
    - useCoordinateTransform - Coordinate system transformations
- Replaced imperative DOM manipulation with Vue components
- Added comprehensive test coverage

## What This PR Does NOT Change

  Preserved Original Styling:
  - Original CSS retained in packages/design-system/src/css/style.css
- Some generic controls (DropdownControl, SliderControl, ToggleControl)
preserved as-is
- Future migration to Tailwind and PrimeVue components is planned but
out of scope for this PR

  Preserved Core Functionality:
  - Drawing algorithms and brush rendering logic remain unchanged
  - Pan/zoom calculations preserved
  - Canvas operations (composite modes, image processing) unchanged
  - Tool behaviors (brush, color select, paint bucket) identical
  - No changes to mask generation or export logic

DO NOT Review:
  -  CSS styling choices (preserved from original)
  - Drawing algorithm implementations (unchanged)
  -  Canvas rendering logic (ported as-is)
  - UI/UX changes (none exist)
  - Component library choices (future work)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6629-fully-refactor-mask-editor-into-vue-based-2a46d73d36508114ab8bd2984b4b54e4)
by [Unito](https://www.unito.io)
2025-11-13 20:57:03 -08:00

137 lines
3.1 KiB
TypeScript

import { ref, computed } from 'vue'
import { useMaskEditorStore } from '@/stores/maskEditorStore'
export function useCanvasHistory(maxStates = 20) {
const store = useMaskEditorStore()
const states = ref<{ mask: ImageData; rgb: ImageData }[]>([])
const currentStateIndex = ref(-1)
const initialized = ref(false)
const canUndo = computed(
() => states.value.length > 1 && currentStateIndex.value > 0
)
const canRedo = computed(() => {
return (
states.value.length > 1 &&
currentStateIndex.value < states.value.length - 1
)
})
const saveInitialState = () => {
const maskCtx = store.maskCtx
const rgbCtx = store.rgbCtx
const maskCanvas = store.maskCanvas
const rgbCanvas = store.rgbCanvas
if (!maskCtx || !rgbCtx || !maskCanvas || !rgbCanvas) {
requestAnimationFrame(saveInitialState)
return
}
if (!maskCanvas.width || !rgbCanvas.width) {
requestAnimationFrame(saveInitialState)
return
}
states.value = []
const maskState = maskCtx.getImageData(
0,
0,
maskCanvas.width,
maskCanvas.height
)
const rgbState = rgbCtx.getImageData(
0,
0,
rgbCanvas.width,
rgbCanvas.height
)
states.value.push({ mask: maskState, rgb: rgbState })
currentStateIndex.value = 0
initialized.value = true
}
const saveState = () => {
const maskCtx = store.maskCtx
const rgbCtx = store.rgbCtx
const maskCanvas = store.maskCanvas
const rgbCanvas = store.rgbCanvas
if (!maskCtx || !rgbCtx || !maskCanvas || !rgbCanvas) return
if (!initialized.value || currentStateIndex.value === -1) {
saveInitialState()
return
}
states.value = states.value.slice(0, currentStateIndex.value + 1)
const maskState = maskCtx.getImageData(
0,
0,
maskCanvas.width,
maskCanvas.height
)
const rgbState = rgbCtx.getImageData(
0,
0,
rgbCanvas.width,
rgbCanvas.height
)
states.value.push({ mask: maskState, rgb: rgbState })
currentStateIndex.value++
if (states.value.length > maxStates) {
states.value.shift()
currentStateIndex.value--
}
}
const undo = () => {
if (!canUndo.value) {
alert('No more undo states available')
return
}
currentStateIndex.value--
restoreState(states.value[currentStateIndex.value])
}
const redo = () => {
if (!canRedo.value) {
alert('No more redo states available')
return
}
currentStateIndex.value++
restoreState(states.value[currentStateIndex.value])
}
const restoreState = (state: { mask: ImageData; rgb: ImageData }) => {
const maskCtx = store.maskCtx
const rgbCtx = store.rgbCtx
if (!maskCtx || !rgbCtx) return
maskCtx.putImageData(state.mask, 0, 0)
rgbCtx.putImageData(state.rgb, 0, 0)
}
const clearStates = () => {
states.value = []
currentStateIndex.value = -1
initialized.value = false
}
return {
canUndo,
canRedo,
saveInitialState,
saveState,
undo,
redo,
clearStates
}
}