Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
217 changes: 216 additions & 1 deletion Content.Client/Decals/DecalPlacementSystem.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,14 @@
using Content.Client.Decals.Overlays;
using Content.Shared.Actions;
using Content.Shared.Decals;
using Content.Shared.Input; // Exodus
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
using Robust.Shared.Input;
using Robust.Shared.Input.Binding;
using Robust.Shared.Map; // Exodus
using Robust.Shared.Player; // Exodus
using Robust.Shared.Prototypes;

namespace Content.Client.Decals;
Expand All @@ -17,6 +20,7 @@ namespace Content.Client.Decals;
public sealed partial class DecalPlacementSystem : EntitySystem
{
[Dependency] private IInputManager _inputManager = default!;
[Dependency] private IMapManager _mapManager = default!; // Exodus
[Dependency] private IOverlayManager _overlay = default!;
[Dependency] private IPrototypeManager _protoMan = default!;
[Dependency] private InputSystem _inputSystem = default!;
Expand All @@ -35,6 +39,48 @@ public sealed partial class DecalPlacementSystem : EntitySystem
private bool _placing;
private bool _erasing;

// Exodus-Start
private bool _eyedropper;

/// <summary>
/// Whether the eyedropper is currently selected.
/// </summary>
public bool EyedropperActive => _eyedropper;

/// <summary>
/// Raised when the eyedropper successfully copies a color from a decal.
/// </summary>
public event Action<Color>? EyedropperPicked;

public void SetEyedropper(bool active)
{
_eyedropper = active && _active;

// The eyedropper and a paste (in copypaste mode) are mutually exclusive.
if (_eyedropper)
{
_stamping = false;
_stamp.Clear();
}
}
Comment thread
TryHardo7 marked this conversation as resolved.

// Copy decal(s) under the cursor, only while the window is open.
private readonly List<(Vector2 Offset, Decal Decal)> _stamp = new();
private bool _stamping;

/// <summary>
/// Whether a multi-decal stamp is currently held and ready to be placed.
/// </summary>
public bool Stamping => _stamping && _stamp.Count > 0;

public IReadOnlyList<(Vector2 Offset, Decal Decal)> Stamp => _stamp;

/// <summary>
/// Raised when a single decal is copied from the map, so the window can mirror its settings.
/// </summary>
public event Action<Decal>? DecalCopied;
// Exodus-End

public (DecalPrototype? Decal, bool Snap, Angle Angle, Color Color) GetActiveDecal()
{
return _active && _decalId != null ?
Expand All @@ -50,6 +96,23 @@ public override void Initialize()
CommandBinds.Builder.Bind(EngineKeyFunctions.EditorPlaceObject, new PointerStateInputCmdHandler(
(session, coords, uid) =>
{
// Exodus-Start: left click while the eyedropper is active copies a color.
if (_eyedropper)
{
_eyedropper = false;
Comment thread
TryHardo7 marked this conversation as resolved.

if (TryPickDecalColor(coords, out var picked))
EyedropperPicked?.Invoke(picked);

return true;
}
if (_stamping)
{
PlaceStamp(coords);
return true;
}
// Exodus-End

if (!_active || _placing || _decalId == null)
return false;

Expand Down Expand Up @@ -85,6 +148,21 @@ public override void Initialize()
.Bind(EngineKeyFunctions.EditorCancelPlace, new PointerStateInputCmdHandler(
(session, coords, uid) =>
{
// Exodus-Start: right click cancels the eyedropper or a held stamp instead of erasing.
if (_eyedropper)
{
_eyedropper = false;
Comment thread
TryHardo7 marked this conversation as resolved.
return true;
}

if (_stamping)
{
_stamping = false;
_stamp.Clear();
return true;
}
// Exodus-End

if (!_active || _erasing)
return false;

Expand All @@ -100,7 +178,12 @@ public override void Initialize()
_erasing = false;

return true;
}, true)).Register<DecalPlacementSystem>();
}, true))
// Exodus-Start: copy the decal (O - not 0) or the whole stack (Ctrl+O) under the cursor.
.Bind(ContentKeyFunctions.EditorCopyDecal, new PointerInputCmdHandler(OnCopyDecal))
.Bind(ContentKeyFunctions.EditorCopyDecalStack, new PointerInputCmdHandler(OnCopyDecalStack))
// Exodus-End
.Register<DecalPlacementSystem>();

SubscribeLocalEvent<FillActionSlotEvent>(OnFillSlot);
SubscribeLocalEvent<PlaceDecalActionEvent>(OnPlaceDecalAction);
Expand Down Expand Up @@ -189,12 +272,144 @@ public void UpdateDecalInfo(string id, Color color, float rotation, bool snap, i
_cleanable = cleanable;
}

// Exodus-Start: clear the active decal. This wast here for years, really?!
public void ClearDecal()
{
_decalId = null;
}
// Exodus-End

public void SetActive(bool active)
{
_active = active;
// Exodus-Start: arming/holding a tool is always an explicit user action.
_eyedropper = false;
_stamping = false;
_stamp.Clear();
// Exodus-End
if (_active)
_inputManager.Contexts.SetActiveContext("editor");
else
_inputSystem.SetEntityContextActive();
}

// Exodus-Start
/// <summary>"O": copy the top decal under the cursor into the placer.</summary>
private bool OnCopyDecal(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
if (!_active)
return false;

_eyedropper = false;
_stamping = false;
_stamp.Clear();

if (TryGetDecalsUnder(coords, out _, out var decals) && Topmost(decals) is { } top)
DecalCopied?.Invoke(top.Decal);

return true;
}

/// <summary>"Ctrl+O": copy every decal under the cursor as a stamp to place as a group.</summary>
private bool OnCopyDecalStack(ICommonSession? session, EntityCoordinates coords, EntityUid uid)
{
if (!_active)
return false;

_eyedropper = false;
_stamp.Clear();
_stamping = false;

if (!TryGetDecalsUnder(coords, out var localPos, out var decals))
return true;

// So stamp stays tile-aligned when placed.
var origin = localPos.Floored();
foreach (var (_, decal) in decals)
_stamp.Add((decal.Coordinates - origin, decal));

_stamping = true;
return true;
}

private void PlaceStamp(EntityCoordinates coords)
{
if (!TryGetGridLocal(coords, out var gridUid, out var localPos))
return;

var origin = localPos.Floored();
foreach (var (offset, decal) in _stamp)
{
var newPos = origin + offset;
var target = new EntityCoordinates(gridUid, newPos);
if (!target.IsValid(EntityManager))
continue;

var copy = new Decal(newPos, decal.Id, decal.Color, decal.Angle, decal.ZIndex, decal.Cleanable);
RaiseNetworkEvent(new RequestDecalPlacementEvent(copy, GetNetCoordinates(target)));
}
}

private bool TryPickDecalColor(EntityCoordinates coords, out Color color)
{
color = Color.White;
if (!TryGetDecalsUnder(coords, out _, out var decals) || Topmost(decals) is not { } top)
return false;

color = top.Decal.Color ?? Color.White;
return true;
}

/// <summary>Resolve the grid and grid-local position under the given coordinates.</summary>
private bool TryGetGridLocal(EntityCoordinates coords, out EntityUid gridUid, out Vector2 localPos)
{
localPos = default;
var mapPos = _transform.ToMapCoordinates(coords);
if (!_mapManager.TryFindGridAt(mapPos, out gridUid, out _))
return false;

localPos = Vector2.Transform(mapPos.Position, _transform.GetInvWorldMatrix(gridUid));
return true;
}

/// <summary>Collect every decal whose 1x1 footprint contains the cursor.</summary>
private bool TryGetDecalsUnder(EntityCoordinates coords, out Vector2 localPos, out List<(uint Index, Decal Decal)> decals)
{
decals = new();
if (!TryGetGridLocal(coords, out var gridUid, out localPos)
|| !TryComp<DecalGridComponent>(gridUid, out var decalGrid))
return false;

var chunkIndices = SharedDecalSystem.GetChunkIndices(localPos);
if (!decalGrid.ChunkCollection.ChunkCollection.TryGetValue(chunkIndices, out var chunk))
return false;

foreach (var (id, decal) in chunk.Decals)
{
// Decals are drawn as a 1x1 tile with their bottom-left corner at Coordinates.
if (localPos.X < decal.Coordinates.X || localPos.X >= decal.Coordinates.X + 1f ||
localPos.Y < decal.Coordinates.Y || localPos.Y >= decal.Coordinates.Y + 1f)
continue;

decals.Add((id, decal));
}

return decals.Count > 0;
}

/// <summary>Pick the decal drawn on top: highest ZIndex, then highest id (matches the overlay).</summary>
private static (uint Index, Decal Decal)? Topmost(List<(uint Index, Decal Decal)> decals)
{
(uint Index, Decal Decal)? best = null;
foreach (var entry in decals)
{
if (best is not { } b
|| entry.Decal.ZIndex > b.Decal.ZIndex
|| (entry.Decal.ZIndex == b.Decal.ZIndex && entry.Index > b.Index))
best = entry;
}

return best;
}
// Exodus-End
}
82 changes: 82 additions & 0 deletions Content.Client/Decals/Overlays/DecalPlacementOverlay.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Numerics;
using Content.Shared.Decals; // Exodus
using Robust.Client.GameObjects;
using Robust.Client.Graphics;
using Robust.Client.Input;
Expand All @@ -13,6 +14,7 @@ public sealed partial class DecalPlacementOverlay : Overlay
[Dependency] private IEyeManager _eyeManager = default!;
[Dependency] private IInputManager _inputManager = default!;
[Dependency] private IMapManager _mapManager = default!;
[Dependency] private IPrototypeManager _protoMan = default!; // Exodus
private readonly DecalPlacementSystem _placement;
private readonly SharedTransformSystem _transform;
private readonly SpriteSystem _sprite;
Expand All @@ -30,6 +32,20 @@ public DecalPlacementOverlay(DecalPlacementSystem placement, SharedTransformSyst

protected override void Draw(in OverlayDrawArgs args)
{
// Exodus-Start: while a tool is active, draw its preview instead of the single-decal preview.
if (_placement.EyedropperActive)
{
DrawEyedropper(args);
return;
}

if (_placement.Stamping)
{
DrawStamp(args);
return;
}
// Exodus-End

var (decal, snap, rotation, color) = _placement.GetActiveDecal();

if (decal == null)
Expand Down Expand Up @@ -67,4 +83,70 @@ protected override void Draw(in OverlayDrawArgs args)
handle.DrawTextureRect(_sprite.Frame0(decal.Sprite), box, color);
handle.SetTransform(Matrix3x2.Identity);
}

// Exodus-Start: preview of the copied decal stack, following the cursor before it is stamped.
private void DrawStamp(in OverlayDrawArgs args)
{
var mouseScreenPos = _inputManager.MouseScreenPosition;
var mousePos = _eyeManager.PixelToMap(mouseScreenPos);

if (mousePos.MapId != args.MapId)
return;

if (!_mapManager.TryFindGridAt(mousePos, out var gridUid, out _))
return;

var handle = args.WorldHandle;
handle.SetTransform(_transform.GetWorldMatrix(gridUid));

var localPos = Vector2.Transform(mousePos.Position, _transform.GetInvWorldMatrix(gridUid));
var origin = localPos.Floored();

foreach (var (offset, decal) in _placement.Stamp)
{
if (!_protoMan.TryIndex<DecalPrototype>(decal.Id, out var proto))
continue;

var texture = _sprite.Frame0(proto.Sprite);
var pos = origin + offset;
var color = decal.Color ?? Color.White;

if (decal.Angle == Angle.Zero)
handle.DrawTexture(texture, pos, color);
else
handle.DrawTexture(texture, pos, decal.Angle, color);
}

handle.SetTransform(Matrix3x2.Identity);
}

// crosshair shown at the cursor marking the sample point of the eyedropper tool.
private void DrawEyedropper(in OverlayDrawArgs args)
{
var mouseScreenPos = _inputManager.MouseScreenPosition;
var mousePos = _eyeManager.PixelToMap(mouseScreenPos);

if (mousePos.MapId != args.MapId)
return;

// The handle is already in world space here (we never set a transform), so draw directly.
var handle = args.WorldHandle;
var pos = mousePos.Position;
const float arm = 0.35f;
const float gap = 0.08f;

// Draw a dark outline first so the crosshair stays visible over any decal color.
DrawCross(handle, pos, arm, gap, 0.04f, Color.Black.WithAlpha(0.6f));
DrawCross(handle, pos, arm, gap, 0f, Color.White);
handle.DrawCircle(pos, gap, Color.White, false);
}

private static void DrawCross(DrawingHandleWorld handle, Vector2 pos, float arm, float gap, float pad, Color color)
{
handle.DrawLine(pos + new Vector2(-arm - pad, 0f), pos + new Vector2(-gap + pad, 0f), color);
handle.DrawLine(pos + new Vector2(gap - pad, 0f), pos + new Vector2(arm + pad, 0f), color);
handle.DrawLine(pos + new Vector2(0f, -arm - pad), pos + new Vector2(0f, -gap + pad), color);
handle.DrawLine(pos + new Vector2(0f, gap - pad), pos + new Vector2(0f, arm + pad), color);
}
// Exodus-End
}
9 changes: 9 additions & 0 deletions Content.Client/Decals/UI/DecalPlacerWindow.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,16 @@

<BoxContainer Orientation="Vertical">
<ColorSelectorSliders Name="ColorPicker" IsAlphaVisible="True" />
<!-- Exodus-Start: toggle the current color as a favorite (★ = saved, ☆ = not saved) -->
<BoxContainer Orientation="Horizontal">
<Label Text="{Loc 'decal-placer-window-favorite-label'}" HorizontalExpand="True" />
<Button Name="FavoriteButton" Text="☆" MinWidth="40"
ToolTip="{Loc 'decal-placer-window-favorite-tooltip'}" />
</BoxContainer>
<!-- Exodus-End -->
<Button Name="PickerOpen" Text="{Loc 'decal-placer-window-palette'}" />
<!-- Exodus: eyedropper tool, copies the color of a decal already on the map -->
<Button Name="EyedropperButton" Text="{Loc 'decal-placer-window-eyedropper'}" />
Comment thread
TryHardo7 marked this conversation as resolved.
</BoxContainer>
<CheckBox Name="EnableAuto" Text="{Loc 'decal-placer-window-enable-auto'}" Margin="0 0 0 10"/>
<CheckBox Name="EnableColor" Text="{Loc 'decal-placer-window-use-color'}" />
Expand Down
Loading
Loading