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:
1 parent
4e27b1e729
commit
b6aaf69235
3 files changed
+120
-9
No files matched your search
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user