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
37 if geom
.on_shape(mx
,my
, line
, shape
) then
38 App
.color(Focus_stroke_color
)
40 App
.color(Stroke_color
)
42 Drawing
.draw_shape(line
, shape
, line_cache
.starty
, State
.left
,State
.right
)
45 local function px(x
) return Drawing
.pixels(x
, State
.width
)+State
.left
end
46 local function py(y
) return Drawing
.pixels(y
, State
.width
)+line_cache
.starty
end
47 for i
,p
in ipairs(line
.points
) do
48 if p
.deleted
== nil then
49 if Drawing
.near(p
, mx
,my
, State
.width
) then
50 App
.color(Focus_stroke_color
)
51 love
.graphics
.circle('line', px(p
.x
),py(p
.y
), Same_point_distance
)
53 App
.color(Stroke_color
)
54 love
.graphics
.circle('fill', px(p
.x
),py(p
.y
), 2)
58 local x
,y
= px(p
.x
)+5, py(p
.y
)+5
59 love
.graphics
.print(p
.name
, x
,y
)
60 if State
.current_drawing_mode
== 'name' and i
== line
.pending
.target_point
then
61 -- create a faint red box for the name
62 App
.color(Current_name_background_color
)
65 name_width
= App
.width('m')
67 name_width
= App
.width(p
.name
)
69 love
.graphics
.rectangle('fill', x
,y
, name_width
, State
.line_height
)
74 App
.color(Current_stroke_color
)
75 Drawing
.draw_pending_shape(line
, line_cache
.starty
, State
.left
,State
.right
)
78 function Drawing
.draw_shape(drawing
, shape
, top
, left
,right
)
79 local width
= right
-left
80 local function px(x
) return Drawing
.pixels(x
, width
)+left
end
81 local function py(y
) return Drawing
.pixels(y
, width
)+top
end
82 if shape
.mode
== 'freehand' then
84 for _
,point
in ipairs(shape
.points
) do
86 love
.graphics
.line(px(prev
.x
),py(prev
.y
), px(point
.x
),py(point
.y
))
90 elseif shape
.mode
== 'line' or shape
.mode
== 'manhattan' then
91 local p1
= drawing
.points
[shape
.p1
]
92 local p2
= drawing
.points
[shape
.p2
]
93 love
.graphics
.line(px(p1
.x
),py(p1
.y
), px(p2
.x
),py(p2
.y
))
94 elseif shape
.mode
== 'polygon' or shape
.mode
== 'rectangle' or shape
.mode
== 'square' then
96 for _
,point
in ipairs(shape
.vertices
) do
97 local curr
= drawing
.points
[point
]
99 love
.graphics
.line(px(prev
.x
),py(prev
.y
), px(curr
.x
),py(curr
.y
))
104 local curr
= drawing
.points
[shape
.vertices
[1]]
105 love
.graphics
.line(px(prev
.x
),py(prev
.y
), px(curr
.x
),py(curr
.y
))
106 elseif shape
.mode
== 'circle' then
108 local center
= drawing
.points
[shape
.center
]
109 love
.graphics
.circle('line', px(center
.x
),py(center
.y
), Drawing
.pixels(shape
.radius
, width
))
110 elseif shape
.mode
== 'arc' then
111 local center
= drawing
.points
[shape
.center
]
112 love
.graphics
.arc('line', 'open', px(center
.x
),py(center
.y
), Drawing
.pixels(shape
.radius
, width
), shape
.start_angle
, shape
.end_angle
, 360)
113 elseif shape
.mode
== 'deleted' then
121 function Drawing
.draw_pending_shape(drawing
, top
, left
,right
)
122 local width
= right
-left
123 local pmx
,pmy
= App
.mouse_x(), App
.mouse_y()
124 local function px(x
) return Drawing
.pixels(x
, width
)+left
end
125 local function py(y
) return Drawing
.pixels(y
, width
)+top
end
126 local mx
= Drawing
.coord(pmx
-left
, width
)
127 local my
= Drawing
.coord(pmy
-top
, width
)
128 -- recreate pixels from coords to precisely mimic how the drawing will look
129 -- after mouse_release
130 pmx
,pmy
= px(mx
), py(my
)
131 local shape
= drawing
.pending
132 if shape
.mode
== nil then
134 elseif shape
.mode
== 'freehand' then
135 local shape_copy
= deepcopy(shape
)
136 Drawing
.smoothen(shape_copy
)
137 Drawing
.draw_shape(drawing
, shape_copy
, top
, left
,right
)
138 elseif shape
.mode
== 'line' then
139 if mx
< 0 or mx
>= 256 or my
< 0 or my
>= drawing
.h
then
142 local p1
= drawing
.points
[shape
.p1
]
143 love
.graphics
.line(px(p1
.x
),py(p1
.y
), pmx
,pmy
)
144 elseif shape
.mode
== 'manhattan' then
145 if mx
< 0 or mx
>= 256 or my
< 0 or my
>= drawing
.h
then
148 local p1
= drawing
.points
[shape
.p1
]
149 if math
.abs(mx
-p1
.x
) > math
.abs(my
-p1
.y
) then
150 love
.graphics
.line(px(p1
.x
),py(p1
.y
), pmx
, py(p1
.y
))
152 love
.graphics
.line(px(p1
.x
),py(p1
.y
), px(p1
.x
),pmy
)
154 elseif shape
.mode
== 'polygon' then
155 -- don't close the loop on a pending polygon
157 for _
,point
in ipairs(shape
.vertices
) do
158 local curr
= drawing
.points
[point
]
160 love
.graphics
.line(px(prev
.x
),py(prev
.y
), px(curr
.x
),py(curr
.y
))
164 love
.graphics
.line(px(prev
.x
),py(prev
.y
), pmx
,pmy
)
165 elseif shape
.mode
== 'rectangle' then
166 local first
= drawing
.points
[shape
.vertices
[1]]
167 if #shape
.vertices
== 1 then
168 love
.graphics
.line(px(first
.x
),py(first
.y
), pmx
,pmy
)
171 local second
= drawing
.points
[shape
.vertices
[2]]
172 local thirdx
,thirdy
, fourthx
,fourthy
= Drawing
.complete_rectangle(first
.x
,first
.y
, second
.x
,second
.y
, mx
,my
)
173 love
.graphics
.line(px(first
.x
),py(first
.y
), px(second
.x
),py(second
.y
))
174 love
.graphics
.line(px(second
.x
),py(second
.y
), px(thirdx
),py(thirdy
))
175 love
.graphics
.line(px(thirdx
),py(thirdy
), px(fourthx
),py(fourthy
))
176 love
.graphics
.line(px(fourthx
),py(fourthy
), px(first
.x
),py(first
.y
))
177 elseif shape
.mode
== 'square' then
178 local first
= drawing
.points
[shape
.vertices
[1]]
179 if #shape
.vertices
== 1 then
180 love
.graphics
.line(px(first
.x
),py(first
.y
), pmx
,pmy
)
183 local second
= drawing
.points
[shape
.vertices
[2]]
184 local thirdx
,thirdy
, fourthx
,fourthy
= Drawing
.complete_square(first
.x
,first
.y
, second
.x
,second
.y
, mx
,my
)
185 love
.graphics
.line(px(first
.x
),py(first
.y
), px(second
.x
),py(second
.y
))
186 love
.graphics
.line(px(second
.x
),py(second
.y
), px(thirdx
),py(thirdy
))
187 love
.graphics
.line(px(thirdx
),py(thirdy
), px(fourthx
),py(fourthy
))
188 love
.graphics
.line(px(fourthx
),py(fourthy
), px(first
.x
),py(first
.y
))
189 elseif shape
.mode
== 'circle' then
190 local center
= drawing
.points
[shape
.center
]
191 if mx
< 0 or mx
>= 256 or my
< 0 or my
>= drawing
.h
then
194 local r
= round(geom
.dist(center
.x
, center
.y
, mx
, my
))
195 local cx
,cy
= px(center
.x
), py(center
.y
)
196 love
.graphics
.circle('line', cx
,cy
, Drawing
.pixels(r
, width
))
197 elseif shape
.mode
== 'arc' then
198 local center
= drawing
.points
[shape
.center
]
199 if mx
< 0 or mx
>= 256 or my
< 0 or my
>= drawing
.h
then
202 shape
.end_angle
= geom
.angle_with_hint(center
.x
,center
.y
, mx
,my
, shape
.end_angle
)
203 local cx
,cy
= px(center
.x
), py(center
.y
)
204 love
.graphics
.arc('line', 'open', cx
,cy
, Drawing
.pixels(shape
.radius
, width
), shape
.start_angle
, shape
.end_angle
, 360)
205 elseif shape
.mode
== 'move' then
206 -- nothing pending; changes are immediately committed
207 elseif shape
.mode
== 'name' then
208 -- nothing pending; changes are immediately committed
215 function Drawing
.in_drawing(drawing
, line_cache
, x
,y
, left
,right
)
216 if line_cache
.starty
== nil then return false end -- outside current page
217 local width
= right
-left
218 return y
>= line_cache
.starty
and y
< line_cache
.starty
+ Drawing
.pixels(drawing
.h
, width
) and x
>= left
and x
< right
221 function Drawing
.mouse_press(State
, drawing_index
, x
,y
, mouse_button
)
222 local drawing
= State
.lines
[drawing_index
]
223 local line_cache
= State
.line_cache
[drawing_index
]
224 local cx
= Drawing
.coord(x
-State
.left
, State
.width
)
225 local cy
= Drawing
.coord(y
-line_cache
.starty
, State
.width
)
226 if State
.current_drawing_mode
== 'freehand' then
227 drawing
.pending
= {mode
=State
.current_drawing_mode
, points
={{x
=cx
, y
=cy
}}}
228 elseif State
.current_drawing_mode
== 'line' or State
.current_drawing_mode
== 'manhattan' then
229 local j
= Drawing
.find_or_insert_point(drawing
.points
, cx
, cy
, State
.width
)
230 drawing
.pending
= {mode
=State
.current_drawing_mode
, p1
=j
}
231 elseif State
.current_drawing_mode
== 'polygon' or State
.current_drawing_mode
== 'rectangle' or State
.current_drawing_mode
== 'square' then
232 local j
= Drawing
.find_or_insert_point(drawing
.points
, cx
, cy
, State
.width
)
233 drawing
.pending
= {mode
=State
.current_drawing_mode
, vertices
={j
}}
234 elseif State
.current_drawing_mode
== 'circle' then
235 local j
= Drawing
.find_or_insert_point(drawing
.points
, cx
, cy
, State
.width
)
236 drawing
.pending
= {mode
=State
.current_drawing_mode
, center
=j
}
237 elseif State
.current_drawing_mode
== 'move' then
238 -- all the action is in mouse_release
239 elseif State
.current_drawing_mode
== 'name' then
242 print(State
.current_drawing_mode
)
247 -- a couple of operations on drawings need to constantly check the state of the mouse
248 function Drawing
.update(State
)
249 if State
.lines
.current_drawing
== nil then return end
250 local drawing
= State
.lines
.current_drawing
251 local line_cache
= State
.line_cache
[State
.lines
.current_drawing_index
]
252 if line_cache
.starty
== nil then
253 -- some event cleared starty just this frame
254 -- draw in this frame will soon set starty
255 -- just skip this frame
258 assert(drawing
.mode
== 'drawing')
259 local pmx
, pmy
= App
.mouse_x(), App
.mouse_y()
260 local mx
= Drawing
.coord(pmx
-State
.left
, State
.width
)
261 local my
= Drawing
.coord(pmy
-line_cache
.starty
, State
.width
)
262 if App
.mouse_down(1) then
263 if Drawing
.in_drawing(drawing
, line_cache
, pmx
,pmy
, State
.left
,State
.right
) then
264 if drawing
.pending
.mode
== 'freehand' then
265 table.insert(drawing
.pending
.points
, {x
=mx
, y
=my
})
266 elseif drawing
.pending
.mode
== 'move' then
267 drawing
.pending
.target_point
.x
= mx
268 drawing
.pending
.target_point
.y
= my
269 Drawing
.relax_constraints(drawing
, drawing
.pending
.target_point_index
)
272 elseif State
.current_drawing_mode
== 'move' then
273 if Drawing
.in_drawing(drawing
, line_cache
, pmx
, pmy
, State
.left
,State
.right
) then
274 drawing
.pending
.target_point
.x
= mx
275 drawing
.pending
.target_point
.y
= my
276 Drawing
.relax_constraints(drawing
, drawing
.pending
.target_point_index
)
283 function Drawing
.relax_constraints(drawing
, p
)
284 for _
,shape
in ipairs(drawing
.shapes
) do
285 if shape
.mode
== 'manhattan' then
286 if shape
.p1
== p
then
288 elseif shape
.p2
== p
then
291 elseif shape
.mode
== 'rectangle' or shape
.mode
== 'square' then
292 for _
,v
in ipairs(shape
.vertices
) do
294 shape
.mode
= 'polygon'
301 function Drawing
.mouse_release(State
, x
,y
, mouse_button
)
302 if State
.current_drawing_mode
== 'move' then
303 State
.current_drawing_mode
= State
.previous_drawing_mode
304 State
.previous_drawing_mode
= nil
305 if State
.lines
.current_drawing
then
306 State
.lines
.current_drawing
.pending
= {}
307 State
.lines
.current_drawing
= nil
309 elseif State
.lines
.current_drawing
then
310 local drawing
= State
.lines
.current_drawing
311 local line_cache
= State
.line_cache
[State
.lines
.current_drawing_index
]
312 if drawing
.pending
then
313 if drawing
.pending
.mode
== nil then
315 elseif drawing
.pending
.mode
== 'freehand' then
316 -- the last point added during update is good enough
317 Drawing
.smoothen(drawing
.pending
)
318 table.insert(drawing
.shapes
, drawing
.pending
)
319 elseif drawing
.pending
.mode
== 'line' then
320 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
321 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
322 drawing
.pending
.p2
= Drawing
.find_or_insert_point(drawing
.points
, mx
,my
, State
.width
)
323 table.insert(drawing
.shapes
, drawing
.pending
)
325 elseif drawing
.pending
.mode
== 'manhattan' then
326 local p1
= drawing
.points
[drawing
.pending
.p1
]
327 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
328 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
329 if math
.abs(mx
-p1
.x
) > math
.abs(my
-p1
.y
) then
330 drawing
.pending
.p2
= Drawing
.find_or_insert_point(drawing
.points
, mx
, p1
.y
, State
.width
)
332 drawing
.pending
.p2
= Drawing
.find_or_insert_point(drawing
.points
, p1
.x
, my
, State
.width
)
334 local p2
= drawing
.points
[drawing
.pending
.p2
]
335 App
.mouse_move(State
.left
+Drawing
.pixels(p2
.x
, State
.width
), line_cache
.starty
+Drawing
.pixels(p2
.y
, State
.width
))
336 table.insert(drawing
.shapes
, drawing
.pending
)
338 elseif drawing
.pending
.mode
== 'polygon' then
339 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
340 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
341 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, mx
,my
, State
.width
))
342 table.insert(drawing
.shapes
, drawing
.pending
)
344 elseif drawing
.pending
.mode
== 'rectangle' then
345 assert(#drawing
.pending
.vertices
<= 2)
346 if #drawing
.pending
.vertices
== 2 then
347 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
348 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
349 local first
= drawing
.points
[drawing
.pending
.vertices
[1]]
350 local second
= drawing
.points
[drawing
.pending
.vertices
[2]]
351 local thirdx
,thirdy
, fourthx
,fourthy
= Drawing
.complete_rectangle(first
.x
,first
.y
, second
.x
,second
.y
, mx
,my
)
352 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, thirdx
,thirdy
, State
.width
))
353 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, fourthx
,fourthy
, State
.width
))
354 table.insert(drawing
.shapes
, drawing
.pending
)
357 -- too few points; draw nothing
359 elseif drawing
.pending
.mode
== 'square' then
360 assert(#drawing
.pending
.vertices
<= 2)
361 if #drawing
.pending
.vertices
== 2 then
362 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
363 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
364 local first
= drawing
.points
[drawing
.pending
.vertices
[1]]
365 local second
= drawing
.points
[drawing
.pending
.vertices
[2]]
366 local thirdx
,thirdy
, fourthx
,fourthy
= Drawing
.complete_square(first
.x
,first
.y
, second
.x
,second
.y
, mx
,my
)
367 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, thirdx
,thirdy
, State
.width
))
368 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, fourthx
,fourthy
, State
.width
))
369 table.insert(drawing
.shapes
, drawing
.pending
)
372 elseif drawing
.pending
.mode
== 'circle' then
373 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
374 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
375 local center
= drawing
.points
[drawing
.pending
.center
]
376 drawing
.pending
.radius
= round(geom
.dist(center
.x
,center
.y
, mx
,my
))
377 table.insert(drawing
.shapes
, drawing
.pending
)
379 elseif drawing
.pending
.mode
== 'arc' then
380 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
381 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
382 local center
= drawing
.points
[drawing
.pending
.center
]
383 drawing
.pending
.end_angle
= geom
.angle_with_hint(center
.x
,center
.y
, mx
,my
, drawing
.pending
.end_angle
)
384 table.insert(drawing
.shapes
, drawing
.pending
)
386 elseif drawing
.pending
.mode
== 'name' then
389 print(drawing
.pending
.mode
)
392 State
.lines
.current_drawing
.pending
= {}
393 State
.lines
.current_drawing
= nil
398 function Drawing
.keychord_press(State
, chord
)
399 if chord
== 'C-p' and not App
.mouse_down(1) then
400 State
.current_drawing_mode
= 'freehand'
401 elseif App
.mouse_down(1) and chord
== 'l' then
402 State
.current_drawing_mode
= 'line'
403 local _
,drawing
= Drawing
.current_drawing(State
)
404 if drawing
.pending
.mode
== 'freehand' then
405 drawing
.pending
.p1
= Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)
406 elseif drawing
.pending
.mode
== 'polygon' or drawing
.pending
.mode
== 'rectangle' or drawing
.pending
.mode
== 'square' then
407 drawing
.pending
.p1
= drawing
.pending
.vertices
[1]
408 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
409 drawing
.pending
.p1
= drawing
.pending
.center
411 drawing
.pending
.mode
= 'line'
412 elseif chord
== 'C-l' and not App
.mouse_down(1) then
413 State
.current_drawing_mode
= 'line'
414 elseif App
.mouse_down(1) and chord
== 'm' then
415 State
.current_drawing_mode
= 'manhattan'
416 local drawing
= Drawing
.select_drawing_at_mouse(State
)
417 if drawing
.pending
.mode
== 'freehand' then
418 drawing
.pending
.p1
= Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)
419 elseif drawing
.pending
.mode
== 'line' then
421 elseif drawing
.pending
.mode
== 'polygon' or drawing
.pending
.mode
== 'rectangle' or drawing
.pending
.mode
== 'square' then
422 drawing
.pending
.p1
= drawing
.pending
.vertices
[1]
423 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
424 drawing
.pending
.p1
= drawing
.pending
.center
426 drawing
.pending
.mode
= 'manhattan'
427 elseif chord
== 'C-m' and not App
.mouse_down(1) then
428 State
.current_drawing_mode
= 'manhattan'
429 elseif chord
== 'C-g' and not App
.mouse_down(1) then
430 State
.current_drawing_mode
= 'polygon'
431 elseif App
.mouse_down(1) and chord
== 'g' then
432 State
.current_drawing_mode
= 'polygon'
433 local _
,drawing
= Drawing
.current_drawing(State
)
434 if drawing
.pending
.mode
== 'freehand' then
435 drawing
.pending
.vertices
= {Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)}
436 elseif drawing
.pending
.mode
== 'line' or drawing
.pending
.mode
== 'manhattan' then
437 if drawing
.pending
.vertices
== nil then
438 drawing
.pending
.vertices
= {drawing
.pending
.p1
}
440 elseif drawing
.pending
.mode
== 'rectangle' or drawing
.pending
.mode
== 'square' then
441 -- reuse existing vertices
442 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
443 drawing
.pending
.vertices
= {drawing
.pending
.center
}
445 drawing
.pending
.mode
= 'polygon'
446 elseif chord
== 'C-r' and not App
.mouse_down(1) then
447 State
.current_drawing_mode
= 'rectangle'
448 elseif App
.mouse_down(1) and chord
== 'r' then
449 State
.current_drawing_mode
= 'rectangle'
450 local _
,drawing
= Drawing
.current_drawing(State
)
451 if drawing
.pending
.mode
== 'freehand' then
452 drawing
.pending
.vertices
= {Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)}
453 elseif drawing
.pending
.mode
== 'line' or drawing
.pending
.mode
== 'manhattan' then
454 if drawing
.pending
.vertices
== nil then
455 drawing
.pending
.vertices
= {drawing
.pending
.p1
}
457 elseif drawing
.pending
.mode
== 'polygon' or drawing
.pending
.mode
== 'square' then
458 -- reuse existing (1-2) vertices
459 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
460 drawing
.pending
.vertices
= {drawing
.pending
.center
}
462 drawing
.pending
.mode
= 'rectangle'
463 elseif chord
== 'C-s' and not App
.mouse_down(1) then
464 State
.current_drawing_mode
= 'square'
465 elseif App
.mouse_down(1) and chord
== 's' then
466 State
.current_drawing_mode
= 'square'
467 local _
,drawing
= Drawing
.current_drawing(State
)
468 if drawing
.pending
.mode
== 'freehand' then
469 drawing
.pending
.vertices
= {Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)}
470 elseif drawing
.pending
.mode
== 'line' or drawing
.pending
.mode
== 'manhattan' then
471 if drawing
.pending
.vertices
== nil then
472 drawing
.pending
.vertices
= {drawing
.pending
.p1
}
474 elseif drawing
.pending
.mode
== 'polygon' then
475 while #drawing
.pending
.vertices
> 2 do
476 table.remove(drawing
.pending
.vertices
)
478 elseif drawing
.pending
.mode
== 'rectangle' then
479 -- reuse existing (1-2) vertices
480 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
481 drawing
.pending
.vertices
= {drawing
.pending
.center
}
483 drawing
.pending
.mode
= 'square'
484 elseif App
.mouse_down(1) and chord
== 'p' and State
.current_drawing_mode
== 'polygon' 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 table.insert(drawing
.pending
.vertices
, j
)
489 elseif App
.mouse_down(1) and chord
== 'p' and (State
.current_drawing_mode
== 'rectangle' or State
.current_drawing_mode
== 'square') then
490 local _
,drawing
,line_cache
= Drawing
.current_drawing(State
)
491 local mx
,my
= Drawing
.coord(App
.mouse_x()-State
.left
, State
.width
), Drawing
.coord(App
.mouse_y()-line_cache
.starty
, State
.width
)
492 local j
= Drawing
.find_or_insert_point(drawing
.points
, mx
,my
, State
.width
)
493 while #drawing
.pending
.vertices
>= 2 do
494 table.remove(drawing
.pending
.vertices
)
496 table.insert(drawing
.pending
.vertices
, j
)
497 elseif chord
== 'C-o' and not App
.mouse_down(1) then
498 State
.current_drawing_mode
= 'circle'
499 elseif App
.mouse_down(1) and chord
== 'a' and State
.current_drawing_mode
== 'circle' then
500 local _
,drawing
,line_cache
= Drawing
.current_drawing(State
)
501 drawing
.pending
.mode
= 'arc'
502 local mx
,my
= Drawing
.coord(App
.mouse_x()-State
.left
, State
.width
), Drawing
.coord(App
.mouse_y()-line_cache
.starty
, State
.width
)
503 local center
= drawing
.points
[drawing
.pending
.center
]
504 drawing
.pending
.radius
= round(geom
.dist(center
.x
,center
.y
, mx
,my
))
505 drawing
.pending
.start_angle
= geom
.angle(center
.x
,center
.y
, mx
,my
)
506 elseif App
.mouse_down(1) and chord
== 'o' then
507 State
.current_drawing_mode
= 'circle'
508 local _
,drawing
= Drawing
.current_drawing(State
)
509 if drawing
.pending
.mode
== 'freehand' then
510 drawing
.pending
.center
= Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)
511 elseif drawing
.pending
.mode
== 'line' or drawing
.pending
.mode
== 'manhattan' then
512 drawing
.pending
.center
= drawing
.pending
.p1
513 elseif drawing
.pending
.mode
== 'polygon' or drawing
.pending
.mode
== 'rectangle' or drawing
.pending
.mode
== 'square' then
514 drawing
.pending
.center
= drawing
.pending
.vertices
[1]
516 drawing
.pending
.mode
= 'circle'
517 elseif chord
== 'C-u' and not App
.mouse_down(1) then
518 local drawing_index
,drawing
,line_cache
,i
,p
= Drawing
.select_point_at_mouse(State
)
520 if State
.previous_drawing_mode
== nil then
521 State
.previous_drawing_mode
= State
.current_drawing_mode
523 State
.current_drawing_mode
= 'move'
524 drawing
.pending
= {mode
=State
.current_drawing_mode
, target_point
=p
, target_point_index
=i
}
525 State
.lines
.current_drawing_index
= drawing_index
526 State
.lines
.current_drawing
= drawing
528 elseif chord
== 'C-n' and not App
.mouse_down(1) then
529 local drawing_index
,drawing
,line_cache
,point_index
,p
= Drawing
.select_point_at_mouse(State
)
531 if State
.previous_drawing_mode
== nil then
533 State
.previous_drawing_mode
= State
.current_drawing_mode
535 State
.current_drawing_mode
= 'name'
537 drawing
.pending
= {mode
=State
.current_drawing_mode
, target_point
=point_index
}
538 State
.lines
.current_drawing_index
= drawing_index
539 State
.lines
.current_drawing
= drawing
541 elseif chord
== 'C-d' and not App
.mouse_down(1) then
542 local _
,drawing
,_
,i
,p
= Drawing
.select_point_at_mouse(State
)
544 for _
,shape
in ipairs(drawing
.shapes
) do
545 if Drawing
.contains_point(shape
, i
) then
546 if shape
.mode
== 'polygon' then
547 local idx
= table.find(shape
.vertices
, i
)
549 table.remove(shape
.vertices
, idx
)
550 if #shape
.vertices
< 3 then
551 shape
.mode
= 'deleted'
554 shape
.mode
= 'deleted'
558 drawing
.points
[i
].deleted
= true
560 local drawing
,_
,_
,shape
= Drawing
.select_shape_at_mouse(State
)
562 shape
.mode
= 'deleted'
564 elseif chord
== 'C-h' and not App
.mouse_down(1) then
565 local drawing
= Drawing
.select_drawing_at_mouse(State
)
567 drawing
.show_help
= true
569 elseif chord
== 'escape' and App
.mouse_down(1) then
570 local _
,drawing
= Drawing
.current_drawing(State
)
575 function Drawing
.complete_rectangle(firstx
,firsty
, secondx
,secondy
, x
,y
)
576 if firstx
== secondx
then
577 return x
,secondy
, x
,firsty
579 if firsty
== secondy
then
580 return secondx
,y
, firstx
,y
582 local first_slope
= (secondy
-firsty
)/(secondx
-firstx
)
583 -- slope of second edge:
585 -- equation of line containing the second edge:
586 -- y-secondy = -1/first_slope*(x-secondx)
587 -- => 1/first_slope*x + y + (- secondy - secondx/first_slope) = 0
588 -- now we want to find the point on this line that's closest to the mouse pointer.
589 -- https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_an_equation
590 local a
= 1/first_slope
591 local c
= -secondy
- secondx
/first_slope
592 local thirdx
= round(((x
-a
*y
) - a
*c
) / (a
*a
+ 1))
593 local thirdy
= round((a
*(-x
+ a
*y
) - c
) / (a
*a
+ 1))
594 -- slope of third edge = first_slope
595 -- equation of line containing third edge:
596 -- y - thirdy = first_slope*(x-thirdx)
597 -- => -first_slope*x + y + (-thirdy + thirdx*first_slope) = 0
598 -- now we want to find the point on this line that's closest to the first point
599 local a
= -first_slope
600 local c
= -thirdy
+ thirdx
*first_slope
601 local fourthx
= round(((firstx
-a
*firsty
) - a
*c
) / (a
*a
+ 1))
602 local fourthy
= round((a
*(-firstx
+ a
*firsty
) - c
) / (a
*a
+ 1))
603 return thirdx
,thirdy
, fourthx
,fourthy
606 function Drawing
.complete_square(firstx
,firsty
, secondx
,secondy
, x
,y
)
607 -- use x,y only to decide which side of the first edge to complete the square on
608 local deltax
= secondx
-firstx
609 local deltay
= secondy
-firsty
610 local thirdx
= secondx
+deltay
611 local thirdy
= secondy
-deltax
612 if not geom
.same_side(firstx
,firsty
, secondx
,secondy
, thirdx
,thirdy
, x
,y
) then
615 thirdx
= secondx
+deltay
616 thirdy
= secondy
-deltax
618 local fourthx
= firstx
+deltay
619 local fourthy
= firsty
-deltax
620 return thirdx
,thirdy
, fourthx
,fourthy
623 function Drawing
.current_drawing(State
)
624 local x
, y
= App
.mouse_x(), App
.mouse_y()
625 for drawing_index
,drawing
in ipairs(State
.lines
) do
626 if drawing
.mode
== 'drawing' then
627 local line_cache
= State
.line_cache
[drawing_index
]
628 if Drawing
.in_drawing(drawing
, line_cache
, x
,y
, State
.left
,State
.right
) then
629 return drawing_index
,drawing
,line_cache
636 function Drawing
.select_shape_at_mouse(State
)
637 for drawing_index
,drawing
in ipairs(State
.lines
) do
638 if drawing
.mode
== 'drawing' then
639 local x
, y
= App
.mouse_x(), App
.mouse_y()
640 local line_cache
= State
.line_cache
[drawing_index
]
641 if Drawing
.in_drawing(drawing
, line_cache
, x
,y
, State
.left
,State
.right
) then
642 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
643 for i
,shape
in ipairs(drawing
.shapes
) do
645 if geom
.on_shape(mx
,my
, drawing
, shape
) then
646 return drawing
,line_cache
,i
,shape
654 function Drawing
.select_point_at_mouse(State
)
655 for drawing_index
,drawing
in ipairs(State
.lines
) do
656 if drawing
.mode
== 'drawing' then
657 local x
, y
= App
.mouse_x(), App
.mouse_y()
658 local line_cache
= State
.line_cache
[drawing_index
]
659 if Drawing
.in_drawing(drawing
, line_cache
, x
,y
, State
.left
,State
.right
) then
660 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
661 for i
,point
in ipairs(drawing
.points
) do
663 if Drawing
.near(point
, mx
,my
, State
.width
) then
664 return drawing_index
,drawing
,line_cache
,i
,point
672 function Drawing
.select_drawing_at_mouse(State
)
673 for drawing_index
,drawing
in ipairs(State
.lines
) do
674 if drawing
.mode
== 'drawing' then
675 local x
, y
= App
.mouse_x(), App
.mouse_y()
676 local line_cache
= State
.line_cache
[drawing_index
]
677 if Drawing
.in_drawing(drawing
, line_cache
, 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
704 function Drawing
.smoothen(shape
)
705 assert(shape
.mode
== 'freehand')
707 for i
=2,#shape
.points
-1 do
708 local a
= shape
.points
[i
-1]
709 local b
= shape
.points
[i
]
710 local c
= shape
.points
[i
+1]
711 b
.x
= round((a
.x
+ b
.x
+ c
.x
)/3)
712 b
.y
= round((a
.y
+ b
.y
+ c
.y
)/3)
718 return math
.floor(num
+.5)
721 function Drawing
.find_or_insert_point(points
, x
,y
, width
)
722 -- check if UI would snap the two points together
723 for i
,point
in ipairs(points
) do
724 if Drawing
.near(point
, x
,y
, width
) then
728 table.insert(points
, {x
=x
, y
=y
})
732 function Drawing
.near(point
, x
,y
, width
)
733 local px
,py
= Drawing
.pixels(x
, width
),Drawing
.pixels(y
, width
)
734 local cx
,cy
= Drawing
.pixels(point
.x
, width
), Drawing
.pixels(point
.y
, width
)
735 return (cx
-px
)*(cx
-px
) + (cy
-py
)*(cy
-py
) < Same_point_distance
*Same_point_distance
738 function Drawing
.pixels(n
, width
) -- parts to pixels
739 return math
.floor(n
*width
/256)
741 function Drawing
.coord(n
, width
) -- pixels to parts
742 return math
.floor(n
*256/width
)
745 function table.find(h
, x
)
746 for k
,v
in pairs(h
) do