From b6aaf692352eeca9cdbd651eac43e0ec3dd3c6b3 Mon Sep 17 00:00:00 2001 From: MelekTaus Moribundus Date: Sun, 28 Jun 2026 16:20:25 +0200 Subject: [PATCH] Add Shift brush controls to paint mode Hold Shift to momentarily enable the pipette (restores prior state on release) and Shift+Scroll to resize the brush. The orbit zoom ignores the wheel while Shift is held so it does not zoom and resize at once. The HUD brush-size slider follows live via a BrushSizeChanged event, and the brush preview now stays visible while Shift is held (drawn before the pipette early-return) so the new size is visible while scrubbing. Co-Authored-By: Claude Opus 4.8 --- Assets/Scripts/MeshPainter.cs | 87 +++++++++++++++++++++++-- Assets/Scripts/PaintHud.cs | 25 ++++++- Assets/Scripts/ThirdPersonOrbitInput.cs | 17 ++++- 3 files changed, 120 insertions(+), 9 deletions(-) diff --git a/Assets/Scripts/MeshPainter.cs b/Assets/Scripts/MeshPainter.cs index 80fae8d..0038e8c 100644 --- a/Assets/Scripts/MeshPainter.cs +++ b/Assets/Scripts/MeshPainter.cs @@ -1,9 +1,11 @@ +using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; #if ENABLE_INPUT_SYSTEM using UnityEngine.InputSystem; #endif +using Random = UnityEngine.Random; // System (for Action) also defines Random -> keep the UnityEngine one /// /// Live mesh painting in Play mode: paints the brush onto the mesh textures with the @@ -118,6 +120,15 @@ public partial class MeshPainter : MonoBehaviour [Range(1, 256)] public int brushSize = 16; + [Tooltip("Texels added/removed per mouse-wheel notch while Shift is held (paint mode brush resize).")] + public int brushSizeScrollStep = 4; + + /// + /// Raised after Shift+wheel changes so the in-game HUD can re-sync its + /// brush-size slider (the slider then re-clamps the value to its own configured range). + /// + public event Action BrushSizeChanged; + [Tooltip("Brush masks: the mask value (luminance x alpha) is the per-texel opacity " + "(gray = 50%). Empty = built-in circle (= classic round brush). Import grayscale " + "masks as Non-Color so midtones map 1:1; alpha-based masks (e.g. soft particles) work too.")] @@ -611,6 +622,10 @@ public partial class MeshPainter : MonoBehaviour return; } + // Shift modifiers: hold Shift for the pipette + Shift+wheel to resize the brush. Runs before + // the pipette/paint branches below so the same frame's pipette state reflects the held Shift. + HandleBrushModifiers(); + if (updateColliderEveryFrame && _skinned) { BakeColliderMesh(); @@ -627,9 +642,15 @@ public partial class MeshPainter : MonoBehaviour if (pressed && IsPointerOverUi()) pressed = false; + // Brush preview (size + mask) at the hover point - independent of painting AND of the + // pipette, so you always see the brush footprint: before pressing the button, while the + // momentary pipette holds Shift, and live as Shift+wheel resizes the brush. + if (previewMode != BrushPreviewMode.Off && _previewMat && paintCamera) + UpdatePreview(screenPos); + // Pipette (eyedropper): a fresh click samples ALL channel values at the hit point // and adopts them into the brush instead of painting. It takes over the click - // entirely (no painting, no brush preview ring while active). + // entirely (no painting), but the brush preview above stays visible. if (pipette) { EndStrokeIfActive(); // switching to the eyedropper ends any active stroke @@ -644,11 +665,6 @@ public partial class MeshPainter : MonoBehaviour } _pickHeld = false; - // Brush preview (size + mask) at the hover point - independent of painting, - // so you see the brush before pressing the button. - if (previewMode != BrushPreviewMode.Off && _previewMat && paintCamera) - UpdatePreview(screenPos); - if (!pressed) { EndStrokeIfActive(); // mouse released -> commit the stroke as one undo step @@ -865,6 +881,65 @@ public partial class MeshPainter : MonoBehaviour #endif } + // --- Shift modifiers (paint mode) --- + + private bool _shiftHeld; // edge tracking for the momentary pipette + private bool _pipetteBeforeShift; // pipette toggle state to restore when Shift is released + + /// + /// While Shift is held the brush enters pipette (eyedropper) mode and the mouse wheel resizes the + /// brush (the orbit camera ignores the wheel while Shift is held, so it does not also zoom). + /// Releasing Shift restores the pipette toggle's prior state. Called every paint-mode frame. + /// + private void HandleBrushModifiers() + { + bool shift = ShiftHeld(); + + // Momentary pipette: arm on the Shift press (remembering the toggle's state), force it on while + // held (re-arms after a one-shot pick), and restore the prior state on release. + if (shift && !_shiftHeld) _pipetteBeforeShift = pipette; + if (shift) pipette = true; + else if (_shiftHeld) pipette = _pipetteBeforeShift; + _shiftHeld = shift; + + if (!shift) return; + + float scroll = ReadScroll(); + if (Mathf.Approximately(scroll, 0f)) return; + + int step = Mathf.Max(1, brushSizeScrollStep); + int next = Mathf.Clamp(brushSize + (int)Mathf.Sign(scroll) * step, 1, 256); + if (next == brushSize) return; + + brushSize = next; + BrushSizeChanged?.Invoke(); + } + + private static bool ShiftHeld() + { +#if ENABLE_INPUT_SYSTEM + var kb = Keyboard.current; + if (kb != null) return kb.leftShiftKey.isPressed || kb.rightShiftKey.isPressed; +#endif +#if ENABLE_LEGACY_INPUT_MANAGER + return Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); +#else + return false; +#endif + } + + private static float ReadScroll() + { +#if ENABLE_INPUT_SYSTEM + if (Mouse.current != null) return Mouse.current.scroll.ReadValue().y; +#endif +#if ENABLE_LEGACY_INPUT_MANAGER + return Input.GetAxis("Mouse ScrollWheel"); +#else + return 0f; +#endif + } + /// True if the pointer is currently over an interactive UI element (raycast target). private static bool IsPointerOverUi() { diff --git a/Assets/Scripts/PaintHud.cs b/Assets/Scripts/PaintHud.cs index f35311b..6eb1fa6 100644 --- a/Assets/Scripts/PaintHud.cs +++ b/Assets/Scripts/PaintHud.cs @@ -148,13 +148,19 @@ public partial class PaintHud : MonoBehaviour { if (!newPainter || newPainter == painter || !_panel) return; - if (painter) painter.Picked -= SyncFromPainter; + if (painter) + { + painter.Picked -= SyncFromPainter; + painter.BrushSizeChanged -= SyncBrushSize; + } painter = newPainter; WirePanel(_panel.transform); // Re-sync every widget when the pipette adopts a sampled material (painter -> HUD). painter.Picked += SyncFromPainter; + // Re-sync the brush-size slider when Shift+wheel resizes the brush in-world (painter -> HUD). + painter.BrushSizeChanged += SyncBrushSize; // First bind this session: restore panel positions + brush values from disk (cross-session). // A later bind is an avatar respawn (scene switch) -> carry the in-session brush values onto the @@ -169,7 +175,11 @@ public partial class PaintHud : MonoBehaviour private void OnDestroy() { - if (painter) painter.Picked -= SyncFromPainter; + if (painter) + { + painter.Picked -= SyncFromPainter; + painter.BrushSizeChanged -= SyncBrushSize; + } } private void OnModeChanged(GameMode mode) @@ -309,6 +319,17 @@ public partial class PaintHud : MonoBehaviour RefreshMaskHighlights(); } + /// + /// Pushes only the brush size back into its slider (painter -> HUD), e.g. after Shift+wheel + /// resized the brush in-world. The slider clamps to its own range and its listener writes the + /// clamped value back to the painter, so the wheel and the slider stay in agreement. + /// + private void SyncBrushSize() + { + if (!_panel || !painter) return; + SetSlider(_panel.transform, SliderBrushSize, painter.brushSize); + } + private static void SetToggle(Transform panel, string name, bool value) { var toggle = Find(panel, name); diff --git a/Assets/Scripts/ThirdPersonOrbitInput.cs b/Assets/Scripts/ThirdPersonOrbitInput.cs index 8c275dc..514f5ea 100644 --- a/Assets/Scripts/ThirdPersonOrbitInput.cs +++ b/Assets/Scripts/ThirdPersonOrbitInput.cs @@ -133,7 +133,9 @@ public class ThirdPersonOrbitInput : MonoBehaviour _zoomInit = true; } - float scroll = ReadScroll(); + // Shift+wheel is reserved for the paint brush resize (MeshPainter), so the orbit camera + // ignores the wheel while Shift is held - otherwise it would zoom and resize at once. + float scroll = ShiftHeld() ? 0f : ReadScroll(); if (!Mathf.Approximately(scroll, 0f)) { // One wheel notch = one zoom step (sign only), independent of the @@ -199,6 +201,19 @@ public class ThirdPersonOrbitInput : MonoBehaviour #endif } + static bool ShiftHeld() + { +#if ENABLE_INPUT_SYSTEM + var kb = Keyboard.current; + if (kb != null) return kb.leftShiftKey.isPressed || kb.rightShiftKey.isPressed; +#endif +#if ENABLE_LEGACY_INPUT_MANAGER + return Input.GetKey(KeyCode.LeftShift) || Input.GetKey(KeyCode.RightShift); +#else + return false; +#endif + } + Vector2 ReadMouseDelta() { #if ENABLE_INPUT_SYSTEM