]> git.smokeofanarchy.ru Git - space-station-14.git/commitdiff
Colour picker, palettes, & other spraypainter stuff (#41943)
authorSabreML <57483089+SabreML@users.noreply.github.com>
Thu, 22 Jan 2026 17:33:21 +0000 (17:33 +0000)
committerGitHub <noreply@github.com>
Thu, 22 Jan 2026 17:33:21 +0000 (17:33 +0000)
* The stuff

* Valid check

* Spraypaintable decals don't actually seem to use `ZIndex`

* Don't need this

* datafield fix and button swap

---------

Co-authored-by: Janet Blackquill <uhhadd@gmail.com>
Content.Client/SprayPainter/UI/SprayPainterBoundUserInterface.cs
Content.Client/SprayPainter/UI/SprayPainterDecals.xaml
Content.Client/SprayPainter/UI/SprayPainterDecals.xaml.cs
Content.Client/SprayPainter/UI/SprayPainterWindow.xaml.cs
Content.Server/SprayPainter/SprayPainterSystem.cs
Content.Shared/SprayPainter/Components/SprayPainterComponent.cs
Content.Shared/SprayPainter/SharedSprayPainterSystem.cs
Content.Shared/SprayPainter/SprayPainterEvents.cs
Resources/Locale/en-US/spray-painter/spray-painter.ftl

index 701ec80bac87b8d453fe0b8c677ae066102d9470..4eb3a43d257593d0f99432c7e740ad3dba907dde 100644 (file)
@@ -30,6 +30,7 @@ public sealed class SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey)
             _window.OnDecalColorChanged += OnDecalColorChanged;
             _window.OnDecalAngleChanged += OnDecalAngleChanged;
             _window.OnDecalSnapChanged += OnDecalSnapChanged;
+            _window.OnDecalColorPickerToggled += OnDecalColorPickerToggled;
         }
 
         var sprayPainter = EntMan.System<SprayPainterSystem>();
@@ -56,6 +57,7 @@ public sealed class SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey)
         _window.SetDecalAngle(sprayPainter.SelectedDecalAngle);
         _window.SetDecalColor(sprayPainter.SelectedDecalColor);
         _window.SetDecalSnap(sprayPainter.SnapDecals);
+        _window.SetDecalColorPicker(sprayPainter.ColorPickerEnabled);
     }
 
     private void OnDecalSnapChanged(bool snap)
@@ -93,4 +95,9 @@ public sealed class SprayPainterBoundUserInterface(EntityUid owner, Enum uiKey)
         var key = _window?.IndexToColorKey(args.ItemIndex);
         SendPredictedMessage(new SprayPainterSetPipeColorMessage(key));
     }
+
+    private void OnDecalColorPickerToggled(bool toggle)
+    {
+        SendPredictedMessage(new SprayPainterSetDecalColorPickerMessage(toggle));
+    }
 }
index 0d5c8e4f167f899f60e58e52a3925be9c5946c55..ef4379e6cb1b39300e4aa018feabe1a926d73c00 100644 (file)
@@ -2,15 +2,26 @@
     xmlns="https://spacestation14.io"
     xmlns:controls="clr-namespace:Content.Client.SprayPainter.UI">
     <BoxContainer Orientation="Vertical">
-        <Label Text="{Loc 'spray-painter-selected-decals'}" />
+        <BoxContainer>
+            <Label Text="{Loc 'spray-painter-selected-decals'}" />
+            <Label Name="SelectedDecalName" StyleClasses="Italic" Margin="4 0" />
+        </BoxContainer>
         <ScrollContainer VerticalExpand="True">
             <GridContainer Columns="7" Name="DecalsGrid">
                 <!-- populated by code -->
             </GridContainer>
         </ScrollContainer>
 
+        <PanelContainer StyleClasses="LowDivider" />
         <BoxContainer Orientation="Vertical">
-            <ColorSelectorSliders Name="ColorSelector" IsAlphaVisible="True" />
+            <ColorSelectorSliders Name="ColorSelector" IsAlphaVisible="True">
+                <BoxContainer HorizontalAlignment="Right" VerticalAlignment="Top" SetHeight="29">
+                    <Button Name="ColorPalette" Text="Palette" />
+                    <Button Name="ColorPicker" ToggleMode="True">
+                        <TextureRect TexturePath="/Textures/Interface/eyedropper.svg.png" Stretch="KeepAspectCentered" SetSize="16 16" />
+                    </Button>
+                </BoxContainer>
+            </ColorSelectorSliders>
             <CheckBox Name="UseCustomColorCheckBox" Text="{Loc 'spray-painter-use-custom-color'}" />
             <CheckBox Name="SnapToTileCheckBox" Text="{Loc 'spray-painter-use-snap-to-tile'}" />
         </BoxContainer>
         <BoxContainer Orientation="Horizontal">
             <Label Text="{Loc 'spray-painter-angle-rotation'}" />
             <SpinBox Name="AngleSpinBox" HorizontalExpand="True" />
