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
)
64 -- TODO: avoid computing name width on every repaint
68 name_text
= App
.newText(love
.graphics
.getFont(), p
.name
)
70 love
.graphics
.rectangle('fill', x
,y
, App
.width(name_text
), State
.line_height
)
75 App
.color(Current_stroke_color
)
76 Drawing
.draw_pending_shape(line
, line_cache
.starty
, State
.left
,State
.right
)
79 function Drawing
.draw_shape(drawing
, shape
, top
, left
,right
)
80 local width
= right
-left
81 local function px(x
) return Drawing
.pixels(x
, width
)+left
end
82 local function py(y
) return Drawing
.pixels(y
, width
)+top
end
83 if shape
.mode
== 'freehand' then
85 for _
,point
in ipairs(shape
.points
) do
87 love
.graphics
.line(px(prev
.x
),py(prev
.y
), px(point
.x
),py(point
.y
))
91 elseif shape
.mode
== 'line' or shape
.mode
== 'manhattan' then
92 local p1
= drawing
.points
[shape
.p1
]
93 local p2
= drawing
.points
[shape
.p2
]
94 love
.graphics
.line(px(p1
.x
),py(p1
.y
), px(p2
.x
),py(p2
.y
))
95 elseif shape
.mode
== 'polygon' or shape
.mode
== 'rectangle' or shape
.mode
== 'square' then
97 for _
,point
in ipairs(shape
.vertices
) do
98 local curr
= drawing
.points
[point
]
100 love
.graphics
.line(px(prev
.x
),py(prev
.y
), px(curr
.x
),py(curr
.y
))
105 local curr
= drawing
.points
[shape
.vertices
[1]]
106 love
.graphics
.line(px(prev
.x
),py(prev
.y
), px(curr
.x
),py(curr
.y
))
107 elseif shape
.mode
== 'circle' then
109 local center
= drawing
.points
[shape
.center
]
110 love
.graphics
.circle('line', px(center
.x
),py(center
.y
), Drawing
.pixels(shape
.radius
, width
))
111 elseif shape
.mode
== 'arc' then
112 local center
= drawing
.points
[shape
.center
]
113 love
.graphics
.arc('line', 'open', px(center
.x
),py(center
.y
), Drawing
.pixels(shape
.radius
, width
), shape
.start_angle
, shape
.end_angle
, 360)
114 elseif shape
.mode
== 'deleted' then
122 function Drawing
.draw_pending_shape(drawing
, top
, left
,right
)
123 local width
= right
-left
124 local pmx
,pmy
= App
.mouse_x(), App
.mouse_y()
125 local function px(x
) return Drawing
.pixels(x
, width
)+left
end
126 local function py(y
) return Drawing
.pixels(y
, width
)+top
end
127 local mx
= Drawing
.coord(pmx
-left
, width
)
128 local my
= Drawing
.coord(pmy
-top
, width
)
129 -- recreate pixels from coords to precisely mimic how the drawing will look
130 -- after mouse_released
131 pmx
,pmy
= px(mx
), py(my
)
132 local shape
= drawing
.pending
133 if shape
.mode
== nil then
135 elseif shape
.mode
== 'freehand' then
136 local shape_copy
= deepcopy(shape
)
137 Drawing
.smoothen(shape_copy
)
138 Drawing
.draw_shape(drawing
, shape_copy
, top
, left
,right
)
139 elseif shape
.mode
== 'line' then
140 if mx
< 0 or mx
>= 256 or my
< 0 or my
>= drawing
.h
then
143 local p1
= drawing
.points
[shape
.p1
]
144 love
.graphics
.line(px(p1
.x
),py(p1
.y
), pmx
,pmy
)
145 elseif shape
.mode
== 'manhattan' then
146 if mx
< 0 or mx
>= 256 or my
< 0 or my
>= drawing
.h
then
149 local p1
= drawing
.points
[shape
.p1
]
150 if math
.abs(mx
-p1
.x
) > math
.abs(my
-p1
.y
) then
151 love
.graphics
.line(px(p1
.x
),py(p1
.y
), pmx
, py(p1
.y
))
153 love
.graphics
.line(px(p1
.x
),py(p1
.y
), px(p1
.x
),pmy
)
155 elseif shape
.mode
== 'polygon' then
156 -- don't close the loop on a pending polygon
158 for _
,point
in ipairs(shape
.vertices
) do
159 local curr
= drawing
.points
[point
]
161 love
.graphics
.line(px(prev
.x
),py(prev
.y
), px(curr
.x
),py(curr
.y
))
165 love
.graphics
.line(px(prev
.x
),py(prev
.y
), pmx
,pmy
)
166 elseif shape
.mode
== 'rectangle' then
167 local first
= drawing
.points
[shape
.vertices
[1]]
168 if #shape
.vertices
== 1 then
169 love
.graphics
.line(px(first
.x
),py(first
.y
), pmx
,pmy
)
172 local second
= drawing
.points
[shape
.vertices
[2]]
173 local thirdx
,thirdy
, fourthx
,fourthy
= Drawing
.complete_rectangle(first
.x
,first
.y
, second
.x
,second
.y
, mx
,my
)
174 love
.graphics
.line(px(first
.x
),py(first
.y
), px(second
.x
),py(second
.y
))
175 love
.graphics
.line(px(second
.x
),py(second
.y
), px(thirdx
),py(thirdy
))
176 love
.graphics
.line(px(thirdx
),py(thirdy
), px(fourthx
),py(fourthy
))
177 love
.graphics
.line(px(fourthx
),py(fourthy
), px(first
.x
),py(first
.y
))
178 elseif shape
.mode
== 'square' then
179 local first
= drawing
.points
[shape
.vertices
[1]]
180 if #shape
.vertices
== 1 then
181 love
.graphics
.line(px(first
.x
),py(first
.y
), pmx
,pmy
)
184 local second
= drawing
.points
[shape
.vertices
[2]]
185 local thirdx
,thirdy
, fourthx
,fourthy
= Drawing
.complete_square(first
.x
,first
.y
, second
.x
,second
.y
, mx
,my
)
186 love
.graphics
.line(px(first
.x
),py(first
.y
), px(second
.x
),py(second
.y
))
187 love
.graphics
.line(px(second
.x
),py(second
.y
), px(thirdx
),py(thirdy
))
188 love
.graphics
.line(px(thirdx
),py(thirdy
), px(fourthx
),py(fourthy
))
189 love
.graphics
.line(px(fourthx
),py(fourthy
), px(first
.x
),py(first
.y
))
190 elseif shape
.mode
== 'circle' then
191 local center
= drawing
.points
[shape
.center
]
192 if mx
< 0 or mx
>= 256 or my
< 0 or my
>= drawing
.h
then
195 local cx
,cy
= px(center
.x
), py(center
.y
)
196 love
.graphics
.circle('line', cx
,cy
, geom
.dist(cx
,cy
, App
.mouse_x(),App
.mouse_y()))
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_pressed(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_released
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 assert(drawing
.mode
== 'drawing')
253 local pmx
, pmy
= App
.mouse_x(), App
.mouse_y()
254 local mx
= Drawing
.coord(pmx
-State
.left
, State
.width
)
255 local my
= Drawing
.coord(pmy
-line_cache
.starty
, State
.width
)
256 if App
.mouse_down(1) then
257 if Drawing
.in_drawing(drawing
, line_cache
, pmx
,pmy
, State
.left
,State
.right
) then
258 if drawing
.pending
.mode
== 'freehand' then
259 table.insert(drawing
.pending
.points
, {x
=mx
, y
=my
})
260 elseif drawing
.pending
.mode
== 'move' then
261 drawing
.pending
.target_point
.x
= mx
262 drawing
.pending
.target_point
.y
= my
263 Drawing
.relax_constraints(drawing
, drawing
.pending
.target_point_index
)
266 elseif State
.current_drawing_mode
== 'move' then
267 if Drawing
.in_drawing(drawing
, line_cache
, pmx
, pmy
, State
.left
,State
.right
) then
268 drawing
.pending
.target_point
.x
= mx
269 drawing
.pending
.target_point
.y
= my
270 Drawing
.relax_constraints(drawing
, drawing
.pending
.target_point_index
)
277 function Drawing
.relax_constraints(drawing
, p
)
278 for _
,shape
in ipairs(drawing
.shapes
) do
279 if shape
.mode
== 'manhattan' then
280 if shape
.p1
== p
then
282 elseif shape
.p2
== p
then
285 elseif shape
.mode
== 'rectangle' or shape
.mode
== 'square' then
286 for _
,v
in ipairs(shape
.vertices
) do
288 shape
.mode
= 'polygon'
295 function Drawing
.mouse_released(State
, x
,y
, mouse_button
)
296 if State
.current_drawing_mode
== 'move' then
297 State
.current_drawing_mode
= State
.previous_drawing_mode
298 State
.previous_drawing_mode
= nil
299 if State
.lines
.current_drawing
then
300 State
.lines
.current_drawing
.pending
= {}
301 State
.lines
.current_drawing
= nil
303 elseif State
.lines
.current_drawing
then
304 local drawing
= State
.lines
.current_drawing
305 local line_cache
= State
.line_cache
[State
.lines
.current_drawing_index
]
306 if drawing
.pending
then
307 if drawing
.pending
.mode
== nil then
309 elseif drawing
.pending
.mode
== 'freehand' then
310 -- the last point added during update is good enough
311 Drawing
.smoothen(drawing
.pending
)
312 table.insert(drawing
.shapes
, drawing
.pending
)
313 elseif drawing
.pending
.mode
== 'line' then
314 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
315 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
316 drawing
.pending
.p2
= Drawing
.find_or_insert_point(drawing
.points
, mx
,my
, State
.width
)
317 table.insert(drawing
.shapes
, drawing
.pending
)
319 elseif drawing
.pending
.mode
== 'manhattan' then
320 local p1
= drawing
.points
[drawing
.pending
.p1
]
321 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
322 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
323 if math
.abs(mx
-p1
.x
) > math
.abs(my
-p1
.y
) then
324 drawing
.pending
.p2
= Drawing
.find_or_insert_point(drawing
.points
, mx
, p1
.y
, State
.width
)
326 drawing
.pending
.p2
= Drawing
.find_or_insert_point(drawing
.points
, p1
.x
, my
, State
.width
)
328 local p2
= drawing
.points
[drawing
.pending
.p2
]
329 App
.mouse_move(State
.left
+Drawing
.pixels(p2
.x
, State
.width
), line_cache
.starty
+Drawing
.pixels(p2
.y
, State
.width
))
330 table.insert(drawing
.shapes
, drawing
.pending
)
332 elseif drawing
.pending
.mode
== 'polygon' then
333 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
334 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
335 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, mx
,my
, State
.width
))
336 table.insert(drawing
.shapes
, drawing
.pending
)
338 elseif drawing
.pending
.mode
== 'rectangle' then
339 assert(#drawing
.pending
.vertices
<= 2)
340 if #drawing
.pending
.vertices
== 2 then
341 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
342 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
343 local first
= drawing
.points
[drawing
.pending
.vertices
[1]]
344 local second
= drawing
.points
[drawing
.pending
.vertices
[2]]
345 local thirdx
,thirdy
, fourthx
,fourthy
= Drawing
.complete_rectangle(first
.x
,first
.y
, second
.x
,second
.y
, mx
,my
)
346 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, thirdx
,thirdy
, State
.width
))
347 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, fourthx
,fourthy
, State
.width
))
348 table.insert(drawing
.shapes
, drawing
.pending
)
351 -- too few points; draw nothing
353 elseif drawing
.pending
.mode
== 'square' then
354 assert(#drawing
.pending
.vertices
<= 2)
355 if #drawing
.pending
.vertices
== 2 then
356 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
357 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
358 local first
= drawing
.points
[drawing
.pending
.vertices
[1]]
359 local second
= drawing
.points
[drawing
.pending
.vertices
[2]]
360 local thirdx
,thirdy
, fourthx
,fourthy
= Drawing
.complete_square(first
.x
,first
.y
, second
.x
,second
.y
, mx
,my
)
361 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, thirdx
,thirdy
, State
.width
))
362 table.insert(drawing
.pending
.vertices
, Drawing
.find_or_insert_point(drawing
.points
, fourthx
,fourthy
, State
.width
))
363 table.insert(drawing
.shapes
, drawing
.pending
)
366 elseif drawing
.pending
.mode
== 'circle' then
367 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
368 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
369 local center
= drawing
.points
[drawing
.pending
.center
]
370 drawing
.pending
.radius
= round(geom
.dist(center
.x
,center
.y
, mx
,my
))
371 table.insert(drawing
.shapes
, drawing
.pending
)
373 elseif drawing
.pending
.mode
== 'arc' then
374 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
375 if mx
>= 0 and mx
< 256 and my
>= 0 and my
< drawing
.h
then
376 local center
= drawing
.points
[drawing
.pending
.center
]
377 drawing
.pending
.end_angle
= geom
.angle_with_hint(center
.x
,center
.y
, mx
,my
, drawing
.pending
.end_angle
)
378 table.insert(drawing
.shapes
, drawing
.pending
)
380 elseif drawing
.pending
.mode
== 'name' then
383 print(drawing
.pending
.mode
)
386 State
.lines
.current_drawing
.pending
= {}
387 State
.lines
.current_drawing
= nil
392 function Drawing
.keychord_pressed(State
, chord
)
393 if chord
== 'C-p' and not App
.mouse_down(1) then
394 State
.current_drawing_mode
= 'freehand'
395 elseif App
.mouse_down(1) and chord
== 'l' then
396 State
.current_drawing_mode
= 'line'
397 local _
,drawing
= Drawing
.current_drawing(State
)
398 if drawing
.pending
.mode
== 'freehand' then
399 drawing
.pending
.p1
= Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)
400 elseif drawing
.pending
.mode
== 'polygon' or drawing
.pending
.mode
== 'rectangle' or drawing
.pending
.mode
== 'square' then
401 drawing
.pending
.p1
= drawing
.pending
.vertices
[1]
402 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
403 drawing
.pending
.p1
= drawing
.pending
.center
405 drawing
.pending
.mode
= 'line'
406 elseif chord
== 'C-l' and not App
.mouse_down(1) then
407 State
.current_drawing_mode
= 'line'
408 elseif App
.mouse_down(1) and chord
== 'm' then
409 State
.current_drawing_mode
= 'manhattan'
410 local drawing
= Drawing
.select_drawing_at_mouse(State
)
411 if drawing
.pending
.mode
== 'freehand' then
412 drawing
.pending
.p1
= Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)
413 elseif drawing
.pending
.mode
== 'line' then
415 elseif drawing
.pending
.mode
== 'polygon' or drawing
.pending
.mode
== 'rectangle' or drawing
.pending
.mode
== 'square' then
416 drawing
.pending
.p1
= drawing
.pending
.vertices
[1]
417 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
418 drawing
.pending
.p1
= drawing
.pending
.center
420 drawing
.pending
.mode
= 'manhattan'
421 elseif chord
== 'C-m' and not App
.mouse_down(1) then
422 State
.current_drawing_mode
= 'manhattan'
423 elseif chord
== 'C-g' and not App
.mouse_down(1) then
424 State
.current_drawing_mode
= 'polygon'
425 elseif App
.mouse_down(1) and chord
== 'g' then
426 State
.current_drawing_mode
= 'polygon'
427 local _
,drawing
= Drawing
.current_drawing(State
)
428 if drawing
.pending
.mode
== 'freehand' then
429 drawing
.pending
.vertices
= {Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)}
430 elseif drawing
.pending
.mode
== 'line' or drawing
.pending
.mode
== 'manhattan' then
431 if drawing
.pending
.vertices
== nil then
432 drawing
.pending
.vertices
= {drawing
.pending
.p1
}
434 elseif drawing
.pending
.mode
== 'rectangle' or drawing
.pending
.mode
== 'square' then
435 -- reuse existing vertices
436 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
437 drawing
.pending
.vertices
= {drawing
.pending
.center
}
439 drawing
.pending
.mode
= 'polygon'
440 elseif chord
== 'C-r' and not App
.mouse_down(1) then
441 State
.current_drawing_mode
= 'rectangle'
442 elseif App
.mouse_down(1) and chord
== 'r' then
443 State
.current_drawing_mode
= 'rectangle'
444 local _
,drawing
= Drawing
.current_drawing(State
)
445 if drawing
.pending
.mode
== 'freehand' then
446 drawing
.pending
.vertices
= {Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)}
447 elseif drawing
.pending
.mode
== 'line' or drawing
.pending
.mode
== 'manhattan' then
448 if drawing
.pending
.vertices
== nil then
449 drawing
.pending
.vertices
= {drawing
.pending
.p1
}
451 elseif drawing
.pending
.mode
== 'polygon' or drawing
.pending
.mode
== 'square' then
452 -- reuse existing (1-2) vertices
453 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
454 drawing
.pending
.vertices
= {drawing
.pending
.center
}
456 drawing
.pending
.mode
= 'rectangle'
457 elseif chord
== 'C-s' and not App
.mouse_down(1) then
458 State
.current_drawing_mode
= 'square'
459 elseif App
.mouse_down(1) and chord
== 's' then
460 State
.current_drawing_mode
= 'square'
461 local _
,drawing
= Drawing
.current_drawing(State
)
462 if drawing
.pending
.mode
== 'freehand' then
463 drawing
.pending
.vertices
= {Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)}
464 elseif drawing
.pending
.mode
== 'line' or drawing
.pending
.mode
== 'manhattan' then
465 if drawing
.pending
.vertices
== nil then
466 drawing
.pending
.vertices
= {drawing
.pending
.p1
}
468 elseif drawing
.pending
.mode
== 'polygon' then
469 while #drawing
.pending
.vertices
> 2 do
470 table.remove(drawing
.pending
.vertices
)
472 elseif drawing
.pending
.mode
== 'rectangle' then
473 -- reuse existing (1-2) vertices
474 elseif drawing
.pending
.mode
== 'circle' or drawing
.pending
.mode
== 'arc' then
475 drawing
.pending
.vertices
= {drawing
.pending
.center
}
477 drawing
.pending
.mode
= 'square'
478 elseif App
.mouse_down(1) and chord
== 'p' and State
.current_drawing_mode
== 'polygon' then
479 local _
,drawing
,line_cache
= Drawing
.current_drawing(State
)
480 local mx
,my
= Drawing
.coord(App
.mouse_x()-State
.left
, State
.width
), Drawing
.coord(App
.mouse_y()-line_cache
.starty
, State
.width
)
481 local j
= Drawing
.find_or_insert_point(drawing
.points
, mx
,my
, State
.width
)
482 table.insert(drawing
.pending
.vertices
, j
)
483 elseif App
.mouse_down(1) and chord
== 'p' and (State
.current_drawing_mode
== 'rectangle' or State
.current_drawing_mode
== 'square') then
484 local _
,drawing
,line_cache
= Drawing
.current_drawing(State
)
485 local mx
,my
= Drawing
.coord(App
.mouse_x()-State
.left
, State
.width
), Drawing
.coord(App
.mouse_y()-line_cache
.starty
, State
.width
)
486 local j
= Drawing
.find_or_insert_point(drawing
.points
, mx
,my
, State
.width
)
487 while #drawing
.pending
.vertices
>= 2 do
488 table.remove(drawing
.pending
.vertices
)
490 table.insert(drawing
.pending
.vertices
, j
)
491 elseif chord
== 'C-o' and not App
.mouse_down(1) then
492 State
.current_drawing_mode
= 'circle'
493 elseif App
.mouse_down(1) and chord
== 'a' and State
.current_drawing_mode
== 'circle' then
494 local _
,drawing
,line_cache
= Drawing
.current_drawing(State
)
495 drawing
.pending
.mode
= 'arc'
496 local mx
,my
= Drawing
.coord(App
.mouse_x()-State
.left
, State
.width
), Drawing
.coord(App
.mouse_y()-line_cache
.starty
, State
.width
)
497 local center
= drawing
.points
[drawing
.pending
.center
]
498 drawing
.pending
.radius
= round(geom
.dist(center
.x
,center
.y
, mx
,my
))
499 drawing
.pending
.start_angle
= geom
.angle(center
.x
,center
.y
, mx
,my
)
500 elseif App
.mouse_down(1) and chord
== 'o' then
501 State
.current_drawing_mode
= 'circle'
502 local _
,drawing
= Drawing
.current_drawing(State
)
503 if drawing
.pending
.mode
== 'freehand' then
504 drawing
.pending
.center
= Drawing
.find_or_insert_point(drawing
.points
, drawing
.pending
.points
[1].x
, drawing
.pending
.points
[1].y
, State
.width
)
505 elseif drawing
.pending
.mode
== 'line' or drawing
.pending
.mode
== 'manhattan' then
506 drawing
.pending
.center
= drawing
.pending
.p1
507 elseif drawing
.pending
.mode
== 'polygon' or drawing
.pending
.mode
== 'rectangle' or drawing
.pending
.mode
== 'square' then
508 drawing
.pending
.center
= drawing
.pending
.vertices
[1]
510 drawing
.pending
.mode
= 'circle'
511 elseif chord
== 'C-u' and not App
.mouse_down(1) then
512 local drawing_index
,drawing
,line_cache
,i
,p
= Drawing
.select_point_at_mouse(State
)
514 if State
.previous_drawing_mode
== nil then
515 State
.previous_drawing_mode
= State
.current_drawing_mode
517 State
.current_drawing_mode
= 'move'
518 drawing
.pending
= {mode
=State
.current_drawing_mode
, target_point
=p
, target_point_index
=i
}
519 State
.lines
.current_drawing_index
= drawing_index
520 State
.lines
.current_drawing
= drawing
522 elseif chord
== 'C-n' and not App
.mouse_down(1) then
523 local drawing_index
,drawing
,line_cache
,point_index
,p
= Drawing
.select_point_at_mouse(State
)
525 if State
.previous_drawing_mode
== nil then
527 State
.previous_drawing_mode
= State
.current_drawing_mode
529 State
.current_drawing_mode
= 'name'
531 drawing
.pending
= {mode
=State
.current_drawing_mode
, target_point
=point_index
}
532 State
.lines
.current_drawing_index
= drawing_index
533 State
.lines
.current_drawing
= drawing
535 elseif chord
== 'C-d' and not App
.mouse_down(1) then
536 local _
,drawing
,_
,i
,p
= Drawing
.select_point_at_mouse(State
)
538 for _
,shape
in ipairs(drawing
.shapes
) do
539 if Drawing
.contains_point(shape
, i
) then
540 if shape
.mode
== 'polygon' then
541 local idx
= table.find(shape
.vertices
, i
)
543 table.remove(shape
.vertices
, idx
)
544 if #shape
.vertices
< 3 then
545 shape
.mode
= 'deleted'
548 shape
.mode
= 'deleted'
552 drawing
.points
[i
].deleted
= true
554 local drawing
,_
,_
,shape
= Drawing
.select_shape_at_mouse(State
)
556 shape
.mode
= 'deleted'
558 elseif chord
== 'C-h' and not App
.mouse_down(1) then
559 local drawing
= Drawing
.select_drawing_at_mouse(State
)
561 drawing
.show_help
= true
563 elseif chord
== 'escape' and App
.mouse_down(1) then
564 local _
,drawing
= Drawing
.current_drawing(State
)
569 function Drawing
.complete_rectangle(firstx
,firsty
, secondx
,secondy
, x
,y
)
570 if firstx
== secondx
then
571 return x
,secondy
, x
,firsty
573 if firsty
== secondy
then
574 return secondx
,y
, firstx
,y
576 local first_slope
= (secondy
-firsty
)/(secondx
-firstx
)
577 -- slope of second edge:
579 -- equation of line containing the second edge:
580 -- y-secondy = -1/first_slope*(x-secondx)
581 -- => 1/first_slope*x + y + (- secondy - secondx/first_slope) = 0
582 -- now we want to find the point on this line that's closest to the mouse pointer.
583 -- https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_an_equation
584 local a
= 1/first_slope
585 local c
= -secondy
- secondx
/first_slope
586 local thirdx
= round(((x
-a
*y
) - a
*c
) / (a
*a
+ 1))
587 local thirdy
= round((a
*(-x
+ a
*y
) - c
) / (a
*a
+ 1))
588 -- slope of third edge = first_slope
589 -- equation of line containing third edge:
590 -- y - thirdy = first_slope*(x-thirdx)
591 -- => -first_slope*x + y + (-thirdy + thirdx*first_slope) = 0
592 -- now we want to find the point on this line that's closest to the first point
593 local a
= -first_slope
594 local c
= -thirdy
+ thirdx
*first_slope
595 local fourthx
= round(((firstx
-a
*firsty
) - a
*c
) / (a
*a
+ 1))
596 local fourthy
= round((a
*(-firstx
+ a
*firsty
) - c
) / (a
*a
+ 1))
597 return thirdx
,thirdy
, fourthx
,fourthy
600 function Drawing
.complete_square(firstx
,firsty
, secondx
,secondy
, x
,y
)
601 -- use x,y only to decide which side of the first edge to complete the square on
602 local deltax
= secondx
-firstx
603 local deltay
= secondy
-firsty
604 local thirdx
= secondx
+deltay
605 local thirdy
= secondy
-deltax
606 if not geom
.same_side(firstx
,firsty
, secondx
,secondy
, thirdx
,thirdy
, x
,y
) then
609 thirdx
= secondx
+deltay
610 thirdy
= secondy
-deltax
612 local fourthx
= firstx
+deltay
613 local fourthy
= firsty
-deltax
614 return thirdx
,thirdy
, fourthx
,fourthy
617 function Drawing
.current_drawing(State
)
618 local x
, y
= App
.mouse_x(), App
.mouse_y()
619 for drawing_index
,drawing
in ipairs(State
.lines
) do
620 if drawing
.mode
== 'drawing' then
621 local line_cache
= State
.line_cache
[drawing_index
]
622 if Drawing
.in_drawing(drawing
, line_cache
, x
,y
, State
.left
,State
.right
) then
623 return drawing_index
,drawing
,line_cache
630 function Drawing
.select_shape_at_mouse(State
)
631 for drawing_index
,drawing
in ipairs(State
.lines
) do
632 if drawing
.mode
== 'drawing' then
633 local x
, y
= App
.mouse_x(), App
.mouse_y()
634 local line_cache
= State
.line_cache
[drawing_index
]
635 if Drawing
.in_drawing(drawing
, line_cache
, x
,y
, State
.left
,State
.right
) then
636 local mx
,my
= Drawing
.coord(x
-State
.left
, State
.width
), Drawing
.coord(y
-line_cache
.starty
, State
.width
)
637 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
657 if Drawing
.near(point
, mx
,my
, State
.width
) then
658 return drawing_index
,drawing
,line_cache
,i
,point
666 function Drawing
.select_drawing_at_mouse(State
)
667 for drawing_index
,drawing
in ipairs(State
.lines
) do
668 if drawing
.mode
== 'drawing' then
669 local x
, y
= App
.mouse_x(), App
.mouse_y()
670 local line_cache
= State
.line_cache
[drawing_index
]
671 if Drawing
.in_drawing(drawing
, line_cache
, x
,y
, State
.left
,State
.right
) then
678 function Drawing
.contains_point(shape
, p
)
679 if shape
.mode
== 'freehand' then
681 elseif shape
.mode
== 'line' or shape
.mode
== 'manhattan' then
682 return shape
.p1
== p
or shape
.p2
== p
683 elseif shape
.mode
== 'polygon' or shape
.mode
== 'rectangle' or shape
.mode
== 'square' then
684 return table.find(shape
.vertices
, p
)
685 elseif shape
.mode
== 'circle' then
686 return shape
.center
== p
687 elseif shape
.mode
== 'arc' then
688 return shape
.center
== p
689 -- ugh, how to support angles
690 elseif shape
.mode
== 'deleted' then
698 function Drawing
.smoothen(shape
)
699 assert(shape
.mode
== 'freehand')
701 for i
=2,#shape
.points
-1 do
702 local a
= shape
.points
[i
-1]
703 local b
= shape
.points
[i
]
704 local c
= shape
.points
[i
+1]
705 b
.x
= round((a
.x
+ b
.x
+ c
.x
)/3)
706 b
.y
= round((a
.y
+ b
.y
+ c
.y
)/3)
712 return math
.floor(num
+.5)
715 function Drawing
.find_or_insert_point(points
, x
,y
, width
)
716 -- check if UI would snap the two points together
717 for i
,point
in ipairs(points
) do
718 if Drawing
.near(point
, x
,y
, width
) then
722 table.insert(points
, {x
=x
, y
=y
})
726 function Drawing
.near(point
, x
,y
, width
)
727 local px
,py
= Drawing
.pixels(x
, width
),Drawing
.pixels(y
, width
)
728 local cx
,cy
= Drawing
.pixels(point
.x
, width
), Drawing
.pixels(point
.y
, width
)
729 return (cx
-px
)*(cx
-px
) + (cy
-py
)*(cy
-py
) < Same_point_distance
*Same_point_distance
732 function Drawing
.pixels(n
, width
) -- parts to pixels
733 return math
.floor(n
*width
/256)
735 function Drawing
.coord(n
, width
) -- pixels to parts
736 return math
.floor(n
*256/width
)
739 function table.find(h
, x
)
740 for k
,v
in pairs(h
) do