1 -- primitives for editing drawings
3 require
'drawing_tests'
5 -- All drawings span 100% of some conceptual 'page width' and divide it up
7 function Drawing
.draw(State
, line_index
, y
)
8 local line
= State
.lines
[line_index
]
9 local line_cache
= State
.line_cache
[line_index
]
11 local pmx
,pmy
= App
.mouse_x(), App
.mouse_y()
12 if pmx
< State
.right
and pmy
> line_cache
.starty
and pmy
< line_cache
.starty
+Drawing
.pixels(line
.h
, State
.width
) then
14 love
.graphics
.rectangle('line', State
.left
,line_cache
.starty
, State
.width
,Drawing
.pixels(line
.h
, State
.width
))
15 if icon
[State
.current_drawing_mode
] then
16 icon
[State
.current_drawing_mode
](State
.right
-22, line_cache
.starty
+4)
18 icon
[State
.previous_drawing_mode
](State
.right
-22, line_cache
.starty
+4)
21 if App
.mouse_down(1) and love
.keyboard
.isDown('h') then
22 draw_help_with_mouse_pressed(State
, line_index
)
27 if line
.show_help
then
28 draw_help_without_mouse_pressed(State
, line_index
)
32 local mx
= Drawing
.coord(pmx
-State
.left
, State
.width
)
33 local my
= Drawing
.coord(pmy
-line_cache
.starty
, State
.width
)
35 for _
,shape
in ipairs(line
.shapes
) do
36 if geom
.on_shape(mx
,my
, line
, shape
) then
37 App
.color(Focus_stroke_color
)
39 App
.color(Stroke_color
)
41 Drawing
.draw_shape(line
, shape
, line_cache
.starty
, State
.left
,State
.right
)
44 local function px(x
) return Drawing
.pixels(x
, State
.width
)+State
.left
end
45 local function py(y
) return Drawing
.pixels(y
, State
.width
)+line_cache
.starty
end
46 for i
,p
in ipairs(line
.points
) do
47 if p
.deleted
== nil then
48 if Drawing
.near(p
, mx
,my
, State
.width
) then
49 App
.color(Focus_stroke_color
)
50 love
.graphics
.circle('line', px(p
.x
),py(p
.y
), Same_point_distance
)
52 App
.color(Stroke_color
)
53 love
.graphics
.circle('fill', px(p
.x
),py(p
.y
), 2)
57 local x
,y
= px(p
.x
)+5, py(p
.y
)+5
58 love
.graphics
.print(p
.name
, x
,y
)
59 if State
.current_drawing_mode
== 'name' and i
== line
.pending
.target_point
then
60 -- create a faint red box for the name
61 App
.color(Current_name_background_color
)
64 name_width
= State
.font
:getWidth('m')
66 name_width
= State
.font
:getWidth(p
.name
)
68 love
.graphics
.rectangle('fill', x
,y
, name_width
, State
.line_height
)
73 App
.color(Current_stroke_color
)
74 Drawing
.draw_pending_shape(line
, line_cache
.starty
, State
.left
,State
.right
)
77 function Drawing
.draw_shape(drawing
, shape
, top
, left
,right
)
78 local width
= right
-left
79 local function px(x
) return Drawing
.pixels(x
, width
)+left
end
80 local function py(y
) return Drawing
.pixels(y
, width
)+top
end
81 if shape
.mode
== 'freehand' then
83 for _
,point
in ipairs(shape
.points
) do
85 love
.graphics
.line(px(prev
.x
),py(prev
.y
), px(point
.x
),py(point
.y
))
89 elseif shape
.mode
== 'line' or shape
.mode
== 'manhattan' then
90 local p1
= drawing
.points
[shape
.p1
]
91 local p2
= drawing
.points
[shape
.p2
]
92 love
.graphics
.line(px(p1
.x
),py(p1
.y
), px(p2
.x
),py(p2
.y
))
93 elseif shape
.mode
== 'polygon' or shape
.mode
== 'rectangle' or shape
.mode
== 'square' then
95 for _
,point
in ipairs(shape
.vertices
) do
96 local curr
= drawing
.points
[point
]
98 love
.graphics
.line(px(prev
.x
),py(prev
.y
), px(curr
.x
),py(curr
.y
))
103 local curr
= drawing
.points
[shape
.vertices
[1]]
104 love
.graphics
.line(px(prev
.x
),py(prev
.y
), px(curr
.x
),py(curr
.y
))
105 elseif shape
.mode
== 'circle' then
107 local center
= drawing
.points
[shape
.center
]
108 love
.graphics
.circle('line', px(center
.x
),py(center
.y
), Drawing
.pixels(shape
.radius
, width
))
109 elseif shape
.mode
== 'arc' then
110 local center
= drawing
.points
[shape
.center
]
111 love
.graphics
.arc('line', 'open', px(center
.x
),py(center
.y
), Drawing
.pixels(shape
.radius
, width
), shape
.start_angle
, shape
.end_angle
, 360)
112 elseif shape
.mode
== 'deleted' then
115 assert(false, ('unknown drawing mode %s'):format(shape
.mode
))
119 function Drawing
.draw_pending_shape(drawing
, top
, left
,right
)
120 local width
= right
-left
121 local pmx
,pmy
= App
.mouse_x(), App
.mouse_y()
122 local function px(x
) return Drawing
.pixels(x
, width
)+left
end
123 local function py(y
) return Drawing
.pixels(y
, width
)+top
end
124 local mx
= Drawing
.coord(pmx
-left
, width
)
125 local my
= Drawing
.coord(pmy
-top
, width
)
126 -- recreate pixels from coords to precisely mimic how the drawing will look
127 -- after mouse_release
128 pmx
,pmy
= px(mx
), py(my
)
129 local shape
= drawing
.pending
130 if shape
.mode
== nil then
132 elseif shape
.mode
== 'freehand' then
133 local shape_copy
= deepcopy(shape
)
134 Drawing
.smoothen(shape_copy
)
135 Drawing
.draw_shape(drawing
, shape_copy
, top
, left
,right
)
136 elseif shape
.mode
== 'line' then
137 if mx
< 0 or mx
>= 256 or my
< 0 or my
>= drawing
.h
then
140 local p1
= drawing
.points
[shape
.p1
]
141 love
.graphics
.line(px(p1
.x
),py(p1
.y
), pmx
,pmy
)
142 elseif shape
.mode
== 'manhattan' then
143 if mx
< 0 or mx
>= 256 or my
< 0 or my
>= drawing
.h
then
146 local p1
= drawing
.points
[shape
.p1
]
147 if math
.abs(mx
-p1
.x
) > math
.abs(my
-p1
.y
) then
148 love
.graphics
.line(px(p1
.x
),py(p1
.y
), pmx
, py(p1
.y
))
150 love
.graphics
.line(px(p1
.x
),py(p1
.y
), px(p1
.x
),pmy
)
152 elseif shape
.mode
== 'polygon' then
153 -- don't close the loop on a pending polygon
155 for _
,point
in ipairs(shape
.vertices
) do
156 local curr
= drawing
.points
[point
]
158 love
.graphics
.line(px(prev
.x
),py(prev
.y
), px(curr
.x
),py(curr
.y
))
162 love
.graphics
.line(px(prev
.x
),py(prev
.y
), pmx
,pmy
)
163 elseif shape
.mode
== 'rectangle' then
164 local first
= drawing
.points
[shape
.vertices
[1]]
165 if #shape
.vertices
== 1 then
166 love
.graphics
.line(px(first
.x
),py(first
.y
), pmx
,pmy
)
169 local second
= drawing
.points
[shape
.vertices
[2]]
170 local thirdx
,thirdy
, fourthx
,fourthy
= Drawing
.complete_rectangle(first
.x
,first
.y
, second
.x
,second
.y
, mx
,my
)
171 love
.graphics
.line(px(first
.x
),py(first
.y
), px(second
.x
),py(second
.y
))
172 love
.graphics
.line(px(second
.x
),py(second
.y
), px(thirdx
),py(thirdy
))
173 love
.graphics
.line(px(thirdx
),py(thirdy
), px(fourthx
),py(fourthy
))
174 love
.graphics
.line(px(fourthx
),py(fourthy
), px(first
.x
),py(first
.y
))
175 elseif shape
.mode
== 'square' then
176 local first
= drawing
.points
[shape
.vertices
[1]]
177 if #shape
.vertices
== 1 then
178 love
.graphics
.line(px(first
.x
),py(first
.y
), pmx
,pmy
)
181 local second
= drawing
.points
[shape
.vertices
[2]]
182 local thirdx
,thirdy
, fourthx
,fourthy
= Drawing
.complete_square(first
.x
,first
.y
, second
.x
,second
.y
, mx
,my
)
183 love
.graphics
.line(px(first
.x
),py(first
.y
), px(second
.x
),py(second
.y
))
184 love
.graphics
.line(px(second
.x
),py(second
.y
), px(thirdx
),py(thirdy
))
185 love
.graphics
.line(px(thirdx
),py(thirdy
), px(fourthx
),py(fourthy
))
186 love
.graphics
.line(px(fourthx
),py(fourthy
), px(first
.x
),py(first
.y
))
187 elseif shape
.mode
== 'circle' then
188 local center
= drawing
.points
[shape
.center
]
189 if mx
< 0 or mx
>= 256 or my
< 0 or my
>= drawing
.h
then
192 local r
= round(geom
.dist(center
.x
, center
.y
, mx
, my
))
193 local cx
,cy
= px(center
.x
), py(center
.y
)
194 love
.graphics
.circle('line', cx
,cy
, Drawing
.pixels(r
, width
))
195 elseif shape
.mode
== 'arc' then
196 local center
= drawing
.points
[shape
.center
]
197 if mx
< 0 or mx
>= 256 or my
< 0 or my
>= drawing
.h
then
200 shape
.end_angle
= geom
.angle_with_hint(center
.x
,center
.y
, mx
,my
, shape
.end_angle
)
201 local cx
,cy
= px(center
.x
), py(center
.y
)
202 love
.graphics
.arc('line', 'open', cx
,cy
, Drawing
.pixels(shape
.radius
, width
), shape
.start_angle
, shape
.end_angle
, 360)
203 elseif shape
.mode
== 'move' then
204 -- nothing pending; changes are immediately committed
205 elseif shape
.mode
== 'name' then
206 -- nothing pending; changes are immediately committed
208 assert(false, ('unknown drawing mode %s'):format(shape
.mode
))
212 function Drawing
.in_drawing(drawing
, line_cache
, x
,y
, left
,right
)
213 if line_cache
.starty
== nil then return false end -- outside current page
214 local width
= right
-left
215 return y
>= line_cache
.starty
and y
< line_cache
.starty
+ Drawing
.pixels(drawing
.h
, width
) and x
>= left
and x
< right
218 function Drawing
.mouse_press(State
, drawing_index
, x
,y
, mouse_button
)
219 local drawing
= State
.lines
[drawing_index
]
220 local line_cache
= State
.line_cache
[drawing_index
]
221 local cx
= Drawing
.coord(x
-State
.left
, State
.width
)
222 local cy
= Drawing
.coord(y
-line_cache
.starty
, State
.width
)
223 if State
.current_drawing_mode
== 'freehand' then
224 drawing
.pending
= {mode
=State
.current_drawing_mode
, points
={{x
=cx
, y
=cy
}}}
225 elseif State
.current_drawing_mode
== 'line' or State
.current_drawing_mode
== 'manhattan' then
226 local j
= Drawing
.find_or_insert_point(drawing
.points
, cx
, cy
, State
.width
)
227 drawing
.pending
= {mode
=State
.current_drawing_mode
, p1
=j
}
228 elseif State
.current_drawing_mode
== 'polygon' or State
.current_drawing_mode
== 'rectangle' or State
.current_drawing_mode
== 'square' then
229 local j
= Drawing
.find_or_insert_point(drawing
.points
, cx
, cy
, State
.width
)
230 drawing
.pending
= {mode
=State
.current_drawing_mode
, vertices
={j
}}
231 elseif State
.current_drawing_mode
== 'circle' then
232 local j
= Drawing
.find_or_insert_point(drawing
.points
, cx
, cy
, State
.width
)
233 drawing
.pending
= {mode
=State
.current_drawing_mode
, center
=j
}
234 elseif State
.current_drawing_mode
== 'move' then
235 -- all the action is in mouse_release
236 elseif State
.current_drawing_mode
== 'name' then
239 assert(false, ('unknown drawing mode %s'):format(State
.current_drawing_mode
))
243 -- a couple of operations on drawings need to constantly check the state of the mouse
244 function Drawing
.update(State
)
245 if State
.lines
.current_drawing
== nil then return end
246 local drawing
= State
.lines
.current_drawing
247 local line_cache
= State
.line_cache
[State
.lines
.current_drawing_index
]
248 if line_cache
.starty
== nil then
249 -- some event cleared starty just this frame
250 -- draw in this frame will soon set starty
251 -- just skip this frame
254 assert(drawing
.mode
== 'drawing', 'Drawing.update: line is not a drawing')
255 local pmx
, pmy
= App
.mouse_x(), App
.mouse_y()
256 local mx
= Drawing
.coord(pmx
-State
.left
, State
.width
)
257 local my
= Drawing
.coord(pmy
-line_cache
.starty
, State
.width
)
258 if App
.mouse_down(1) then
259 if Drawing
.in_drawing(drawing
, line_cache
, pmx
,pmy
, State
.left
,State
.right
) then
260 if drawing
.pending
.mode
== 'freehand' then
261 table.insert(drawing
.pending
.points
, {x
=mx
, y
=my
})
262 elseif drawing
.pending
.mode
== 'move' then
263 drawing
.pending
.target_point
.x
= mx
264 drawing
.pending
.target_point
.y
= my
265 Drawing
.relax_constraints(drawing
, drawing
.pending
.target_point_index
)
268 elseif State
.current_drawing_mode
== 'move' then
269 if Drawing
.in_drawing(drawing
, line_cache
, pmx
, pmy
, State
.left
,State
.right
) then
270 drawing
.pending
.target_point
.x
= mx
271 drawing
.pending
.target_point
.y
= my
272 Drawing
.relax_constraints(drawing
, drawing
.pending
.target_point_index
)
279 function Drawing
.relax_constraints(drawing
, p
)
280 for _
,shape
in ipairs(drawing
.shapes
) do
281 if shape
.mode
== 'manhattan' then
282 if shape
.p1
== p
then
284 elseif shape
.p2
== p
then
287 elseif shape
.mode
== 'rectangle' or shape
.mode
== 'square' then
288 for _
,v
in ipairs(shape
.vertices
) do
290 shape
.mode
= 'polygon'
297 function Drawing
.mouse_release(State
, x
,y
, mouse_button
)
298 if State
.current_drawing_mode
== 'move' then
299 State
.current_drawing_mode
= State
.previous_drawing_mode
300 State
.previous_drawing_mode
= nil
301 if State
.lines
.current_drawing
then
302 State
.lines
.current_drawing
.pending
= {}
303 State
.lines
.current_drawing
= nil
305 elseif State
.lines
.current_drawing
then
306 local drawing
= State
.lines
.current_drawing
307 local line_cache
= State
.line_cache
[State
.lines
.current_drawing_index
]
308 if drawing
.pending
then
309 if drawing
.pending
.mode
== nil then
311 elseif drawing
.pending
.mode
== 'freehand' then
312 -- the last point added during update is good enough
313 Drawing
.smoothen(drawing
.pending
)
314 table.insert(drawing
.shapes
, drawing
.pending
)
315 elseif drawing
.pending
.mode
== 'line' then
316 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
317 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
318 drawing
.pending
.p2
= Drawing
.find_or_insert_point(drawing
.points
, mx
,my
, State
.width
)
319 table.insert(drawing
.shapes
, drawing
.pending
)
321 elseif drawing
.pending
.mode
== 'manhattan' then
322 local p1
= drawing
.points
[drawing
.pending
.p1
]
323 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
324 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
325 if math
.abs(mx
-p1
.x
) > math
.abs(my
-p1
.y
) then
326 drawing
.pending
.p2
= Drawing
.find_or_insert_point(drawing
.points
, mx
, p1
.y
, State
.width
)
328 drawing
.pending
.p2
= Drawing
.find_or_insert_point(drawing
.points
, p1
.x
, my
, State
.width
)
330 local p2
= drawing
.points
[drawing
.pending
.p2
]
331 App
.mouse_move(State
.left
+Drawing
.pixels(p2
.x
, State
.width
), line_cache
.starty
+Drawing
.pixels(p2
.y
, State
.width
))
332 table.insert(drawing
.shapes
, drawing
.pending
)
334 elseif drawing
.pending
.mode
== 'polygon' then
335 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
336 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
337 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, mx
,my
, State
.width
))
338 table.insert(drawing
.shapes
, drawing
.pending
)
340 elseif drawing
.pending
.mode
== 'rectangle' then
341 assert(#drawing
.pending
.vertices
<= 2, 'Drawing.mouse_release: rectangle has too many pending vertices')
342 if #drawing
.pending
.vertices
== 2 then
343 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
344 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
345 local first
= drawing
.points
[drawing
.pending
.vertices
[1]]
346 local second
= drawing
.points
[drawing
.pending
.vertices
[2]]
347 local thirdx
,thirdy
, fourthx
,fourthy
= Drawing
.complete_rectangle(first
.x
,first
.y
, second
.x
,second
.y
, mx
,my
)
348 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, thirdx
,thirdy
, State
.width
))
349 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, fourthx
,fourthy
, State
.width
))
350 table.insert(drawing
.shapes
, drawing
.pending
)
353 -- too few points; draw nothing
355 elseif drawing
.pending
.mode
== 'square' then
356 assert(#drawing
.pending
.vertices
<= 2, 'Drawing.mouse_release: square has too many pending vertices')
357 if #drawing
.pending
.vertices
== 2 then
358 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
359 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
360 local first
= drawing
.points
[drawing
.pending
.vertices
[1]]
361 local second
= drawing
.points
[drawing
.pending
.vertices
[2]]
362 local thirdx
,thirdy
, fourthx
,fourthy
= Drawing
.complete_square(first
.x
,first
.y
, second
.x
,second
.y
, mx
,my
)
363 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, thirdx
,thirdy
, State
.width
))
364 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, fourthx
,fourthy
, State
.width
))
365 table.insert(drawing
.shapes
, drawing
.pending
)
368 elseif drawing
.pending
.mode
== 'circle' then
369 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
370 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
371 local center
= drawing
.points
[drawing
.pending
.center
]
372 drawing
.pending
.radius
= round(geom
.dist(center
.x
,center
.y
, mx
,my
))
373 table.insert(drawing
.shapes
, drawing
.pending
)
375 elseif drawing
.pending
.mode
== 'arc' then
376 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
377 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
378 local center
= drawing
.points
[drawing
.pending
.center
]
379 drawing
.pending
.end_angle
= geom
.angle_with_hint(center
.x
,center
.y
, mx
,my
, drawing
.pending
.end_angle
)
380 table.insert(drawing
.shapes
, drawing
.pending
)
382 elseif drawing
.pending
.mode
== 'name' then
385 assert(false, ('unknown drawing mode %s'):format(drawing
.pending
.mode
))
387 State
.lines
.current_drawing
.pending
= {}
388 State
.lines
.current_drawing
= nil
393 function Drawing
.keychord_press(State
, chord
)
394 if chord
== 'C-p' and not App
.mouse_down(1) then
395 State
.current_drawing_mode
= 'freehand'
396 elseif App
.mouse_down(1) and chord
== 'l' then
397 State
.current_drawing_mode
= 'line'
398 local _
,drawing
= Drawing
.current_drawing(State
)
399 if drawing
.pending
.mode
== 'freehand' then
400 drawing
.pending
.p1
= Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)
401 elseif drawing
.pending
.mode
== 'polygon' or drawing
.pending
.mode
== 'rectangle' or drawing
.pending
.mode
== 'square' then
402 drawing
.pending
.p1
= drawing
.pending
.vertices
[1]
403 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
404 drawing
.pending
.p1
= drawing
.pending
.center
406 drawing
.pending
.mode
= 'line'
407 elseif chord
== 'C-l' and not App
.mouse_down(1) then
408 State
.current_drawing_mode
= 'line'
409 elseif App
.mouse_down(1) and chord
== 'm' then
410 State
.current_drawing_mode
= 'manhattan'
411 local drawing
= Drawing
.select_drawing_at_mouse(State
)
412 if drawing
.pending
.mode
== 'freehand' then
413 drawing
.pending
.p1
= Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)
414 elseif drawing
.pending
.mode
== 'line' then
416 elseif drawing
.pending
.mode
== 'polygon' or drawing
.pending
.mode
== 'rectangle' or drawing
.pending
.mode
== 'square' then
417 drawing
.pending
.p1
= drawing
.pending
.vertices
[1]
418 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
419 drawing
.pending
.p1
= drawing
.pending
.center
421 drawing
.pending
.mode
= 'manhattan'
422 elseif chord
== 'C-m' and not App
.mouse_down(1) then
423 State
.current_drawing_mode
= 'manhattan'
424 elseif chord
== 'C-g' and not App
.mouse_down(1) then
425 State
.current_drawing_mode
= 'polygon'
426 elseif App
.mouse_down(1) and chord
== 'g' then
427 State
.current_drawing_mode
= 'polygon'
428 local _
,drawing
= Drawing
.current_drawing(State
)
429 if drawing
.pending
.mode
== 'freehand' then
430 drawing
.pending
.vertices
= {Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)}
431 elseif drawing
.pending
.mode
== 'line' or drawing
.pending
.mode
== 'manhattan' then
432 if drawing
.pending
.vertices
== nil then
433 drawing
.pending
.vertices
= {drawing
.pending
.p1
}
435 elseif drawing
.pending
.mode
== 'rectangle' or drawing
.pending
.mode
== 'square' then
436 -- reuse existing vertices
437 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
438 drawing
.pending
.vertices
= {drawing
.pending
.center
}
440 drawing
.pending
.mode
= 'polygon'
441 elseif chord
== 'C-r' and not App
.mouse_down(1) then
442 State
.current_drawing_mode
= 'rectangle'
443 elseif App
.mouse_down(1) and chord
== 'r' then
444 State
.current_drawing_mode
= 'rectangle'
445 local _
,drawing
= Drawing
.current_drawing(State
)
446 if drawing
.pending
.mode
== 'freehand' then
447 drawing
.pending
.vertices
= {Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)}
448 elseif drawing
.pending
.mode
== 'line' or drawing
.pending
.mode
== 'manhattan' then
449 if drawing
.pending
.vertices
== nil then
450 drawing
.pending
.vertices
= {drawing
.pending
.p1
}
452 elseif drawing
.pending
.mode
== 'polygon' or drawing
.pending
.mode
== 'square' then
453 -- reuse existing (1-2) vertices
454 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
455 drawing
.pending
.vertices
= {drawing
.pending
.center
}
457 drawing
.pending
.mode
= 'rectangle'
458 elseif chord
== 'C-s' and not App
.mouse_down(1) then
459 State
.current_drawing_mode
= 'square'
460 elseif App
.mouse_down(1) and chord
== 's' then
461 State
.current_drawing_mode
= 'square'
462 local _
,drawing
= Drawing
.current_drawing(State
)
463 if drawing
.pending
.mode
== 'freehand' then
464 drawing
.pending
.vertices
= {Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)}
465 elseif drawing
.pending
.mode
== 'line' or drawing
.pending
.mode
== 'manhattan' then
466 if drawing
.pending
.vertices
== nil then
467 drawing
.pending
.vertices
= {drawing
.pending
.p1
}
469 elseif drawing
.pending
.mode
== 'polygon' then
470 while #drawing
.pending
.vertices
> 2 do
471 table.remove(drawing
.pending
.vertices
)
473 elseif drawing
.pending
.mode
== 'rectangle' then
474 -- reuse existing (1-2) vertices
475 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
476 drawing
.pending
.vertices
= {drawing
.pending
.center
}
478 drawing
.pending
.mode
= 'square'
479 elseif App
.mouse_down(1) and chord
== 'p' and State
.current_drawing_mode
== 'polygon' then
480 local _
,drawing
,line_cache
= Drawing
.current_drawing(State
)
481 local mx
,my
= Drawing
.coord(App
.mouse_x()-State
.left
, State
.width
), Drawing
.coord(App
.mouse_y()-line_cache
.starty
, State
.width
)
482 local j
= Drawing
.find_or_insert_point(drawing
.points
, mx
,my
, State
.width
)
483 table.insert(drawing
.pending
.vertices
, j
)
484 elseif App
.mouse_down(1) and chord
== 'p' and (State
.current_drawing_mode
== 'rectangle' or State
.current_drawing_mode
== 'square') then
485 local _
,drawing
,line_cache
= Drawing
.current_drawing(State
)
486 local mx
,my
= Drawing
.coord(App
.mouse_x()-State
.left
, State
.width
), Drawing
.coord(App
.mouse_y()-line_cache
.starty
, State
.width
)
487 local j
= Drawing
.find_or_insert_point(drawing
.points
, mx
,my
, State
.width
)
488 while #drawing
.pending
.vertices
>= 2 do
489 table.remove(drawing
.pending
.vertices
)
491 table.insert(drawing
.pending
.vertices
, j
)
492 elseif chord
== 'C-o' and not App
.mouse_down(1) then
493 State
.current_drawing_mode
= 'circle'
494 elseif App
.mouse_down(1) and chord
== 'a' and State
.current_drawing_mode
== 'circle' then
495 local _
,drawing
,line_cache
= Drawing
.current_drawing(State
)
496 drawing
.pending
.mode
= 'arc'
497 local mx
,my
= Drawing
.coord(App
.mouse_x()-State
.left
, State
.width
), Drawing
.coord(App
.mouse_y()-line_cache
.starty
, State
.width
)
498 local center
= drawing
.points
[drawing
.pending
.center
]
499 drawing
.pending
.radius
= round(geom
.dist(center
.x
,center
.y
, mx
,my
))
500 drawing
.pending
.start_angle
= geom
.angle(center
.x
,center
.y
, mx
,my
)
501 elseif App
.mouse_down(1) and chord
== 'o' then
502 State
.current_drawing_mode
= 'circle'
503 local _
,drawing
= Drawing
.current_drawing(State
)
504 if drawing
.pending
.mode
== 'freehand' then
505 drawing
.pending
.center
= Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)
506 elseif drawing
.pending
.mode
== 'line' or drawing
.pending
.mode
== 'manhattan' then
507 drawing
.pending
.center
= drawing
.pending
.p1
508 elseif drawing
.pending
.mode
== 'polygon' or drawing
.pending
.mode
== 'rectangle' or drawing
.pending
.mode
== 'square' then
509 drawing
.pending
.center
= drawing
.pending
.vertices
[1]
511 drawing
.pending
.mode
= 'circle'
512 elseif chord
== 'C-u' and not App
.mouse_down(1) then
513 local drawing_index
,drawing
,line_cache
,i
,p
= Drawing
.select_point_at_mouse(State
)
515 if State
.previous_drawing_mode
== nil then
516 State
.previous_drawing_mode
= State
.current_drawing_mode
518 State
.current_drawing_mode
= 'move'
519 drawing
.pending
= {mode
=State
.current_drawing_mode
, target_point
=p
, target_point_index
=i
}
520 State
.lines
.current_drawing_index
= drawing_index
521 State
.lines
.current_drawing
= drawing
523 elseif chord
== 'C-n' and not App
.mouse_down(1) then
524 local drawing_index
,drawing
,line_cache
,point_index
,p
= Drawing
.select_point_at_mouse(State
)
526 if State
.previous_drawing_mode
== nil then
528 State
.previous_drawing_mode
= State
.current_drawing_mode
530 State
.current_drawing_mode
= 'name'
532 drawing
.pending
= {mode
=State
.current_drawing_mode
, target_point
=point_index
}
533 State
.lines
.current_drawing_index
= drawing_index
534 State
.lines
.current_drawing
= drawing
536 elseif chord
== 'C-d' and not App
.mouse_down(1) then
537 local _
,drawing
,_
,i
,p
= Drawing
.select_point_at_mouse(State
)
539 for _
,shape
in ipairs(drawing
.shapes
) do
540 if Drawing
.contains_point(shape
, i
) then
541 if shape
.mode
== 'polygon' then
542 local idx
= table.find(shape
.vertices
, i
)
543 assert(idx
, 'point to delete is not in vertices')
544 table.remove(shape
.vertices
, idx
)
545 if #shape
.vertices
< 3 then
546 shape
.mode
= 'deleted'
549 shape
.mode
= 'deleted'
553 drawing
.points
[i
].deleted
= true
555 local drawing
,_
,_
,shape
= Drawing
.select_shape_at_mouse(State
)
557 shape
.mode
= 'deleted'
559 elseif chord
== 'C-h' and not App
.mouse_down(1) then
560 local drawing
= Drawing
.select_drawing_at_mouse(State
)
562 drawing
.show_help
= true
564 elseif chord
== 'escape' and App
.mouse_down(1) then
565 local _
,drawing
= Drawing
.current_drawing(State
)
570 function Drawing
.complete_rectangle(firstx
,firsty
, secondx
,secondy
, x
,y
)
571 if firstx
== secondx
then
572 return x
,secondy
, x
,firsty
574 if firsty
== secondy
then
575 return secondx
,y
, firstx
,y
577 local first_slope
= (secondy
-firsty
)/(secondx
-firstx
)
578 -- slope of second edge:
580 -- equation of line containing the second edge:
581 -- y-secondy = -1/first_slope*(x-secondx)
582 -- => 1/first_slope*x + y + (- secondy - secondx/first_slope) = 0
583 -- now we want to find the point on this line that's closest to the mouse pointer.
584 -- https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_an_equation
585 local a
= 1/first_slope
586 local c
= -secondy
- secondx
/first_slope
587 local thirdx
= round(((x
-a
*y
) - a
*c
) / (a
*a
+ 1))
588 local thirdy
= round((a
*(-x
+ a
*y
) - c
) / (a
*a
+ 1))
589 -- slope of third edge = first_slope
590 -- equation of line containing third edge:
591 -- y - thirdy = first_slope*(x-thirdx)
592 -- => -first_slope*x + y + (-thirdy + thirdx*first_slope) = 0
593 -- now we want to find the point on this line that's closest to the first point
594 local a
= -first_slope
595 local c
= -thirdy
+ thirdx
*first_slope
596 local fourthx
= round(((firstx
-a
*firsty
) - a
*c
) / (a
*a
+ 1))
597 local fourthy
= round((a
*(-firstx
+ a
*firsty
) - c
) / (a
*a
+ 1))
598 return thirdx
,thirdy
, fourthx
,fourthy
601 function Drawing
.complete_square(firstx
,firsty
, secondx
,secondy
, x
,y
)
602 -- use x,y only to decide which side of the first edge to complete the square on
603 local deltax
= secondx
-firstx
604 local deltay
= secondy
-firsty
605 local thirdx
= secondx
+deltay
606 local thirdy
= secondy
-deltax
607 if not geom
.same_side(firstx
,firsty
, secondx
,secondy
, thirdx
,thirdy
, x
,y
) then
610 thirdx
= secondx
+deltay
611 thirdy
= secondy
-deltax
613 local fourthx
= firstx
+deltay
614 local fourthy
= firsty
-deltax
615 return thirdx
,thirdy
, fourthx
,fourthy
618 function Drawing
.current_drawing(State
)
619 local x
, y
= App
.mouse_x(), App
.mouse_y()
620 for drawing_index
,drawing
in ipairs(State
.lines
) do
621 if drawing
.mode
== 'drawing' then
622 local line_cache
= State
.line_cache
[drawing_index
]
623 if Drawing
.in_drawing(drawing
, line_cache
, x
,y
, State
.left
,State
.right
) then
624 return drawing_index
,drawing
,line_cache
631 function Drawing
.select_shape_at_mouse(State
)
632 for drawing_index
,drawing
in ipairs(State
.lines
) do
633 if drawing
.mode
== 'drawing' then
634 local x
, y
= App
.mouse_x(), App
.mouse_y()
635 local line_cache
= State
.line_cache
[drawing_index
]
636 if Drawing
.in_drawing(drawing
, line_cache
, x
,y
, State
.left
,State
.right
) then
637 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
638 for i
,shape
in ipairs(drawing
.shapes
) do
639 if geom
.on_shape(mx
,my
, drawing
, shape
) then
640 return drawing
,line_cache
,i
,shape
648 function Drawing
.select_point_at_mouse(State
)
649 for drawing_index
,drawing
in ipairs(State
.lines
) do
650 if drawing
.mode
== 'drawing' then
651 local x
, y
= App
.mouse_x(), App
.mouse_y()
652 local line_cache
= State
.line_cache
[drawing_index
]
653 if Drawing
.in_drawing(drawing
, line_cache
, x
,y
, State
.left
,State
.right
) then
654 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
655 for i
,point
in ipairs(drawing
.points
) do
656 if Drawing
.near(point
, mx
,my
, State
.width
) then
657 return drawing_index
,drawing
,line_cache
,i
,point
665 function Drawing
.select_drawing_at_mouse(State
)
666 for drawing_index
,drawing
in ipairs(State
.lines
) do
667 if drawing
.mode
== 'drawing' then
668 local x
, y
= App
.mouse_x(), App
.mouse_y()
669 local line_cache
= State
.line_cache
[drawing_index
]
670 if Drawing
.in_drawing(drawing
, line_cache
, x
,y
, State
.left
,State
.right
) then
677 function Drawing
.contains_point(shape
, p
)
678 if shape
.mode
== 'freehand' then
680 elseif shape
.mode
== 'line' or shape
.mode
== 'manhattan' then
681 return shape
.p1
== p
or shape
.p2
== p
682 elseif shape
.mode
== 'polygon' or shape
.mode
== 'rectangle' or shape
.mode
== 'square' then
683 return table.find(shape
.vertices
, p
)
684 elseif shape
.mode
== 'circle' then
685 return shape
.center
== p
686 elseif shape
.mode
== 'arc' then
687 return shape
.center
== p
688 -- ugh, how to support angles
689 elseif shape
.mode
== 'deleted' then
692 assert(false, ('unknown drawing mode %s'):format(shape
.mode
))
696 function Drawing
.smoothen(shape
)
697 assert(shape
.mode
== 'freehand', 'can only smoothen freehand shapes')
699 for i
=2,#shape
.points
-1 do
700 local a
= shape
.points
[i
-1]
701 local b
= shape
.points
[i
]
702 local c
= shape
.points
[i
+1]
703 b
.x
= round((a
.x
+ b
.x
+ c
.x
)/3)
704 b
.y
= round((a
.y
+ b
.y
+ c
.y
)/3)
710 return math
.floor(num
+.5)
713 function Drawing
.find_or_insert_point(points
, x
,y
, width
)
714 -- check if UI would snap the two points together
715 for i
,point
in ipairs(points
) do
716 if Drawing
.near(point
, x
,y
, width
) then
720 table.insert(points
, {x
=x
, y
=y
})
724 function Drawing
.near(point
, x
,y
, width
)
725 local px
,py
= Drawing
.pixels(x
, width
),Drawing
.pixels(y
, width
)
726 local cx
,cy
= Drawing
.pixels(point
.x
, width
), Drawing
.pixels(point
.y
, width
)
727 return (cx
-px
)*(cx
-px
) + (cy
-py
)*(cy
-py
) < Same_point_distance
*Same_point_distance
730 function Drawing
.pixels(n
, width
) -- parts to pixels
731 return math
.floor(n
*width
/256)
733 function Drawing
.coord(n
, width
) -- pixels to parts
734 return math
.floor(n
*256/width
)
737 function table.find(h
, x
)
738 for k
,v
in pairs(h
) do