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 pmx
,pmy
= App
.mouse_x(), App
.mouse_y()
10 local starty
= Text
.starty(State
, line_index
)
11 if pmx
< State
.right
and pmy
> starty
and pmy
< starty
+Drawing
.pixels(line
.h
, State
.width
) then
13 love
.graphics
.rectangle('line', State
.left
,starty
, State
.width
,Drawing
.pixels(line
.h
, State
.width
))
14 if icon
[State
.current_drawing_mode
] then
15 icon
[State
.current_drawing_mode
](State
.right
-22, starty
+4)
17 icon
[State
.previous_drawing_mode
](State
.right
-22, starty
+4)
20 if App
.mouse_down(1) and love
.keyboard
.isDown('h') then
21 draw_help_with_mouse_pressed(State
, line_index
)
26 if line
.show_help
then
27 draw_help_without_mouse_pressed(State
, line_index
)
31 local mx
= Drawing
.coord(pmx
-State
.left
, State
.width
)
32 local my
= Drawing
.coord(pmy
-starty
, State
.width
)
34 for _
,shape
in ipairs(line
.shapes
) do
35 if geom
.on_shape(mx
,my
, line
, shape
) then
36 App
.color(Focus_stroke_color
)
38 App
.color(Stroke_color
)
40 Drawing
.draw_shape(line
, shape
, starty
, State
.left
,State
.right
)
43 local function px(x
) return Drawing
.pixels(x
, State
.width
)+State
.left
end
44 local function py(y
) return Drawing
.pixels(y
, State
.width
)+starty
end
45 for i
,p
in ipairs(line
.points
) do
46 if p
.deleted
== nil then
47 if Drawing
.near(p
, mx
,my
, State
.width
) then
48 App
.color(Focus_stroke_color
)
49 love
.graphics
.circle('line', px(p
.x
),py(p
.y
), Same_point_distance
)
51 App
.color(Stroke_color
)
52 love
.graphics
.circle('fill', px(p
.x
),py(p
.y
), 2)
56 local x
,y
= px(p
.x
)+5, py(p
.y
)+5
57 love
.graphics
.print(p
.name
, x
,y
)
58 if State
.current_drawing_mode
== 'name' and i
== line
.pending
.target_point
then
59 -- create a faint red box for the name
60 App
.color(Current_name_background_color
)
63 name_width
= State
.font
:getWidth('m')
65 name_width
= State
.font
:getWidth(p
.name
)
67 love
.graphics
.rectangle('fill', x
,y
, name_width
, State
.line_height
)
72 App
.color(Current_stroke_color
)
73 Drawing
.draw_pending_shape(line
, starty
, State
.left
,State
.right
)
76 function Drawing
.draw_shape(drawing
, shape
, top
, left
,right
)
77 local width
= right
-left
78 local function px(x
) return Drawing
.pixels(x
, width
)+left
end
79 local function py(y
) return Drawing
.pixels(y
, width
)+top
end
80 if shape
.mode
== 'freehand' then
82 for _
,point
in ipairs(shape
.points
) do
84 love
.graphics
.line(px(prev
.x
),py(prev
.y
), px(point
.x
),py(point
.y
))
88 elseif shape
.mode
== 'line' or shape
.mode
== 'manhattan' then
89 local p1
= drawing
.points
[shape
.p1
]
90 local p2
= drawing
.points
[shape
.p2
]
91 love
.graphics
.line(px(p1
.x
),py(p1
.y
), px(p2
.x
),py(p2
.y
))
92 elseif shape
.mode
== 'polygon' or shape
.mode
== 'rectangle' or shape
.mode
== 'square' then
94 for _
,point
in ipairs(shape
.vertices
) do
95 local curr
= drawing
.points
[point
]
97 love
.graphics
.line(px(prev
.x
),py(prev
.y
), px(curr
.x
),py(curr
.y
))
102 local curr
= drawing
.points
[shape
.vertices
[1]]
103 love
.graphics
.line(px(prev
.x
),py(prev
.y
), px(curr
.x
),py(curr
.y
))
104 elseif shape
.mode
== 'circle' then
106 local center
= drawing
.points
[shape
.center
]
107 love
.graphics
.circle('line', px(center
.x
),py(center
.y
), Drawing
.pixels(shape
.radius
, width
))
108 elseif shape
.mode
== 'arc' then
109 local center
= drawing
.points
[shape
.center
]
110 love
.graphics
.arc('line', 'open', px(center
.x
),py(center
.y
), Drawing
.pixels(shape
.radius
, width
), shape
.start_angle
, shape
.end_angle
, 360)
111 elseif shape
.mode
== 'deleted' then
114 assert(false, ('unknown drawing mode %s'):format(shape
.mode
))
118 function Drawing
.draw_pending_shape(drawing
, top
, left
,right
)
119 local width
= right
-left
120 local pmx
,pmy
= App
.mouse_x(), App
.mouse_y()
121 local function px(x
) return Drawing
.pixels(x
, width
)+left
end
122 local function py(y
) return Drawing
.pixels(y
, width
)+top
end
123 local mx
= Drawing
.coord(pmx
-left
, width
)
124 local my
= Drawing
.coord(pmy
-top
, width
)
125 -- recreate pixels from coords to precisely mimic how the drawing will look
126 -- after mouse_release
127 pmx
,pmy
= px(mx
), py(my
)
128 local shape
= drawing
.pending
129 if shape
.mode
== nil then
131 elseif shape
.mode
== 'freehand' then
132 local shape_copy
= deepcopy(shape
)
133 Drawing
.smoothen(shape_copy
)
134 Drawing
.draw_shape(drawing
, shape_copy
, top
, left
,right
)
135 elseif shape
.mode
== 'line' then
136 if mx
< 0 or mx
>= 256 or my
< 0 or my
>= drawing
.h
then
139 local p1
= drawing
.points
[shape
.p1
]
140 love
.graphics
.line(px(p1
.x
),py(p1
.y
), pmx
,pmy
)
141 elseif shape
.mode
== 'manhattan' then
142 if mx
< 0 or mx
>= 256 or my
< 0 or my
>= drawing
.h
then
145 local p1
= drawing
.points
[shape
.p1
]
146 if math
.abs(mx
-p1
.x
) > math
.abs(my
-p1
.y
) then
147 love
.graphics
.line(px(p1
.x
),py(p1
.y
), pmx
, py(p1
.y
))
149 love
.graphics
.line(px(p1
.x
),py(p1
.y
), px(p1
.x
),pmy
)
151 elseif shape
.mode
== 'polygon' then
152 -- don't close the loop on a pending polygon
154 for _
,point
in ipairs(shape
.vertices
) do
155 local curr
= drawing
.points
[point
]
157 love
.graphics
.line(px(prev
.x
),py(prev
.y
), px(curr
.x
),py(curr
.y
))
161 love
.graphics
.line(px(prev
.x
),py(prev
.y
), pmx
,pmy
)
162 elseif shape
.mode
== 'rectangle' then
163 local first
= drawing
.points
[shape
.vertices
[1]]
164 if #shape
.vertices
== 1 then
165 love
.graphics
.line(px(first
.x
),py(first
.y
), pmx
,pmy
)
168 local second
= drawing
.points
[shape
.vertices
[2]]
169 local thirdx
,thirdy
, fourthx
,fourthy
= Drawing
.complete_rectangle(first
.x
,first
.y
, second
.x
,second
.y
, mx
,my
)
170 love
.graphics
.line(px(first
.x
),py(first
.y
), px(second
.x
),py(second
.y
))
171 love
.graphics
.line(px(second
.x
),py(second
.y
), px(thirdx
),py(thirdy
))
172 love
.graphics
.line(px(thirdx
),py(thirdy
), px(fourthx
),py(fourthy
))
173 love
.graphics
.line(px(fourthx
),py(fourthy
), px(first
.x
),py(first
.y
))
174 elseif shape
.mode
== 'square' then
175 local first
= drawing
.points
[shape
.vertices
[1]]
176 if #shape
.vertices
== 1 then
177 love
.graphics
.line(px(first
.x
),py(first
.y
), pmx
,pmy
)
180 local second
= drawing
.points
[shape
.vertices
[2]]
181 local thirdx
,thirdy
, fourthx
,fourthy
= Drawing
.complete_square(first
.x
,first
.y
, second
.x
,second
.y
, mx
,my
)
182 love
.graphics
.line(px(first
.x
),py(first
.y
), px(second
.x
),py(second
.y
))
183 love
.graphics
.line(px(second
.x
),py(second
.y
), px(thirdx
),py(thirdy
))
184 love
.graphics
.line(px(thirdx
),py(thirdy
), px(fourthx
),py(fourthy
))
185 love
.graphics
.line(px(fourthx
),py(fourthy
), px(first
.x
),py(first
.y
))
186 elseif shape
.mode
== 'circle' then
187 local center
= drawing
.points
[shape
.center
]
188 if mx
< 0 or mx
>= 256 or my
< 0 or my
>= drawing
.h
then
191 local r
= round(geom
.dist(center
.x
, center
.y
, mx
, my
))
192 local cx
,cy
= px(center
.x
), py(center
.y
)
193 love
.graphics
.circle('line', cx
,cy
, Drawing
.pixels(r
, width
))
194 elseif shape
.mode
== 'arc' then
195 local center
= drawing
.points
[shape
.center
]
196 if mx
< 0 or mx
>= 256 or my
< 0 or my
>= drawing
.h
then
199 shape
.end_angle
= geom
.angle_with_hint(center
.x
,center
.y
, mx
,my
, shape
.end_angle
)
200 local cx
,cy
= px(center
.x
), py(center
.y
)
201 love
.graphics
.arc('line', 'open', cx
,cy
, Drawing
.pixels(shape
.radius
, width
), shape
.start_angle
, shape
.end_angle
, 360)
202 elseif shape
.mode
== 'move' then
203 -- nothing pending; changes are immediately committed
204 elseif shape
.mode
== 'name' then
205 -- nothing pending; changes are immediately committed
207 assert(false, ('unknown drawing mode %s'):format(shape
.mode
))
211 function Drawing
.in_current_drawing(State
, x
,y
, left
,right
)
212 return Drawing
.in_drawing(State
, State
.lines
.current_drawing_index
, x
,y
, left
,right
)
215 function Drawing
.in_drawing(State
, line_index
, x
,y
, left
,right
)
216 assert(State
.lines
[line_index
].mode
== 'drawing')
217 local starty
= Text
.starty(State
, line_index
)
218 if starty
== nil then return false end -- outside current page
219 local drawing
= State
.lines
[line_index
]
220 local width
= right
-left
221 return y
>= starty
and y
< starty
+ Drawing
.pixels(drawing
.h
, width
) and x
>= left
and x
< right
224 function Drawing
.mouse_press(State
, drawing_index
, x
,y
, mouse_button
)
225 local drawing
= State
.lines
[drawing_index
]
226 local starty
= Text
.starty(State
, drawing_index
)
227 local cx
= Drawing
.coord(x
-State
.left
, State
.width
)
228 local cy
= Drawing
.coord(y
-starty
, State
.width
)
229 if State
.current_drawing_mode
== 'freehand' then
230 drawing
.pending
= {mode
=State
.current_drawing_mode
, points
={{x
=cx
, y
=cy
}}}
231 elseif State
.current_drawing_mode
== 'line' or State
.current_drawing_mode
== 'manhattan' then
232 local j
= Drawing
.find_or_insert_point(drawing
.points
, cx
, cy
, State
.width
)
233 drawing
.pending
= {mode
=State
.current_drawing_mode
, p1
=j
}
234 elseif State
.current_drawing_mode
== 'polygon' or State
.current_drawing_mode
== 'rectangle' or State
.current_drawing_mode
== 'square' then
235 local j
= Drawing
.find_or_insert_point(drawing
.points
, cx
, cy
, State
.width
)
236 drawing
.pending
= {mode
=State
.current_drawing_mode
, vertices
={j
}}
237 elseif State
.current_drawing_mode
== 'circle' then
238 local j
= Drawing
.find_or_insert_point(drawing
.points
, cx
, cy
, State
.width
)
239 drawing
.pending
= {mode
=State
.current_drawing_mode
, center
=j
}
240 elseif State
.current_drawing_mode
== 'move' then
241 -- all the action is in mouse_release
242 elseif State
.current_drawing_mode
== 'name' then
245 assert(false, ('unknown drawing mode %s'):format(State
.current_drawing_mode
))
249 -- a couple of operations on drawings need to constantly check the state of the mouse
250 function Drawing
.update(State
)
251 if State
.lines
.current_drawing
== nil then return end
252 local drawing
= State
.lines
.current_drawing
253 local starty
= Text
.starty(State
, State
.lines
.current_drawing_index
)
254 if starty
== nil then
255 -- some event cleared starty just this frame
256 -- draw in this frame will soon set starty
257 -- just skip this frame
260 assert(drawing
.mode
== 'drawing', 'Drawing.update: line is not a drawing')
261 local pmx
, pmy
= App
.mouse_x(), App
.mouse_y()
262 local mx
= Drawing
.coord(pmx
-State
.left
, State
.width
)
263 local my
= Drawing
.coord(pmy
-starty
, State
.width
)
264 if App
.mouse_down(1) then
265 if Drawing
.in_current_drawing(State
, pmx
,pmy
, State
.left
,State
.right
) then
266 if drawing
.pending
.mode
== 'freehand' then
267 table.insert(drawing
.pending
.points
, {x
=mx
, y
=my
})
268 elseif drawing
.pending
.mode
== 'move' then
269 drawing
.pending
.target_point
.x
= mx
270 drawing
.pending
.target_point
.y
= my
271 Drawing
.relax_constraints(drawing
, drawing
.pending
.target_point_index
)
274 elseif State
.current_drawing_mode
== 'move' then
275 if Drawing
.in_current_drawing(State
, pmx
, pmy
, State
.left
,State
.right
) then
276 drawing
.pending
.target_point
.x
= mx
277 drawing
.pending
.target_point
.y
= my
278 Drawing
.relax_constraints(drawing
, drawing
.pending
.target_point_index
)
285 function Drawing
.relax_constraints(drawing
, p
)
286 for _
,shape
in ipairs(drawing
.shapes
) do
287 if shape
.mode
== 'manhattan' then
288 if shape
.p1
== p
then
290 elseif shape
.p2
== p
then
293 elseif shape
.mode
== 'rectangle' or shape
.mode
== 'square' then
294 for _
,v
in ipairs(shape
.vertices
) do
296 shape
.mode
= 'polygon'
303 function Drawing
.mouse_release(State
, x
,y
, mouse_button
)
304 if State
.current_drawing_mode
== 'move' then
305 State
.current_drawing_mode
= State
.previous_drawing_mode
306 State
.previous_drawing_mode
= nil
307 if State
.lines
.current_drawing
then
308 State
.lines
.current_drawing
.pending
= {}
309 State
.lines
.current_drawing
= nil
311 elseif State
.lines
.current_drawing
then
312 local drawing
= State
.lines
.current_drawing
313 local starty
= Text
.starty(State
, State
.lines
.current_drawing_index
)
314 if drawing
.pending
then
315 if drawing
.pending
.mode
== nil then
317 elseif drawing
.pending
.mode
== 'freehand' then
318 -- the last point added during update is good enough
319 Drawing
.smoothen(drawing
.pending
)
320 table.insert(drawing
.shapes
, drawing
.pending
)
321 elseif drawing
.pending
.mode
== 'line' then
322 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-starty
, State
.width
)
323 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
324 drawing
.pending
.p2
= Drawing
.find_or_insert_point(drawing
.points
, mx
,my
, State
.width
)
325 table.insert(drawing
.shapes
, drawing
.pending
)
327 elseif drawing
.pending
.mode
== 'manhattan' then
328 local p1
= drawing
.points
[drawing
.pending
.p1
]
329 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-starty
, State
.width
)
330 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
331 if math
.abs(mx
-p1
.x
) > math
.abs(my
-p1
.y
) then
332 drawing
.pending
.p2
= Drawing
.find_or_insert_point(drawing
.points
, mx
, p1
.y
, State
.width
)
334 drawing
.pending
.p2
= Drawing
.find_or_insert_point(drawing
.points
, p1
.x
, my
, State
.width
)
336 local p2
= drawing
.points
[drawing
.pending
.p2
]
337 App
.mouse_move(State
.left
+Drawing
.pixels(p2
.x
, State
.width
), starty
+Drawing
.pixels(p2
.y
, State
.width
))
338 table.insert(drawing
.shapes
, drawing
.pending
)
340 elseif drawing
.pending
.mode
== 'polygon' then
341 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-starty
, State
.width
)
342 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
343 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, mx
,my
, State
.width
))
344 table.insert(drawing
.shapes
, drawing
.pending
)
346 elseif drawing
.pending
.mode
== 'rectangle' then
347 assert(#drawing
.pending
.vertices
<= 2, 'Drawing.mouse_release: rectangle has too many pending vertices')
348 if #drawing
.pending
.vertices
== 2 then
349 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-starty
, State
.width
)
350 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
351 local first
= drawing
.points
[drawing
.pending
.vertices
[1]]
352 local second
= drawing
.points
[drawing
.pending
.vertices
[2]]
353 local thirdx
,thirdy
, fourthx
,fourthy
= Drawing
.complete_rectangle(first
.x
,first
.y
, second
.x
,second
.y
, mx
,my
)
354 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, thirdx
,thirdy
, State
.width
))
355 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, fourthx
,fourthy
, State
.width
))
356 table.insert(drawing
.shapes
, drawing
.pending
)
359 -- too few points; draw nothing
361 elseif drawing
.pending
.mode
== 'square' then
362 assert(#drawing
.pending
.vertices
<= 2, 'Drawing.mouse_release: square has too many pending vertices')
363 if #drawing
.pending
.vertices
== 2 then
364 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-starty
, State
.width
)
365 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
366 local first
= drawing
.points
[drawing
.pending
.vertices
[1]]
367 local second
= drawing
.points
[drawing
.pending
.vertices
[2]]
368 local thirdx
,thirdy
, fourthx
,fourthy
= Drawing
.complete_square(first
.x
,first
.y
, second
.x
,second
.y
, mx
,my
)
369 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, thirdx
,thirdy
, State
.width
))
370 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, fourthx
,fourthy
, State
.width
))
371 table.insert(drawing
.shapes
, drawing
.pending
)
374 elseif drawing
.pending
.mode
== 'circle' then
375 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-starty
, State
.width
)
376 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
377 local center
= drawing
.points
[drawing
.pending
.center
]
378 drawing
.pending
.radius
= round(geom
.dist(center
.x
,center
.y
, mx
,my
))
379 table.insert(drawing
.shapes
, drawing
.pending
)
381 elseif drawing
.pending
.mode
== 'arc' then
382 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-starty
, State
.width
)
383 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
384 local center
= drawing
.points
[drawing
.pending
.center
]
385 drawing
.pending
.end_angle
= geom
.angle_with_hint(center
.x
,center
.y
, mx
,my
, drawing
.pending
.end_angle
)
386 table.insert(drawing
.shapes
, drawing
.pending
)
388 elseif drawing
.pending
.mode
== 'name' then
391 assert(false, ('unknown drawing mode %s'):format(drawing
.pending
.mode
))
393 State
.lines
.current_drawing
.pending
= {}
394 State
.lines
.current_drawing
= nil
399 function Drawing
.keychord_press(State
, chord
)
400 if chord
== 'C-p' and not App
.mouse_down(1) then
401 State
.current_drawing_mode
= 'freehand'
402 elseif App
.mouse_down(1) and chord
== 'l' then
403 State
.current_drawing_mode
= 'line'
404 local _
,drawing
= Drawing
.current_drawing(State
)
405 if drawing
.pending
.mode
== 'freehand' then
406 drawing
.pending
.p1
= Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)
407 elseif drawing
.pending
.mode
== 'polygon' or drawing
.pending
.mode
== 'rectangle' or drawing
.pending
.mode
== 'square' then
408 drawing
.pending
.p1
= drawing
.pending
.vertices
[1]
409 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
410 drawing
.pending
.p1
= drawing
.pending
.center
412 drawing
.pending
.mode
= 'line'
413 elseif chord
== 'C-l' and not App
.mouse_down(1) then
414 State
.current_drawing_mode
= 'line'
415 elseif App
.mouse_down(1) and chord
== 'm' then
416 State
.current_drawing_mode
= 'manhattan'
417 local drawing
= Drawing
.select_drawing_at_mouse(State
)
418 if drawing
.pending
.mode
== 'freehand' then
419 drawing
.pending
.p1
= Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)
420 elseif drawing
.pending
.mode
== 'line' then
422 elseif drawing
.pending
.mode
== 'polygon' or drawing
.pending
.mode
== 'rectangle' or drawing
.pending
.mode
== 'square' then
423 drawing
.pending
.p1
= drawing
.pending
.vertices
[1]
424 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
425 drawing
.pending
.p1
= drawing
.pending
.center
427 drawing
.pending
.mode
= 'manhattan'
428 elseif chord
== 'C-m' and not App
.mouse_down(1) then
429 State
.current_drawing_mode
= 'manhattan'
430 elseif chord
== 'C-g' and not App
.mouse_down(1) then
431 State
.current_drawing_mode
= 'polygon'
432 elseif App
.mouse_down(1) and chord
== 'g' then
433 State
.current_drawing_mode
= 'polygon'
434 local _
,drawing
= Drawing
.current_drawing(State
)
435 if drawing
.pending
.mode
== 'freehand' then
436 drawing
.pending
.vertices
= {Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)}
437 elseif drawing
.pending
.mode
== 'line' or drawing
.pending
.mode
== 'manhattan' then
438 if drawing
.pending
.vertices
== nil then
439 drawing
.pending
.vertices
= {drawing
.pending
.p1
}
441 elseif drawing
.pending
.mode
== 'rectangle' or drawing
.pending
.mode
== 'square' then
442 -- reuse existing vertices
443 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
444 drawing
.pending
.vertices
= {drawing
.pending
.center
}
446 drawing
.pending
.mode
= 'polygon'
447 elseif chord
== 'C-r' and not App
.mouse_down(1) then
448 State
.current_drawing_mode
= 'rectangle'
449 elseif App
.mouse_down(1) and chord
== 'r' then
450 State
.current_drawing_mode
= 'rectangle'
451 local _
,drawing
= Drawing
.current_drawing(State
)
452 if drawing
.pending
.mode
== 'freehand' then
453 drawing
.pending
.vertices
= {Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)}
454 elseif drawing
.pending
.mode
== 'line' or drawing
.pending
.mode
== 'manhattan' then
455 if drawing
.pending
.vertices
== nil then
456 drawing
.pending
.vertices
= {drawing
.pending
.p1
}
458 elseif drawing
.pending
.mode
== 'polygon' or drawing
.pending
.mode
== 'square' then
459 -- reuse existing (1-2) vertices
460 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
461 drawing
.pending
.vertices
= {drawing
.pending
.center
}
463 drawing
.pending
.mode
= 'rectangle'
464 elseif chord
== 'C-s' and not App
.mouse_down(1) then
465 State
.current_drawing_mode
= 'square'
466 elseif App
.mouse_down(1) and chord
== 's' then
467 State
.current_drawing_mode
= 'square'
468 local _
,drawing
= Drawing
.current_drawing(State
)
469 if drawing
.pending
.mode
== 'freehand' then
470 drawing
.pending
.vertices
= {Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)}
471 elseif drawing
.pending
.mode
== 'line' or drawing
.pending
.mode
== 'manhattan' then
472 if drawing
.pending
.vertices
== nil then
473 drawing
.pending
.vertices
= {drawing
.pending
.p1
}
475 elseif drawing
.pending
.mode
== 'polygon' then
476 while #drawing
.pending
.vertices
> 2 do
477 table.remove(drawing
.pending
.vertices
)
479 elseif drawing
.pending
.mode
== 'rectangle' then
480 -- reuse existing (1-2) vertices
481 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
482 drawing
.pending
.vertices
= {drawing
.pending
.center
}
484 drawing
.pending
.mode
= 'square'
485 elseif App
.mouse_down(1) and chord
== 'p' and State
.current_drawing_mode
== 'polygon' then
486 local drawing_index
,drawing
= Drawing
.current_drawing(State
)
487 local starty
= Text
.starty(State
, drawing_index
)
488 local mx
,my
= Drawing
.coord(App
.mouse_x()-State
.left
, State
.width
), Drawing
.coord(App
.mouse_y()-starty
, State
.width
)
489 local j
= Drawing
.find_or_insert_point(drawing
.points
, mx
,my
, State
.width
)
490 table.insert(drawing
.pending
.vertices
, j
)
491 elseif App
.mouse_down(1) and chord
== 'p' and (State
.current_drawing_mode
== 'rectangle' or State
.current_drawing_mode
== 'square') then
492 local drawing_index
,drawing
= Drawing
.current_drawing(State
)
493 local starty
= Text
.starty(State
, drawing_index
)
494 local mx
,my
= Drawing
.coord(App
.mouse_x()-State
.left
, State
.width
), Drawing
.coord(App
.mouse_y()-starty
, State
.width
)
495 local j
= Drawing
.find_or_insert_point(drawing
.points
, mx
,my
, State
.width
)
496 while #drawing
.pending
.vertices
>= 2 do
497 table.remove(drawing
.pending
.vertices
)
499 table.insert(drawing
.pending
.vertices
, j
)
500 elseif chord
== 'C-o' and not App
.mouse_down(1) then
501 State
.current_drawing_mode
= 'circle'
502 elseif App
.mouse_down(1) and chord
== 'a' and State
.current_drawing_mode
== 'circle' then
503 local drawing_index
,drawing
= Drawing
.current_drawing(State
)
504 local starty
= Text
.starty(State
, drawing_index
)
505 drawing
.pending
.mode
= 'arc'
506 local mx
,my
= Drawing
.coord(App
.mouse_x()-State
.left
, State
.width
), Drawing
.coord(App
.mouse_y()-starty
, State
.width
)
507 local center
= drawing
.points
[drawing
.pending
.center
]
508 drawing
.pending
.radius
= round(geom
.dist(center
.x
,center
.y
, mx
,my
))
509 drawing
.pending
.start_angle
= geom
.angle(center
.x
,center
.y
, mx
,my
)
510 elseif App
.mouse_down(1) and chord
== 'o' then
511 State
.current_drawing_mode
= 'circle'
512 local _
,drawing
= Drawing
.current_drawing(State
)
513 if drawing
.pending
.mode
== 'freehand' then
514 drawing
.pending
.center
= Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)
515 elseif drawing
.pending
.mode
== 'line' or drawing
.pending
.mode
== 'manhattan' then
516 drawing
.pending
.center
= drawing
.pending
.p1
517 elseif drawing
.pending
.mode
== 'polygon' or drawing
.pending
.mode
== 'rectangle' or drawing
.pending
.mode
== 'square' then
518 drawing
.pending
.center
= drawing
.pending
.vertices
[1]
520 drawing
.pending
.mode
= 'circle'
521 elseif chord
== 'C-u' and not App
.mouse_down(1) then
522 local drawing_index
,drawing
,_
,i
,p
= Drawing
.select_point_at_mouse(State
)
524 if State
.previous_drawing_mode
== nil then
525 State
.previous_drawing_mode
= State
.current_drawing_mode
527 State
.current_drawing_mode
= 'move'
528 drawing
.pending
= {mode
=State
.current_drawing_mode
, target_point
=p
, target_point_index
=i
}
529 State
.lines
.current_drawing_index
= drawing_index
530 State
.lines
.current_drawing
= drawing
532 elseif chord
== 'C-n' and not App
.mouse_down(1) then
533 local drawing_index
,drawing
,_
,point_index
,p
= Drawing
.select_point_at_mouse(State
)
535 if State
.previous_drawing_mode
== nil then
537 State
.previous_drawing_mode
= State
.current_drawing_mode
539 State
.current_drawing_mode
= 'name'
541 drawing
.pending
= {mode
=State
.current_drawing_mode
, target_point
=point_index
}
542 State
.lines
.current_drawing_index
= drawing_index
543 State
.lines
.current_drawing
= drawing
545 elseif chord
== 'C-d' and not App
.mouse_down(1) then
546 local _
,drawing
,_
,i
,p
= Drawing
.select_point_at_mouse(State
)
548 for _
,shape
in ipairs(drawing
.shapes
) do
549 if Drawing
.contains_point(shape
, i
) then
550 if shape
.mode
== 'polygon' then
551 local idx
= table.find(shape
.vertices
, i
)
552 assert(idx
, 'point to delete is not in vertices')
553 table.remove(shape
.vertices
, idx
)
554 if #shape
.vertices
< 3 then
555 shape
.mode
= 'deleted'
558 shape
.mode
= 'deleted'
562 drawing
.points
[i
].deleted
= true
564 local drawing
,_
,_
,shape
= Drawing
.select_shape_at_mouse(State
)
566 shape
.mode
= 'deleted'
568 elseif chord
== 'C-h' and not App
.mouse_down(1) then
569 local drawing
= Drawing
.select_drawing_at_mouse(State
)
571 drawing
.show_help
= true
573 elseif chord
== 'escape' and App
.mouse_down(1) then
574 local _
,drawing
= Drawing
.current_drawing(State
)
579 function Drawing
.complete_rectangle(firstx
,firsty
, secondx
,secondy
, x
,y
)
580 if firstx
== secondx
then
581 return x
,secondy
, x
,firsty
583 if firsty
== secondy
then
584 return secondx
,y
, firstx
,y
586 local first_slope
= (secondy
-firsty
)/(secondx
-firstx
)
587 -- slope of second edge:
589 -- equation of line containing the second edge:
590 -- y-secondy = -1/first_slope*(x-secondx)
591 -- => 1/first_slope*x + y + (- secondy - secondx/first_slope) = 0
592 -- now we want to find the point on this line that's closest to the mouse pointer.
593 -- https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_an_equation
594 local a
= 1/first_slope
595 local c
= -secondy
- secondx
/first_slope
596 local thirdx
= round(((x
-a
*y
) - a
*c
) / (a
*a
+ 1))
597 local thirdy
= round((a
*(-x
+ a
*y
) - c
) / (a
*a
+ 1))
598 -- slope of third edge = first_slope
599 -- equation of line containing third edge:
600 -- y - thirdy = first_slope*(x-thirdx)
601 -- => -first_slope*x + y + (-thirdy + thirdx*first_slope) = 0
602 -- now we want to find the point on this line that's closest to the first point
603 local a
= -first_slope
604 local c
= -thirdy
+ thirdx
*first_slope
605 local fourthx
= round(((firstx
-a
*firsty
) - a
*c
) / (a
*a
+ 1))
606 local fourthy
= round((a
*(-firstx
+ a
*firsty
) - c
) / (a
*a
+ 1))
607 return thirdx
,thirdy
, fourthx
,fourthy
610 function Drawing
.complete_square(firstx
,firsty
, secondx
,secondy
, x
,y
)
611 -- use x,y only to decide which side of the first edge to complete the square on
612 local deltax
= secondx
-firstx
613 local deltay
= secondy
-firsty
614 local thirdx
= secondx
+deltay
615 local thirdy
= secondy
-deltax
616 if not geom
.same_side(firstx
,firsty
, secondx
,secondy
, thirdx
,thirdy
, x
,y
) then
619 thirdx
= secondx
+deltay
620 thirdy
= secondy
-deltax
622 local fourthx
= firstx
+deltay
623 local fourthy
= firsty
-deltax
624 return thirdx
,thirdy
, fourthx
,fourthy
627 function Drawing
.current_drawing(State
)
628 local x
, y
= App
.mouse_x(), App
.mouse_y()
629 for drawing_index
,drawing
in ipairs(State
.lines
) do
630 if drawing
.mode
== 'drawing' then
631 if Drawing
.in_drawing(State
, drawing_index
, x
,y
, State
.left
,State
.right
) then
632 return drawing_index
,drawing
639 function Drawing
.select_shape_at_mouse(State
)
640 for drawing_index
,drawing
in ipairs(State
.lines
) do
641 if drawing
.mode
== 'drawing' then
642 local x
, y
= App
.mouse_x(), App
.mouse_y()
643 local starty
= Text
.starty(State
, drawing_index
)
644 if Drawing
.in_drawing(State
, drawing_index
, x
,y
, State
.left
,State
.right
) then
645 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-starty
, State
.width
)
646 for i
,shape
in ipairs(drawing
.shapes
) do
647 if geom
.on_shape(mx
,my
, drawing
, shape
) then
648 return drawing
,starty
,i
,shape
656 function Drawing
.select_point_at_mouse(State
)
657 for drawing_index
,drawing
in ipairs(State
.lines
) do
658 if drawing
.mode
== 'drawing' then
659 local x
, y
= App
.mouse_x(), App
.mouse_y()
660 local starty
= Text
.starty(State
, drawing_index
)
661 if Drawing
.in_drawing(State
, drawing_index
, x
,y
, State
.left
,State
.right
) then
662 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-starty
, State
.width
)
663 for i
,point
in ipairs(drawing
.points
) do
664 if Drawing
.near(point
, mx
,my
, State
.width
) then
665 return drawing_index
,drawing
,starty
,i
,point
673 function Drawing
.select_drawing_at_mouse(State
)
674 for drawing_index
,drawing
in ipairs(State
.lines
) do
675 if drawing
.mode
== 'drawing' then
676 local x
, y
= App
.mouse_x(), App
.mouse_y()
677 if Drawing
.in_drawing(State
, drawing_index
, x
,y
, State
.left
,State
.right
) then
684 function Drawing
.contains_point(shape
, p
)
685 if shape
.mode
== 'freehand' then
687 elseif shape
.mode
== 'line' or shape
.mode
== 'manhattan' then
688 return shape
.p1
== p
or shape
.p2
== p
689 elseif shape
.mode
== 'polygon' or shape
.mode
== 'rectangle' or shape
.mode
== 'square' then
690 return table.find(shape
.vertices
, p
)
691 elseif shape
.mode
== 'circle' then
692 return shape
.center
== p
693 elseif shape
.mode
== 'arc' then
694 return shape
.center
== p
695 -- ugh, how to support angles
696 elseif shape
.mode
== 'deleted' then
699 assert(false, ('unknown drawing mode %s'):format(shape
.mode
))
703 function Drawing
.smoothen(shape
)
704 assert(shape
.mode
== 'freehand', 'can only smoothen freehand shapes')
706 for i
=2,#shape
.points
-1 do
707 local a
= shape
.points
[i
-1]
708 local b
= shape
.points
[i
]
709 local c
= shape
.points
[i
+1]
710 b
.x
= round((a
.x
+ b
.x
+ c
.x
)/3)
711 b
.y
= round((a
.y
+ b
.y
+ c
.y
)/3)
717 return math
.floor(num
+.5)
720 function Drawing
.find_or_insert_point(points
, x
,y
, width
)
721 -- check if UI would snap the two points together
722 for i
,point
in ipairs(points
) do
723 if Drawing
.near(point
, x
,y
, width
) then
727 table.insert(points
, {x
=x
, y
=y
})
731 function Drawing
.near(point
, x
,y
, width
)
732 local px
,py
= Drawing
.pixels(x
, width
),Drawing
.pixels(y
, width
)
733 local cx
,cy
= Drawing
.pixels(point
.x
, width
), Drawing
.pixels(point
.y
, width
)
734 return (cx
-px
)*(cx
-px
) + (cy
-py
)*(cy
-py
) < Same_point_distance
*Same_point_distance
737 function Drawing
.pixels(n
, width
) -- parts to pixels
738 return math
.floor(n
*width
/256)
740 function Drawing
.coord(n
, width
) -- pixels to parts
741 return math
.floor(n
*256/width
)
744 function table.find(h
, x
)
745 for k
,v
in pairs(h
) do