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:
brucew4yn3rp
2026-01-03 10:33:57 -05:00
committed by GitHub
parent 664aafb705
commit 825ec722c3
4 changed files with 196 additions and 91 deletions

View File

@@ -1,8 +1,6 @@
<template>
<div class="flex flex-col gap-3 pb-3">
<h3
class="text-center text-[15px] font-sans text-[var(--descrip-text)] mt-2.5"
>
<h3 class="text-center text-[15px] font-sans text-descrip-text mt-2.5">
{{ t('maskEditor.brushSettings') }}
</h3>
@@ -10,120 +8,211 @@
{{ t('maskEditor.resetToDefault') }}
</button>
<!-- Brush Shape -->
<div class="flex flex-col gap-3 pb-3">
<span class="text-left text-xs font-sans text-[var(--descrip-text)]">{{
t('maskEditor.brushShape')
}}</span>
<span class="text-left text-xs font-sans text-descrip-text">
{{ t('maskEditor.brushShape') }}
</span>
<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
class="maskEditor_sidePanelBrushShapeCircle bg-transparent hover:bg-comfy-menu-bg"
:class="{ active: store.brushSettings.type === BrushShape.Arc }"
:style="{
background:
class="maskEditor_sidePanelBrushShapeCircle hover:bg-comfy-menu-bg"
:class="
cn(
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)"
></div>
<div
class="maskEditor_sidePanelBrushShapeSquare bg-transparent hover:bg-comfy-menu-bg"
:class="{ active: store.brushSettings.type === BrushShape.Rect }"
:style="{
background:
class="maskEditor_sidePanelBrushShapeSquare hover:bg-comfy-menu-bg"
:class="
cn(
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)"
></div>
</div>
</div>
<!-- Color -->
<div class="flex flex-col gap-3 pb-3">
<span class="text-left text-xs font-sans text-[var(--descrip-text)]">{{
t('maskEditor.colorSelector')
}}</span>
<input type="color" :value="store.rgbColor" @input="onColorChange" />
<span class="text-left text-xs font-sans text-descrip-text">
{{ t('maskEditor.colorSelector') }}
</span>
<input
ref="colorInputRef"
v-model="store.rgbColor"
type="color"
class="h-10 rounded-md cursor-pointer"
/>
</div>
<SliderControl
:label="t('maskEditor.thickness')"
:min="1"
:max="500"
:step="1"
:model-value="store.brushSettings.size"
@update:model-value="onThicknessChange"
/>
<!-- Thickness -->
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-left text-xs font-sans text-descrip-text">
{{ t('maskEditor.thickness') }}
</span>
<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
:label="t('maskEditor.opacity')"
:min="0"
:max="1"
:step="0.01"
:model-value="store.brushSettings.opacity"
@update:model-value="onOpacityChange"
/>
<!-- Opacity -->
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-left text-xs font-sans text-descrip-text">
{{ t('maskEditor.opacity') }}
</span>
<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
:label="t('maskEditor.hardness')"
:min="0"
:max="1"
:step="0.01"
:model-value="store.brushSettings.hardness"
@update:model-value="onHardnessChange"
/>
<!-- Hardness -->
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-left text-xs font-sans text-descrip-text">
{{ t('maskEditor.hardness') }}
</span>
<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
:label="$t('maskEditor.stepSize')"
:min="1"
:max="100"
:step="1"
:model-value="store.brushSettings.stepSize"
@update:model-value="onStepSizeChange"
/>
<!-- Step Size -->
<div class="flex flex-col gap-2">
<div class="flex items-center justify-between">
<span class="text-left text-xs font-sans text-descrip-text">
{{ t('maskEditor.stepSize') }}
</span>
<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>
</template>
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { BrushShape } from '@/extensions/core/maskeditor/types'
import { t } from '@/i18n'
import { useMaskEditorStore } from '@/stores/maskEditorStore'
import { cn } from '@/utils/tailwindUtil'
import SliderControl from './controls/SliderControl.vue'
const store = useMaskEditorStore()
const textButtonClass =
'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 colorInputRef = ref<HTMLInputElement>()
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) => {
store.brushSettings.type = shape
}
const onColorChange = (event: Event) => {
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)
}
/* Reset */
const resetToDefault = () => {
store.resetBrushToDefault()
}
onMounted(() => {
if (colorInputRef.value) {
store.colorInput = colorInputRef.value
}
})
onBeforeUnmount(() => {
store.colorInput = null
})
</script>

View File

@@ -202,6 +202,7 @@ onBeforeUnmount(() => {
}
store.canvasHistory.clearStates()
store.resetState()
dataStore.reset()
})

View File

@@ -37,6 +37,15 @@ function isOpened(): boolean {
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({
name: 'Comfy.MaskEditor',
settings: [
@@ -82,13 +91,24 @@ app.registerExtension({
id: 'Comfy.MaskEditor.BrushSize.Increase',
icon: 'pi pi-plus-circle',
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',
icon: 'pi pi-minus-circle',
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() {
@@ -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)
}

View File

@@ -73,6 +73,8 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
const tgpuRoot = ref<any>(null)
const colorInput = ref<HTMLInputElement | null>(null)
watch(maskCanvas, (canvas) => {
if (canvas) {
maskCtx.value = canvas.getContext('2d', { willReadFrequently: true })
@@ -113,7 +115,7 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
})
function setBrushSize(size: number): void {
brushSettings.value.size = _.clamp(size, 1, 500)
brushSettings.value.size = _.clamp(size, 1, 250)
}
function setBrushOpacity(opacity: number): void {
@@ -252,6 +254,8 @@ export const useMaskEditorStore = defineStore('maskEditor', () => {
tgpuRoot,
colorInput,
setBrushSize,
setBrushOpacity,
setBrushHardness,