Skip to content

Commit f298e52

Browse files
committed
Add line, rect, circle tools
1 parent e8eee26 commit f298e52

File tree

4 files changed

+331
-1
lines changed

4 files changed

+331
-1
lines changed

src/main.lua

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,9 @@ function App.new()
9595
self.currentAction = { tool = "brush" }
9696
self.startTime = os.clock()
9797
self.overlaySelection = nil
98+
self.overlayLine = nil
99+
self.overlayRectangle = nil
100+
self.overlayCircle = nil
98101

99102
return self
100103
end
@@ -717,12 +720,60 @@ function App:event(event, handler)
717720
)
718721
end
719722

723+
if self.overlayLine then
724+
local start = self.overlayLine.start
725+
local finish = self.overlayLine.finish or start
726+
727+
self.plugins.overlay:addLine(
728+
event.window,
729+
start.x, start.y,
730+
finish.x, finish.y,
731+
self.currentColor,
732+
2
733+
)
734+
end
735+
736+
if self.overlayRectangle then
737+
local start = self.overlayRectangle.start
738+
local finish = self.overlayRectangle.finish or start
739+
740+
local x1 = start.x
741+
local y1 = start.y
742+
local x2 = finish.x
743+
local y2 = finish.y
744+
745+
local boxX = math.min(x1, x2)
746+
local boxY = math.min(y1, y2)
747+
local boxW = math.abs(x2 - x1)
748+
local boxH = math.abs(y2 - y1)
749+
750+
self.plugins.overlay:addBox(
751+
event.window,
752+
boxX, boxY, boxW, boxH,
753+
self.currentColor,
754+
2
755+
)
756+
end
757+
758+
if self.overlayCircle then
759+
local start = self.overlayCircle.start
760+
local finish = self.overlayCircle.finish or start
761+
762+
self.plugins.overlay:addEllipse(
763+
event.window,
764+
start.x, start.y,
765+
finish.x, finish.y,
766+
self.currentColor,
767+
2
768+
)
769+
end
770+
720771
local time = os.clock() - self.startTime
721772
self.plugins.overlay:draw(event.window, "marching_ants", time)
722773

723774
ctx.renderCtx:swapBuffers()
724775

725-
if self.overlaySelection then
776+
if self.overlaySelection or self.overlayLine or self.overlayRectangle or self.overlayCircle then
726777
handler:requestRedraw(event.window)
727778
end
728779

@@ -793,6 +844,21 @@ function App:update(message, window)
793844
)
794845
self.isDrawing = true
795846
self.plugins.ui:refreshView(window)
847+
elseif self.currentAction.tool == "line" then
848+
local x = (message.x / message.elementWidth) * 800
849+
local y = (message.y / message.elementHeight) * 600
850+
self.overlayLine = { start = { x = x, y = y }, finish = nil }
851+
self.isDrawing = true
852+
elseif self.currentAction.tool == "square" then
853+
local x = (message.x / message.elementWidth) * 800
854+
local y = (message.y / message.elementHeight) * 600
855+
self.overlayRectangle = { start = { x = x, y = y }, finish = nil }
856+
self.isDrawing = true
857+
elseif self.currentAction.tool == "circle" then
858+
local x = (message.x / message.elementWidth) * 800
859+
local y = (message.y / message.elementHeight) * 600
860+
self.overlayCircle = { start = { x = x, y = y }, finish = nil }
861+
self.isDrawing = true
796862
end
797863
elseif message.type == "StopDrawing" then
798864
if self.currentAction.tool == "select" and self.overlaySelection then
@@ -809,6 +875,51 @@ function App:update(message, window)
809875
self.resources.compute:setSelection(startPos.x, startPos.y, finishPos.x, finishPos.y)
810876
self.overlaySelection.finish = { x = x, y = y }
811877
end
878+
elseif self.currentAction.tool == "line" and self.overlayLine then
879+
local x = (message.x / message.elementWidth) * 800
880+
local y = (message.y / message.elementHeight) * 600
881+
882+
local start = self.overlayLine.start
883+
if start.x ~= x or start.y ~= y then
884+
self.resources.compute:drawLine(
885+
start.x, start.y,
886+
x, y,
887+
2,
888+
self.currentColor
889+
)
890+
self.plugins.ui:refreshView(window)
891+
end
892+
self.overlayLine = nil
893+
elseif self.currentAction.tool == "square" and self.overlayRectangle then
894+
local x = (message.x / message.elementWidth) * 800
895+
local y = (message.y / message.elementHeight) * 600
896+
897+
local start = self.overlayRectangle.start
898+
if start.x ~= x or start.y ~= y then
899+
self.resources.compute:drawRectangle(
900+
start.x, start.y,
901+
x, y,
902+
2,
903+
self.currentColor
904+
)
905+
self.plugins.ui:refreshView(window)
906+
end
907+
self.overlayRectangle = nil
908+
elseif self.currentAction.tool == "circle" and self.overlayCircle then
909+
local x = (message.x / message.elementWidth) * 800
910+
local y = (message.y / message.elementHeight) * 600
911+
912+
local start = self.overlayCircle.start
913+
if start.x ~= x or start.y ~= y then
914+
self.resources.compute:drawEllipse(
915+
start.x, start.y,
916+
x, y,
917+
2,
918+
self.currentColor
919+
)
920+
self.plugins.ui:refreshView(window)
921+
end
922+
self.overlayCircle = nil
812923
end
813924
self.isDrawing = false
814925
window.shouldRedraw = true
@@ -839,6 +950,21 @@ function App:update(message, window)
839950
local y = (message.y / message.elementHeight) * 600
840951
self.overlaySelection.finish = { x = x, y = y }
841952
window.shouldRedraw = true
953+
elseif self.currentAction.tool == "line" and self.overlayLine then
954+
local x = (message.x / message.elementWidth) * 800
955+
local y = (message.y / message.elementHeight) * 600
956+
self.overlayLine.finish = { x = x, y = y }
957+
window.shouldRedraw = true
958+
elseif self.currentAction.tool == "square" and self.overlayRectangle then
959+
local x = (message.x / message.elementWidth) * 800
960+
local y = (message.y / message.elementHeight) * 600
961+
self.overlayRectangle.finish = { x = x, y = y }
962+
window.shouldRedraw = true
963+
elseif self.currentAction.tool == "circle" and self.overlayCircle then
964+
local x = (message.x / message.elementWidth) * 800
965+
local y = (message.y / message.elementHeight) * 600
966+
self.overlayCircle.finish = { x = x, y = y }
967+
window.shouldRedraw = true
842968
end
843969
self.plugins.ui:requestRedraw(window)
844970
end

