Colour picker, palettes, & other spraypainter stuff (#41943)

* 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>
This commit is contained in:
SabreML
2026-01-22 17:33:21 +00:00
committed by GitHub
parent 4eb55ded54
commit e9be97dee0
9 changed files with 139 additions and 14 deletions

View 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));
}
}

View 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>
@@ -18,9 +29,13 @@
<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>

View 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;
}
}

View 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;

View 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);
}
}

View 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>

View 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>

View 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
{

View 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.