Skip to content
Merged
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
41 changes: 41 additions & 0 deletions Content.Tests/DMProject/Tests/Icon/DrawBox.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
// Assumes /icon.GetPixel(x, y) is working
// Assumes splittext(string, delimiter) is working

/*
=== Making a test icon ===
The pattern for a test case is like so: color,x1,y1,x2,y2
Empty parameters will be converted into null, the only thing important
Examples:
- Make a fully red icon: red,1,1,32,32
- Put a dot in the center: red,15,15,,
- Empty call: ,,,,

Operations can be chained together with ";" as a separator
Examples:
- Make a red icon, then cut a hole in the center: red,1,1,32,32;,8,8,25,25
- Make a red-green checkerboard: red,1,1,16,16;green,1,17,16,32;red,17,17,32,32;green,17,1,32,16
*/

/proc/CompareIcons(operation, icon/generated, icon/expected)
for(var/y in 1 to expected.Height())
for(var/x in 1 to expected.Width())
if(generated.GetPixel(x, y) != expected.GetPixel(x, y))
throw EXCEPTION("[operation] was not equivalent")

/proc/to_args(text)
. = splittext(text, ",")
for(var/i in 2 to length(.))
.[i] = text2num(.[i])

/proc/RunTest()
var/static/list/ignored_states = list()

var/static/icon/results_icon = icon('expected_results/DrawBox.dmi')
var/static/icon/base_icon = icon(results_icon, "")

for(var/state in icon_states(results_icon, 1) - "")
var/icon/expected_icon = icon('expected_results/DrawBox.dmi', state) // FIXME: passing results_icon doesn't work here
var/icon/generated_icon = icon(base_icon)
for(var/operation in splittext(state, ";"))
generated_icon.DrawBox(arglist(to_args(operation)))
CompareIcons(state, generated_icon, expected_icon)
Binary file not shown.
3 changes: 1 addition & 2 deletions DMCompiler/DMStandard/Types/Icon.dm
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@
proc/Crop(x1, y1, x2, y2)
set opendream_unimplemented = TRUE

proc/DrawBox(rgb, x1, y1, x2 = x1, y2 = y1)
set opendream_unimplemented = TRUE
proc/DrawBox(rgb as color|null, x1 as num, y1 as num, x2 = x1 as num|null, y2 = y1 as num|null)

proc/Flip(dir)
set opendream_unimplemented = TRUE
Expand Down
42 changes: 42 additions & 0 deletions OpenDreamRuntime/Objects/DreamIcon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -285,10 +285,52 @@ private ParsedDMIFrame[] DrawFrames(Rgba32[] pixels, int firstFrameIndex, List<I
}

public interface IDreamIconOperation {
/// <summary>
/// Called before icon operations are processed.
/// </summary>
/// <param name="icon">The icon that's being operated on.</param>
public void OnApply(DreamIcon icon);

/// <summary>
/// For every frame, in every direction, do this.
/// </summary>
/// <param name="pixels">A span of every pixel in the image sequence.</param>
/// <param name="imageSpan">The width of the entire image sequence, in pixels.</param>
/// <param name="frame">The index of the current frame.</param>
/// <param name="dir">The current direction of the frame.</param>
/// <param name="bounds">The width and height of a given frame.</param>
public void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, AtomDirection dir, UIBox2i bounds);
}