src/plugin/overlay.lua

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,42 @@ function OverlayPlugin:addBox(window, x, y, width, height, color, thickness, z)
111111
self:addLine(window, x, y + height, x, y, color, thickness, z)
112112
end
113113

114+
---@param window Window
115+
---@param x1 number
116+
---@param y1 number
117+
---@param x2 number
118+
---@param y2 number
119+
---@param color {r: number, g: number, b: number, a: number}
120+
---@param thickness number?
121+
---@param z number?
122+
function OverlayPlugin:addEllipse(window, x1, y1, x2, y2, color, thickness, z)
123+
local ctx = self:getContext(window)
124+
if not ctx then return end
125+
126+
thickness = thickness or 1
127+
z = z or 99999
128+
129+
local centerX = (x1 + x2) / 2
130+
local centerY = (y1 + y2) / 2
131+
local radiusX = math.abs(x2 - x1) / 2
132+
local radiusY = math.abs(y2 - y1) / 2
133+
134+
local avgRadius = (radiusX + radiusY) / 2
135+
local segments = math.max(16, math.floor(avgRadius / 2))
136+
137+
for i = 0, segments - 1 do
138+
local angle1 = (i / segments) * 2 * math.pi
139+
local angle2 = ((i + 1) / segments) * 2 * math.pi
140+
141+
local px1 = centerX + math.cos(angle1) * radiusX
142+
local py1 = centerY + math.sin(angle1) * radiusY
143+
local px2 = centerX + math.cos(angle2) * radiusX
144+
local py2 = centerY + math.sin(angle2) * radiusY
145+
146+
self:addLine(window, px1, py1, px2, py2, color, thickness, z)
147+
end
148+
end
149+
114150
---@param window Window
115151
---@param x1 number
116152
---@param y1 number

src/shaders/brush.compute.glsl

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,15 @@ layout(location = 3) uniform vec4 color;
1212
// 0 - Brush
1313
// 1 - Eraser
1414
// 2 - Fill
15+
// 3 - Line
16+
// 4 - Rectangle
17+
// 5 - Circle
1518
layout(location = 4) uniform int tool;
1619

1720
layout(location = 5) uniform int readLayer;
1821
layout(location = 6) uniform vec2 selectTopLeft;
1922
layout(location = 7) uniform vec2 selectBottomRight;
23+
layout(location = 8) uniform ivec2 lineEnd;
2024