-            <Button Text="{Loc 'spray-painter-angle-rotation-90-sub'}" Name="SubAngleButton" />
+            <Button Name="AddAngleButton" ToolTip="{Loc 'spray-painter-angle-rotation-90-add'}" SetSize="48 32">
+                <TextureRect TexturePath="/Textures/Interface/VerbIcons/rotate_ccw.svg.192dpi.png" Stretch="KeepAspectCentered" SetSize="32 32" />
+            </Button>
             <Button Text="{Loc 'spray-painter-angle-rotation-reset'}" Name="SetZeroAngleButton" />
-            <Button Text="{Loc 'spray-painter-angle-rotation-90-add'}" Name="AddAngleButton" />
+            <Button Name="SubAngleButton" ToolTip="{Loc 'spray-painter-angle-rotation-90-sub'}" SetSize="48 32">
+                <TextureRect TexturePath="/Textures/Interface/VerbIcons/rotate_cw.svg.192dpi.png" Stretch="KeepAspectCentered" SetSize="32 32" />
+            </Button>
         </BoxContainer>
     </BoxContainer>
 </controls:SprayPainterDecals>
index 64d1f78d3ce779473fce13638059ab1abe355afa..66530eba91715189bd280fe786aabce126ec770d 100644 (file)
@@ -1,4 +1,4 @@
-using System.Numerics;
+using Content.Client.Decals.UI;
 using Content.Client.Stylesheets;
 using Content.Shared.Decals;
 using Robust.Client.AutoGenerated;
@@ -8,6 +8,8 @@ using Robust.Client.UserInterface;
 using Robust.Client.UserInterface.Controls;
 using Robust.Client.UserInterface.XAML;
 using Robust.Shared.Prototypes;
+using System.Linq;
+using System.Numerics;
 
 namespace Content.Client.SprayPainter.UI;
 
@@ -21,6 +23,9 @@ public sealed partial class SprayPainterDecals : Control
     public Action<Color?>? OnColorChanged;
     public Action<int>? OnAngleChanged;
     public Action<bool>? OnSnapChanged;
+    public Action<bool>? OnColorPickerToggled;
+
+    private PaletteColorPicker? _palette;
 
     private SpriteSystem? _sprite;
     private string _selectedDecal = string.Empty;
@@ -30,14 +35,17 @@ public sealed partial class SprayPainterDecals : Control
     {
         RobustXamlLoader.Load(this);
 
-        AddAngleButton.OnButtonUp += _ => AngleSpinBox.Value += 90;
-        SubAngleButton.OnButtonUp += _ => AngleSpinBox.Value -= 90;
+        AddAngleButton.OnButtonUp += _ => AngleSpinBox.Value = (AngleSpinBox.Value + 90) % 360;
+        SubAngleButton.OnButtonUp += _ => AngleSpinBox.Value = (AngleSpinBox.Value - 90) % 360;
         SetZeroAngleButton.OnButtonUp += _ => AngleSpinBox.Value = 0;
         AngleSpinBox.ValueChanged += args => OnAngleChanged?.Invoke(args.Value);
 
         UseCustomColorCheckBox.OnPressed += UseCustomColorCheckBoxOnOnPressed;
         SnapToTileCheckBox.OnPressed += SnapToTileCheckBoxOnOnPressed;
         ColorSelector.OnColorChanged += OnColorSelected;
+
+        ColorPalette.OnPressed += ColorPaletteOnPressed;
+        ColorPicker.OnPressed += args => OnColorPickerToggled?.Invoke(args.Button.Pressed);
     }
 
     private void UseCustomColorCheckBoxOnOnPressed(BaseButton.ButtonEventArgs _)
@@ -147,6 +155,7 @@ public sealed partial class SprayPainterDecals : Control
     public void SetSelectedDecal(string name)
     {
         _selectedDecal = name;
+        SelectedDecalName.Text = name;
 
         if (_sprite is null)
             return;
@@ -171,4 +180,35 @@ public sealed partial class SprayPainterDecals : Control
     {
         SnapToTileCheckBox.Pressed = snap;
     }
+
+    private void ColorPaletteOnPressed(BaseButton.ButtonEventArgs _)
+    {
+        // Code copied from other implementations of `PaletteColorPicker`.
+        if (_palette is null)
+        {
+            _palette = new PaletteColorPicker();
+            _palette.OpenCenteredLeft();
+            _palette.PaletteList.OnItemSelected += args =>
+            {
+                var color = (args.ItemList.GetSelected().First().Metadata as Color?)!.Value;
+                ColorSelector.Color = color;
+                OnColorSelected(color);
+            };
+            return;
+        }
+
+        if (_palette.IsOpen)
+        {
+            _palette.Close();
+        }
+        else
+        {
+            _palette.Open();
+        }
+    }
+
+    public void SetColorPicker(bool enabled)
+    {
+        ColorPicker.Pressed = enabled;
+    }
 }
