mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-28 18:22:40 +00:00
## 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)
137 lines
3.1 KiB
TypeScript
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
|
|
}
|
|
}
|