public sealed class DreamIconOperationDrawBox(Color rgb, Vector2i startPixel, Vector2i endPixel) : IDreamIconOperation {
private readonly Rgba32 _color = new(rgb.RByte, rgb.GByte, rgb.BByte, rgb.AByte);
private readonly Vector2i _bottomLeft = Vector2i.ComponentMin(startPixel, endPixel) - 1;
private readonly Vector2i _topRight = Vector2i.ComponentMax(startPixel, endPixel);

public void OnApply(DreamIcon icon) { }

public void ApplyToFrame(Rgba32[] pixels, int imageSpan, int frame, AtomDirection dir, UIBox2i bounds) {
// FIXME: This stuff can definitely be calculated once/ahead of time but we can't guarantee this in OnApply
var workingBounds = new UIBox2i(
Math.Max(_bottomLeft.X, bounds.Left), // left
Math.Max(bounds.Bottom - (_topRight.Y - bounds.Top), bounds.Top), // top
Math.Min(_topRight.X, bounds.Right), // right
Math.Min(bounds.Bottom - (_bottomLeft.Y - bounds.Top), bounds.Bottom) // bottom
);

var endPosY = Math.Min(bounds.Bottom, workingBounds.Bottom);
var endPosX = Math.Min(bounds.Right, workingBounds.Right);

for (int y = workingBounds.Top; y < endPosY; y++) {
for (int x = workingBounds.Left; x < endPosX; x++) {
int dstPixelPosition = (y * imageSpan) + x;

pixels[dstPixelPosition].FromRgba32(_color);
}
}
}
}

[Virtual]
public class DreamIconOperationBlend : IDreamIconOperation {
// With the same values as the ICON_* defines in DMStandard
Expand Down
1 change: 1 addition & 0 deletions OpenDreamRuntime/Procs/Native/DreamProcNative.cs
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ public static void SetupNativeProcs(DreamObjectTree objectTree) {
objectTree.SetNativeProc(objectTree.Icon, DreamProcNativeIcon.NativeProc_Height);
objectTree.SetNativeProc(objectTree.Icon, DreamProcNativeIcon.NativeProc_Insert);
objectTree.SetNativeProc(objectTree.Icon, DreamProcNativeIcon.NativeProc_Blend);
objectTree.SetNativeProc(objectTree.Icon, DreamProcNativeIcon.NativeProc_DrawBox);
objectTree.SetNativeProc(objectTree.Icon, DreamProcNativeIcon.NativeProc_GetPixel);
objectTree.SetNativeProc(objectTree.Icon, DreamProcNativeIcon.NativeProc_Scale);
objectTree.SetNativeProc(objectTree.Icon, DreamProcNativeIcon.NativeProc_Turn);
Expand Down
29 changes: 29 additions & 0 deletions OpenDreamRuntime/Procs/Native/DreamProcNativeIcon.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,35 @@ public static DreamValue NativeProc_Blend(NativeProc.Bundle bundle, DreamObject?
return DreamValue.Null;
}

[DreamProc("DrawBox")]
[DreamProcParameter("rgb", Type = DreamValueTypeFlag.String)]
[DreamProcParameter("x1", Type = DreamValueTypeFlag.Float)]
[DreamProcParameter("y1", Type = DreamValueTypeFlag.Float)]
[DreamProcParameter("x2", Type = DreamValueTypeFlag.Float)]
[DreamProcParameter("y2", Type = DreamValueTypeFlag.Float)]
public static DreamValue NativeProc_DrawBox(NativeProc.Bundle bundle, DreamObject? src, DreamObject? usr) {
var srcDreamIcon = ((DreamObjectIcon)src!).Icon;

var rgbValue = bundle.GetArgument(0, "rgb");
if(!rgbValue.TryGetValueAsString(out var rgbStr) || !ColorHelpers.TryParseColor(rgbStr, out var rgb))
if(string.IsNullOrEmpty(rgbStr))
rgb = Color.Transparent;
else
throw new ArgumentException($"invalid rgb value {rgbValue}");

int x1 = (int)bundle.GetArgument(1, "x1").UnsafeGetValueAsFloat();
int y1 = (int)bundle.GetArgument(2, "y1").UnsafeGetValueAsFloat();

if(!bundle.GetArgument(3, "x2").TryGetValueAsInteger(out var x2))
x2 = x1;
if(!bundle.GetArgument(4, "y2").TryGetValueAsInteger(out var y2))
y2 = y1;

srcDreamIcon.ApplyOperation(new DreamIconOperationDrawBox(rgb, new(x1, y1), new(x2, y2)));

return DreamValue.Null;
}

[DreamProc("GetPixel")]
[DreamProcParameter("x", Type = DreamValueTypeFlag.Float)]
[DreamProcParameter("y", Type = DreamValueTypeFlag.Float)]
Expand Down
Loading