index eb1218ad6784398744647d0b903b60bd36915c56..2f727960438edb9aa9948c5da7e968f8843df060 100644 (file)
@@ -30,6 +30,7 @@ public sealed partial class SprayPainterWindow : DefaultWindow
     public event Action<Color?>? OnDecalColorChanged;
     public event Action<int>? OnDecalAngleChanged;
     public event Action<bool>? OnDecalSnapChanged;
+    public event Action<bool>? OnDecalColorPickerToggled;
 
     // Pipe color data
     private ItemList _colorList = default!;
@@ -195,6 +196,7 @@ public sealed partial class SprayPainterWindow : DefaultWindow
                 _sprayPainterDecals.OnColorChanged += color => OnDecalColorChanged?.Invoke(color);
                 _sprayPainterDecals.OnAngleChanged += angle => OnDecalAngleChanged?.Invoke(angle);
                 _sprayPainterDecals.OnSnapChanged += snap => OnDecalSnapChanged?.Invoke(snap);
+                _sprayPainterDecals.OnColorPickerToggled += toggle => OnDecalColorPickerToggled?.Invoke(toggle);
 
                 Tabs.AddChild(_sprayPainterDecals);
                 TabContainer.SetTabTitle(_sprayPainterDecals, Loc.GetString("spray-painter-tab-category-decals"));
@@ -298,7 +300,12 @@ public sealed partial class SprayPainterWindow : DefaultWindow
         if (_sprayPainterDecals != null)
             _sprayPainterDecals.SetSnap(snap);
     }
-    # endregion
+
+    public void SetDecalColorPicker(bool colorPickerEnabled)
+    {
+        _sprayPainterDecals?.SetColorPicker(colorPickerEnabled);
+    }
+    #endregion
 }
 
 public record SpriteListData(string Group, string Style, EntProtoId Prototype, int SelectedIndex) : ListData;
index f00ae1d7dd7103ba7320a790f1ac774709edcb00..a4c631db41138373dfa7e331491e2f9cffe6b68c 100644 (file)
@@ -15,7 +15,8 @@ using Content.Shared.SprayPainter;
 using Content.Shared.SprayPainter.Components;
 using Robust.Server.Audio;
 using Robust.Server.GameObjects;
-using Robust.Shared.Prototypes;
+using System.Linq;
+using System.Numerics;
 
 namespace Content.Server.SprayPainter;
 
@@ -48,7 +49,16 @@ public sealed class SprayPainterSystem : SharedSprayPainterSystem
     /// </summary>
     private void OnFloorAfterInteract(Entity<SprayPainterComponent> ent, ref AfterInteractEvent args)
     {
-        if (args.Handled || !args.CanReach || args.Target != null)
+        if (args.Handled || args.Target != null)
+            return;
+
+        if (ent.Comp.ColorPickerEnabled)
+        {
+            PickColor(ent, ref args);
+            return;
+        }
+
+        if (!args.CanReach)
             return;
 
         // Includes both off and all other don't cares
@@ -83,7 +93,7 @@ public sealed class SprayPainterSystem : SharedSprayPainterSystem
                 return;
             }
 
-            var decals = _decals.GetDecalsInRange(grid, position.Position, validDelegate: IsDecalRemovable);
+            var decals = _decals.GetDecalsInRange(grid, position.Position, validDelegate: IsDecalValid);
             if (decals.Count <= 0)
             {
                 _popup.PopupEntity(Loc.GetString("spray-painter-interact-nothing-to-remove"), args.User, args.User);
@@ -104,10 +114,9 @@ public sealed class SprayPainterSystem : SharedSprayPainterSystem
     }
 
     /// <summary>
-    /// Handles drawing decals when a spray painter is used to interact with the floor.
-    /// Spray painter must have decal painting enabled and enough charges of paint to paint on the floor.
+    /// Returns whether <paramref name="decal"/> is valid to interact with when a spray painter is used to interact with the floor.
     /// </summary>
-    private bool IsDecalRemovable(Decal decal)
+    private bool IsDecalValid(Decal decal)
     {
         if (!Proto.TryIndex<DecalPrototype>(decal.Id, out var decalProto))
             return false;
@@ -189,4 +198,26 @@ public sealed class SprayPainterSystem : SharedSprayPainterSystem
 
         args.Handled = DoAfter.TryStartDoAfter(doAfterEventArgs);
     }
