mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-29 18:52:19 +00:00
Fixed Brush Settings Post Refactor and Added Numeric Control (#7783)
## Summary
Refactored BrushSettingsPanel layout to stack labels and number inputs
above sliders, and fixed brush size keybinding limits to match the
updated 1-250 range.
## Changes
- **What**:
- Reorganized BrushSettingsPanel UI to display labels and number inputs
in a row above each slider (instead of side-by-side), creating a cleaner
vertical layout with better visual hierarchy.
- Updated brush size increase/decrease keybindings to clamp between
1-250 (previously 1-100) to match the refactored slider limits.
- Added setting for color picker keybinding
- **Breaking**: None
## Review Focus
- Verify the stacked layout (label + number input above slider) works
well across different panel widths
- Confirm all slider controls properly sync with their corresponding
number inputs
- Test brush size keybindings (increase/decrease) respect the new 1-250
limits
## Screenshot
<img width="1713" height="848" alt="image"
src="https://github.com/user-attachments/assets/22a26ad2-61be-4031-92d0-b4577a003552"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7783-Fixed-Brush-Settings-Port-Refactor-and-Added-Numeric-Control-2d76d73d365081bda7a8e12d3c649085)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
This commit is contained in:
@@ -1,8 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="flex flex-col gap-3 pb-3">
|
<div class="flex flex-col gap-3 pb-3">
|
||||||
<h3
|
<h3 class="text-center text-[15px] font-sans text-descrip-text mt-2.5">
|
||||||
class="text-center text-[15px] font-sans text-[var(--descrip-text)] mt-2.5"
|
|
||||||
>
|
|
||||||
{{ t('maskEditor.brushSettings') }}
|
{{ t('maskEditor.brushSettings') }}
|
||||||
</h3>
|
</h3>
|
||||||
|
|
||||||
@@ -10,120 +8,211 @@
|
|||||||
{{ t('maskEditor.resetToDefault') }}
|
{{ t('maskEditor.resetToDefault') }}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
|
<!-- Brush Shape -->
|
||||||
<div class="flex flex-col gap-3 pb-3">
|
<div class="flex flex-col gap-3 pb-3">
|
||||||
<span class="text-left text-xs font-sans text-[var(--descrip-text)]">{{
|
<span class="text-left text-xs font-sans text-descrip-text">
|
||||||
t('maskEditor.brushShape')
|
{{ t('maskEditor.brushShape') }}
|
||||||
}}</span>
|
</span>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex flex-row gap-2.5 items-center min-h-6 relative h-[50px] w-full rounded-[10px] bg-secondary-background-hover"
|
class="flex flex-row gap-2.5 items-center h-[50px] w-full rounded-[10px] bg-secondary-background-hover"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="maskEditor_sidePanelBrushShapeCircle bg-transparent hover:bg-comfy-menu-bg"
|
class="maskEditor_sidePanelBrushShapeCircle hover:bg-comfy-menu-bg"
|
||||||
:class="{ active: store.brushSettings.type === BrushShape.Arc }"
|
:class="
|
||||||
:style="{
|
cn(
|
||||||
background:
|
|
||||||
store.brushSettings.type === BrushShape.Arc
|
store.brushSettings.type === BrushShape.Arc
|
||||||
? 'var(--p-button-text-primary-color)'
|
? 'bg-[var(--p-button-text-primary-color)] active'
|
||||||
: ''
|
: 'bg-transparent'
|
||||||
}"
|
)
|
||||||
|
"
|
||||||
@click="setBrushShape(BrushShape.Arc)"
|
@click="setBrushShape(BrushShape.Arc)"
|
||||||
></div>
|
></div>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="maskEditor_sidePanelBrushShapeSquare bg-transparent hover:bg-comfy-menu-bg"
|
class="maskEditor_sidePanelBrushShapeSquare hover:bg-comfy-menu-bg"
|
||||||
:class="{ active: store.brushSettings.type === BrushShape.Rect }"
|
:class="
|
||||||
:style="{
|
cn(
|
||||||
background:
|
|
||||||
store.brushSettings.type === BrushShape.Rect
|
store.brushSettings.type === BrushShape.Rect
|
||||||
? 'var(--p-button-text-primary-color)'
|
? 'bg-[var(--p-button-text-primary-color)] active'
|
||||||
: ''
|
: 'bg-transparent'
|
||||||
}"
|
)
|
||||||
|
"
|
||||||
@click="setBrushShape(BrushShape.Rect)"
|
@click="setBrushShape(BrushShape.Rect)"
|
||||||
></div>
|
></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Color -->
|
||||||
<div class="flex flex-col gap-3 pb-3">
|
<div class="flex flex-col gap-3 pb-3">
|
||||||
<span class="text-left text-xs font-sans text-[var(--descrip-text)]">{{
|
<span class="text-left text-xs font-sans text-descrip-text">
|
||||||
t('maskEditor.colorSelector')
|
{{ t('maskEditor.colorSelector') }}
|
||||||
}}</span>
|
</span>
|
||||||
<input type="color" :value="store.rgbColor" @input="onColorChange" />
|
<input
|
||||||
|
ref="colorInputRef"
|
||||||
|
v-model="store.rgbColor"
|
||||||
|
type="color"
|
||||||
|
class="h-10 rounded-md cursor-pointer"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<SliderControl
|
<!-- Thickness -->
|
||||||
:label="t('maskEditor.thickness')"
|
<div class="flex flex-col gap-2">
|
||||||
:min="1"
|
<div class="flex items-center justify-between">
|
||||||
:max="500"
|
<span class="text-left text-xs font-sans text-descrip-text">
|
||||||
:step="1"
|
{{ t('maskEditor.thickness') }}
|
||||||
:model-value="store.brushSettings.size"
|
</span>
|
||||||
@update:model-value="onThicknessChange"
|
<input
|
||||||
/>
|
v-model.number="brushSize"
|
||||||
|
type="number"
|
||||||
|
class="w-16 px-2 py-1 text-sm text-center border rounded-md bg-comfy-menu-bg border-p-form-field-border-color text-input-text"
|
||||||
|
:min="1"
|
||||||
|
:max="250"
|
||||||
|
:step="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SliderControl
|
||||||
|
v-model="brushSize"
|
||||||
|
class="flex-1"
|
||||||
|
label=""
|
||||||
|
:min="1"
|
||||||
|
:max="250"
|
||||||
|
:step="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SliderControl
|
<!-- Opacity -->
|
||||||
:label="t('maskEditor.opacity')"
|
<div class="flex flex-col gap-2">
|
||||||
:min="0"
|
<div class="flex items-center justify-between">
|
||||||
:max="1"
|
<span class="text-left text-xs font-sans text-descrip-text">
|
||||||
:step="0.01"
|
{{ t('maskEditor.opacity') }}
|
||||||
:model-value="store.brushSettings.opacity"
|
</span>
|
||||||
@update:model-value="onOpacityChange"
|
<input
|
||||||
/>
|
v-model.number="brushOpacity"
|
||||||
|
type="number"
|
||||||
|
class="w-16 px-2 py-1 text-sm text-center border rounded-md bg-comfy-menu-bg border-p-form-field-border-color text-input-text"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SliderControl
|
||||||
|
v-model="brushOpacity"
|
||||||
|
class="flex-1"
|
||||||
|
label=""
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SliderControl
|
<!-- Hardness -->
|
||||||
:label="t('maskEditor.hardness')"
|
<div class="flex flex-col gap-2">
|
||||||
:min="0"
|
<div class="flex items-center justify-between">
|
||||||
:max="1"
|
<span class="text-left text-xs font-sans text-descrip-text">
|
||||||
:step="0.01"
|
{{ t('maskEditor.hardness') }}
|
||||||
:model-value="store.brushSettings.hardness"
|
</span>
|
||||||
@update:model-value="onHardnessChange"
|
<input
|
||||||
/>
|
v-model.number="brushHardness"
|
||||||
|
type="number"
|
||||||
|
class="w-16 px-2 py-1 text-sm text-center border rounded-md bg-comfy-menu-bg border-p-form-field-border-color text-input-text"
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SliderControl
|
||||||
|
v-model="brushHardness"
|
||||||
|
class="flex-1"
|
||||||
|
label=""
|
||||||
|
:min="0"
|
||||||
|
:max="1"
|
||||||
|
:step="0.01"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
<SliderControl
|
<!-- Step Size -->
|
||||||
:label="$t('maskEditor.stepSize')"
|
<div class="flex flex-col gap-2">
|
||||||
:min="1"
|
<div class="flex items-center justify-between">
|
||||||
:max="100"
|
<span class="text-left text-xs font-sans text-descrip-text">
|
||||||
:step="1"
|
{{ t('maskEditor.stepSize') }}
|
||||||
:model-value="store.brushSettings.stepSize"
|
</span>
|
||||||
@update:model-value="onStepSizeChange"
|
<input
|
||||||
/>
|
v-model.number="brushStepSize"
|
||||||
|
type="number"
|
||||||
|
class="w-16 px-2 py-1 text-sm text-center border rounded-md bg-comfy-menu-bg border-p-form-field-border-color text-input-text"
|
||||||
|
:min="1"
|
||||||
|
:max="100"
|
||||||
|
:step="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<SliderControl
|
||||||
|
v-model="brushStepSize"
|
||||||
|
class="flex-1"
|
||||||
|
label=""
|
||||||
|
:min="1"
|
||||||
|
:max="100"
|
||||||
|
:step="1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
|
||||||
import { BrushShape } from '@/extensions/core/maskeditor/types'
|
import { BrushShape } from '@/extensions/core/maskeditor/types'
|
||||||
import { t } from '@/i18n'
|
import { t } from '@/i18n'
|
||||||
import { useMaskEditorStore } from '@/stores/maskEditorStore'
|
import { useMaskEditorStore } from '@/stores/maskEditorStore'
|
||||||
|
import { cn } from '@/utils/tailwindUtil'
|
||||||
|
|
||||||
import SliderControl from './controls/SliderControl.vue'
|
import SliderControl from './controls/SliderControl.vue'
|
||||||
|
|
||||||
const store = useMaskEditorStore()
|
const store = useMaskEditorStore()
|
||||||
|
|
||||||
const textButtonClass =
|
const colorInputRef = ref<HTMLInputElement>()
|
||||||
'h-7.5 w-32 rounded-[10px] border border-[var(--p-form-field-border-color)] text-[var(--input-text)] font-sans pointer-events-auto transition-colors duration-100 bg-[var(--comfy-menu-bg)] hover:bg-secondary-background-hover'
|
|
||||||
|
|
||||||
|
const textButtonClass =
|
||||||
|
'h-7.5 w-32 rounded-[10px] border border-p-form-field-border-color text-input-text font-sans transition-colors duration-100 bg-comfy-menu-bg hover:bg-secondary-background-hover'
|
||||||
|
|
||||||
|
/* Computed properties that use store setters for validation */
|
||||||
|
const brushSize = computed({
|
||||||
|
get: () => store.brushSettings.size,
|
||||||
|
set: (value: number) => store.setBrushSize(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const brushOpacity = computed({
|
||||||
|
get: () => store.brushSettings.opacity,
|
||||||
|
set: (value: number) => store.setBrushOpacity(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const brushHardness = computed({
|
||||||
|
get: () => store.brushSettings.hardness,
|
||||||
|
set: (value: number) => store.setBrushHardness(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
const brushStepSize = computed({
|
||||||
|
get: () => store.brushSettings.stepSize,
|
||||||
|
set: (value: number) => store.setBrushStepSize(value)
|
||||||
|
})
|
||||||
|
|
||||||
|
/* Brush shape */
|
||||||
const setBrushShape = (shape: BrushShape) => {
|
const setBrushShape = (shape: BrushShape) => {
|
||||||
store.brushSettings.type = shape
|
store.brushSettings.type = shape
|
||||||
}
|
}
|
||||||
|
|
||||||
const onColorChange = (event: Event) => {
|
/* Reset */
|
||||||
store.rgbColor = (event.target as HTMLInputElement).value
|
|
||||||
}
|
|
||||||
|
|
||||||
const onThicknessChange = (value: number) => {
|
|
||||||
store.setBrushSize(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onOpacityChange = (value: number) => {
|
|
||||||
store.setBrushOpacity(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onHardnessChange = (value: number) => {
|
|
||||||
store.setBrushHardness(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const onStepSizeChange = (value: number) => {
|
|
||||||
store.setBrushStepSize(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resetToDefault = () => {
|
const resetToDefault = () => {
|
||||||
store.resetBrushToDefault()
|
store.resetBrushToDefault()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (colorInputRef.value) {
|
||||||
|
store.colorInput = colorInputRef.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
store.colorInput = null
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -202,6 +202,7 @@ onBeforeUnmount(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
store.canvasHistory.clearStates()
|
store.canvasHistory.clearStates()
|
||||||
|
|
||||||
store.resetState()
|
store.resetState()
|
||||||
dataStore.reset()
|
dataStore.reset()
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -37,6 +37,15 @@ function isOpened(): boolean {
|
|||||||
return useDialogStore().isDialogOpen('global-mask-editor')
|
return useDialogStore().isDialogOpen('global-mask-editor')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const changeBrushSize = async (sizeChanger: (oldSize: number) => number) => {
|
||||||
|
if (!isOpened()) return
|
||||||
|
|
||||||
|
const store = useMaskEditorStore()
|
||||||
|
const oldBrushSize = store.brushSettings.size
|
||||||
|
const newBrushSize = sizeChanger(oldBrushSize)
|
||||||
|
store.setBrushSize(newBrushSize)
|
||||||
|
}
|
||||||
|
|
||||||
app.registerExtension({
|
app.registerExtension({
|
||||||
name: 'Comfy.MaskEditor',
|
name: 'Comfy.MaskEditor',
|
||||||
settings: [
|
settings: [
|
||||||
@@ -82,13 +91,24 @@ app.registerExtension({
|
|||||||
id: 'Comfy.MaskEditor.BrushSize.Increase',
|
id: 'Comfy.MaskEditor.BrushSize.Increase',
|
||||||
icon: 'pi pi-plus-circle',
|
icon: 'pi pi-plus-circle',
|
||||||
label: 'Increase Brush Size in MaskEditor',
|
label: 'Increase Brush Size in MaskEditor',
|
||||||
function: () => changeBrushSize((old) => _.clamp(old + 4, 1, 100))
|
function: () => changeBrushSize((old) => _.clamp(old + 2, 1, 250))
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: 'Comfy.MaskEditor.BrushSize.Decrease',
|
id: 'Comfy.MaskEditor.BrushSize.Decrease',
|
||||||
icon: 'pi pi-minus-circle',
|
icon: 'pi pi-minus-circle',
|
||||||
label: 'Decrease Brush Size in MaskEditor',
|
label: 'Decrease Brush Size in MaskEditor',
|
||||||
function: () => changeBrushSize((old) => _.clamp(old - 4, 1, 100))
|
function: () => changeBrushSize((old) => _.clamp(old - 2, 1, 250))
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'Comfy.MaskEditor.ColorPicker',
|
||||||
|
icon: 'pi pi-palette',
|
||||||
|
label: 'Open Color Picker in MaskEditor',
|
||||||
|
function: () => {
|
||||||
|
if (!isOpened()) return
|
||||||
|
|
||||||
|
const store = useMaskEditorStore()
|
||||||
|
store.colorInput?.click()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
init() {
|
init() {
|
||||||
@@ -101,12 +121,3 @@ app.registerExtension({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const changeBrushSize = async (sizeChanger: (oldSize: number) => number) => {
|
|
||||||
if (!isOpened()) return
|
|
||||||
|
|
||||||
const store = useMaskEditorStore()
|
|
||||||
const oldBrushSize = store.brushSettings.size
|
|
||||||
const newBrushSize = sizeChanger(oldBrushSize)
|
|
||||||
store.setBrushSize(newBrushSize)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
|
|||||||
|
|
||||||
const tgpuRoot = ref<any>(null)
|
const tgpuRoot = ref<any>(null)
|
||||||
|
|
||||||
|
const colorInput = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
watch(maskCanvas, (canvas) => {
|
watch(maskCanvas, (canvas) => {
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
maskCtx.value = canvas.getContext('2d', { willReadFrequently: true })
|
maskCtx.value = canvas.getContext('2d', { willReadFrequently: true })
|
||||||
@@ -113,7 +115,7 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
function setBrushSize(size: number): void {
|
function setBrushSize(size: number): void {
|
||||||
brushSettings.value.size = _.clamp(size, 1, 500)
|
brushSettings.value.size = _.clamp(size, 1, 250)
|
||||||
}
|
}
|
||||||
|
|
||||||
function setBrushOpacity(opacity: number): void {
|
function setBrushOpacity(opacity: number): void {
|
||||||
@@ -252,6 +254,8 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
|
|||||||
|
|
||||||
tgpuRoot,
|
tgpuRoot,
|
||||||
|
|
||||||
|
colorInput,
|
||||||
|
|
||||||
setBrushSize,
|
setBrushSize,
|
||||||
setBrushOpacity,
|
setBrushOpacity,
|
||||||
setBrushHardness,
|
setBrushHardness,
|
||||||
|
|||||||
Reference in New Issue
Block a user