2125
float colorDistance(vec3 color1, vec3 color2) {
2226
vec3 diff = color1 - color2;
@@ -29,6 +33,15 @@ void main() {
2933
if (tool == 2) {
3034
// Fill uses direct coordinates
3135
pixelCoords = ivec2(gl_GlobalInvocationID.xy);
36+
} else if (tool == 3) {
37+
// Line uses direct coordinates
38+
pixelCoords = ivec2(gl_GlobalInvocationID.xy);
39+
} else if (tool == 4) {
40+
// Rectangle uses direct coordinates
41+
pixelCoords = ivec2(gl_GlobalInvocationID.xy);
42+
} else if (tool == 5) {
43+
// Circle uses direct coordinates
44+
pixelCoords = ivec2(gl_GlobalInvocationID.xy);
3245
} else {
3346
// Brush/eraser use radius-based coords
3447
ivec2 localCoords = ivec2(gl_GlobalInvocationID.xy);
@@ -48,6 +61,68 @@ void main() {
4861
imageStore(imgOutput, ivec3(pixelCoords, writeLayer), color);
4962
} else if (tool == 1) {
5063
imageStore(imgOutput, ivec3(pixelCoords, writeLayer), vec4(0.0, 0.0, 0.0, 0.0));
64+
} else if (tool == 3) {
65+
// Line drawing using Bresenham-like distance check
66+
vec2 lineStart = vec2(center);
67+
vec2 lineEndPos = vec2(lineEnd);
68+
vec2 pixelPos = vec2(pixelCoords);
69+
70+
vec2 lineVec = lineEndPos - lineStart;
71+
float lineLen = length(lineVec);
72+
73+
if (lineLen < 0.001) return;
74+
75+
vec2 lineDir = lineVec / lineLen;
76+
vec2 toPixel = pixelPos - lineStart;
77+
78+
float proj = dot(toPixel, lineDir);
79+
proj = clamp(proj, 0.0, lineLen);
80+
81+
vec2 closest = lineStart + lineDir * proj;
82+
float dist = distance(pixelPos, closest);
83+
84+
if (dist <= radius) {
85+
imageStore(imgOutput, ivec3(pixelCoords, writeLayer), color);
86+
}
87+
} else if (tool == 4) {
88+
// Rectangle drawing
89+
vec2 topLeft = vec2(min(center.x, lineEnd.x), min(center.y, lineEnd.y));
90+
vec2 bottomRight = vec2(max(center.x, lineEnd.x), max(center.y, lineEnd.y));
91+
vec2 pixelPos = vec2(pixelCoords);
92+
93+
float distLeft = abs(pixelPos.x - topLeft.x);
94+
float distRight = abs(pixelPos.x - bottomRight.x);
95+
float distTop = abs(pixelPos.y - topLeft.y);
96+
float distBottom = abs(pixelPos.y - bottomRight.y);
97+
98+
bool onLeft = (pixelPos.y >= topLeft.y && pixelPos.y <= bottomRight.y) && distLeft <= radius;
99+
bool onRight = (pixelPos.y >= topLeft.y && pixelPos.y <= bottomRight.y) && distRight <= radius;
100+
bool onTop = (pixelPos.x >= topLeft.x && pixelPos.x <= bottomRight.x) && distTop <= radius;
101+
bool onBottom = (pixelPos.x >= topLeft.x && pixelPos.x <= bottomRight.x) && distBottom <= radius;
102+
103+
if (onLeft || onRight || onTop || onBottom) {
104+
imageStore(imgOutput, ivec3(pixelCoords, writeLayer), color);
105+
}
106+
} else if (tool == 5) {
107+
// Ellipse drawing with corner-to-corner bounding box
108+
vec2 corner1 = vec2(center);
109+
vec2 corner2 = vec2(lineEnd);
110+
vec2 pixelPos = vec2(pixelCoords);
111+
112+
vec2 centerPos = (corner1 + corner2) / 2.0;
113+
vec2 radii = abs(corner2 - corner1) / 2.0;
114+
115+
if (radii.x < 0.001 || radii.y < 0.001) return;
116+
117+
vec2 normalized = (pixelPos - centerPos) / radii;
118+
float distFromEllipse = length(normalized);
119+
120+
float innerDist = 1.0 - (radius / max(radii.x, radii.y));
121+
float outerDist = 1.0 + (radius / max(radii.x, radii.y));
122+
123+
if (distFromEllipse >= innerDist && distFromEllipse <= outerDist) {
124+
imageStore(imgOutput, ivec3(pixelCoords, writeLayer), color);
125+
}
51126
} else if (tool == 2) {
52127
// At the origin - mark as filled
53128
if (center == pixelCoords) {

0 commit comments

Comments
 (0)