+
+    private void PickColor(Entity<SprayPainterComponent> ent, ref AfterInteractEvent args)
+    {
+        if (!args.ClickLocation.IsValid(EntityManager) || _transform.GetGrid(args.ClickLocation) is not { } grid)
+            return;
+
+        var clickPos = args.ClickLocation.Position;
+        var decals = _decals.GetDecalsInRange(grid, clickPos, validDelegate: IsDecalValid);
+        if (decals.Count == 0)
+        {
+            _popup.PopupEntity(Loc.GetString("spray-painter-interact-no-color-pick"), args.User, args.User);
+            return;
+        }
+
+        var closestDecal = decals.MinBy(d => Vector2.Distance(d.Decal.Coordinates, clickPos)).Decal;
+
+        _popup.PopupEntity(Loc.GetString("spray-painter-interact-color-picked", ("id", closestDecal.Id)), args.User, args.User);
+
+        ent.Comp.SelectedDecalColor = closestDecal.Color;
+        ent.Comp.ColorPickerEnabled = false;
+        Dirty(ent);
+    }
 }
index b9a7057347de440e2c692a5b9298eaa2bc2bbec8..6b3e36bef21296dcdeb9aaeb93d4a446a7bb080e 100644 (file)
@@ -105,6 +105,12 @@ public sealed partial class SprayPainterComponent : Component
     /// </summary>
     [DataField]
     public SoundSpecifier SoundSwitchDecalMode = new SoundPathSpecifier("/Audio/Machines/quickbeep.ogg", AudioParams.Default.WithVolume(1.5f));
+
+    /// <summary>
+    /// Whether the decal color picker is currently active.
+    /// </summary>
+    [DataField, AutoNetworkedField]
+    public bool ColorPickerEnabled = false;
 }
 
 /// <summary>
index d1f19d0c25c279ca9cace46cccb6e7aa7ec4e99f..a3eb26a892d09862afcef8074963d1c9a71b4f67 100644 (file)
@@ -53,6 +53,7 @@ public abstract class SharedSprayPainterSystem : EntitySystem
                 subs.Event<SprayPainterSetDecalColorMessage>(OnSetDecalColor);
                 subs.Event<SprayPainterSetDecalAngleMessage>(OnSetDecalAngle);
                 subs.Event<SprayPainterSetDecalSnapMessage>(OnSetDecalSnap);
+                subs.Event<SprayPainterSetDecalColorPickerMessage>(OnSetDecalColorPicker);
             });
     }
 
@@ -300,6 +301,16 @@ public abstract class SharedSprayPainterSystem : EntitySystem
         UpdateUi(ent);
     }
 
+    /// <summary>
+    /// Enables or disables the decal colour picker.
+    /// </summary>
+    private void OnSetDecalColorPicker(Entity<SprayPainterComponent> ent, ref SprayPainterSetDecalColorPickerMessage args)
+    {
+        ent.Comp.ColorPickerEnabled = args.Toggle;
+        Dirty(ent);
+        UpdateUi(ent);
+    }
+
     /// <summary>
     /// Sets the decal to paint on the ground.
     /// </summary>
index db9de9c27879f6fe3429244b9dee78dafdacc984..806cfd288880296d1fa2fd3bc1875f98f8da60ba 100644 (file)
@@ -56,6 +56,12 @@ public sealed class SprayPainterSetPipeColorMessage(string? key) : BoundUserInte
     public readonly string? Key = key;
 }
 
+[Serializable, NetSerializable]
+public sealed class SprayPainterSetDecalColorPickerMessage(bool toggle) : BoundUserInterfaceMessage
+{
+    public bool Toggle = toggle;
+}
+
 [Serializable, NetSerializable]
 public sealed partial class SprayPainterDoAfterEvent : DoAfterEvent
 {
index dc54c5c8b8c06777cdf066304bd95167937404a4..18c8c36f909de2ff3a98e96697b8a7eb1bbebddf 100644 (file)
@@ -5,6 +5,8 @@ spray-painter-ammo-after-interact-refilled = You refill the spray painter.
 
 spray-painter-interact-no-charges = Not enough paint left.
 spray-painter-interact-nothing-to-remove = Nothing to remove!
+spray-painter-interact-no-color-pick = Can't find a color to pick!
+spray-painter-interact-color-picked = Picked color from '{$id}'.
 
 spray-painter-on-examined-painted-message = It seems to have been freshly painted.
 spray-painter-style-not-available = Cannot apply the selected style to this object.