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 <noreply@anthropic.com>
This commit is contained in:
adminandClaude Opus 4.8 committed 2026-06-28 16:20:25 +02:00
1 parent 4e27b1e729
commit b6aaf69235
3 files changed
+120 -9

No files matched your search

+81 -6
View File
@@ -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
/// <summary>
/// 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;
/// <summary>
/// Raised after Shift+wheel changes <see cref="brushSize"/> so the in-game HUD can re-sync its
/// brush-size slider (the slider then re-clamps the value to its own configured range).
/// </summary>
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
/// <summary>
/// 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.
/// </summary>
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
}
/// <summary>True if the pointer is currently over an interactive UI element (raycast target).</summary>
private static bool IsPointerOverUi()
{
+23 -2
View File
@@ -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();
}
/// <summary>
/// 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.
/// </summary>
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<Toggle>(panel, name);
+16 -1
View File
@@ -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