drawing.lua 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751
  1. -- primitives for editing drawings
  2. Drawing = {}
  3. require 'drawing_tests'
  4. -- All drawings span 100% of some conceptual 'page width' and divide it up
  5. -- into 256 parts.
  6. function Drawing.draw(State, line_index, y)
  7. local line = State.lines[line_index]
  8. local pmx,pmy = App.mouse_x(), App.mouse_y()
  9. local starty = Text.starty(State, line_index)
  10. if pmx < State.right and pmy > starty and pmy < starty+Drawing.pixels(line.h, State.width) then
  11. App.color(Icon_color)
  12. love.graphics.rectangle('line', State.left,starty, State.width,Drawing.pixels(line.h, State.width))
  13. if icon[State.current_drawing_mode] then
  14. icon[State.current_drawing_mode](State.right-22, starty+4)
  15. else
  16. icon[State.previous_drawing_mode](State.right-22, starty+4)
  17. end
  18. if App.mouse_down(1) and love.keyboard.isDown('h') then
  19. draw_help_with_mouse_pressed(State, line_index)
  20. return
  21. end
  22. end
  23. if line.show_help then
  24. draw_help_without_mouse_pressed(State, line_index)
  25. return
  26. end
  27. local mx = Drawing.coord(pmx-State.left, State.width)
  28. local my = Drawing.coord(pmy-starty, State.width)
  29. for _,shape in ipairs(line.shapes) do
  30. if geom.on_shape(mx,my, line, shape) then
  31. App.color(Focus_stroke_color)
  32. else
  33. App.color(Stroke_color)
  34. end
  35. Drawing.draw_shape(line, shape, starty, State.left,State.right)
  36. end
  37. local function px(x) return Drawing.pixels(x, State.width)+State.left end
  38. local function py(y) return Drawing.pixels(y, State.width)+starty end
  39. for i,p in ipairs(line.points) do
  40. if p.deleted == nil then
  41. if Drawing.near(p, mx,my, State.width) then
  42. App.color(Focus_stroke_color)
  43. love.graphics.circle('line', px(p.x),py(p.y), Same_point_distance)
  44. else
  45. App.color(Stroke_color)
  46. love.graphics.circle('fill', px(p.x),py(p.y), 2)
  47. end
  48. if p.name then
  49. -- TODO: clip
  50. local x,y = px(p.x)+5, py(p.y)+5
  51. love.graphics.print(p.name, x,y)
  52. if State.current_drawing_mode == 'name' and i == line.pending.target_point then
  53. -- create a faint red box for the name
  54. App.color(Current_name_background_color)
  55. local name_width
  56. if p.name == '' then
  57. name_width = State.font:getWidth('m')
  58. else
  59. name_width = State.font:getWidth(p.name)
  60. end
  61. love.graphics.rectangle('fill', x,y, name_width, State.line_height)
  62. end
  63. end
  64. end
  65. end
  66. App.color(Current_stroke_color)
  67. Drawing.draw_pending_shape(line, starty, State.left,State.right)
  68. end
  69. function Drawing.draw_shape(drawing, shape, top, left,right)
  70. local width = right-left
  71. local function px(x) return Drawing.pixels(x, width)+left end
  72. local function py(y) return Drawing.pixels(y, width)+top end
  73. if shape.mode == 'freehand' then
  74. local prev = nil
  75. for _,point in ipairs(shape.points) do
  76. if prev then
  77. love.graphics.line(px(prev.x),py(prev.y), px(point.x),py(point.y))
  78. end
  79. prev = point
  80. end
  81. elseif shape.mode == 'line' or shape.mode == 'manhattan' then
  82. local p1 = drawing.points[shape.p1]
  83. local p2 = drawing.points[shape.p2]
  84. love.graphics.line(px(p1.x),py(p1.y), px(p2.x),py(p2.y))
  85. elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then
  86. local prev = nil
  87. for _,point in ipairs(shape.vertices) do
  88. local curr = drawing.points[point]
  89. if prev then
  90. love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y))
  91. end
  92. prev = curr
  93. end
  94. -- close the loop
  95. local curr = drawing.points[shape.vertices[1]]
  96. love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y))
  97. elseif shape.mode == 'circle' then
  98. -- TODO: clip
  99. local center = drawing.points[shape.center]
  100. love.graphics.circle('line', px(center.x),py(center.y), Drawing.pixels(shape.radius, width))
  101. elseif shape.mode == 'arc' then
  102. local center = drawing.points[shape.center]
  103. love.graphics.arc('line', 'open', px(center.x),py(center.y), Drawing.pixels(shape.radius, width), shape.start_angle, shape.end_angle, 360)
  104. elseif shape.mode == 'deleted' then
  105. -- ignore
  106. else
  107. assert(false, ('unknown drawing mode %s'):format(shape.mode))
  108. end
  109. end
  110. function Drawing.draw_pending_shape(drawing, top, left,right)
  111. local width = right-left
  112. local pmx,pmy = App.mouse_x(), App.mouse_y()
  113. local function px(x) return Drawing.pixels(x, width)+left end
  114. local function py(y) return Drawing.pixels(y, width)+top end
  115. local mx = Drawing.coord(pmx-left, width)
  116. local my = Drawing.coord(pmy-top, width)
  117. -- recreate pixels from coords to precisely mimic how the drawing will look
  118. -- after mouse_release
  119. pmx,pmy = px(mx), py(my)
  120. local shape = drawing.pending
  121. if shape.mode == nil then
  122. -- nothing pending
  123. elseif shape.mode == 'freehand' then
  124. local shape_copy = deepcopy(shape)
  125. Drawing.smoothen(shape_copy)
  126. Drawing.draw_shape(drawing, shape_copy, top, left,right)
  127. elseif shape.mode == 'line' then
  128. if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then
  129. return
  130. end
  131. local p1 = drawing.points[shape.p1]
  132. love.graphics.line(px(p1.x),py(p1.y), pmx,pmy)
  133. elseif shape.mode == 'manhattan' then
  134. if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then
  135. return
  136. end
  137. local p1 = drawing.points[shape.p1]
  138. if math.abs(mx-p1.x) > math.abs(my-p1.y) then
  139. love.graphics.line(px(p1.x),py(p1.y), pmx, py(p1.y))
  140. else
  141. love.graphics.line(px(p1.x),py(p1.y), px(p1.x),pmy)
  142. end
  143. elseif shape.mode == 'polygon' then
  144. -- don't close the loop on a pending polygon
  145. local prev = nil
  146. for _,point in ipairs(shape.vertices) do
  147. local curr = drawing.points[point]
  148. if prev then
  149. love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y))
  150. end
  151. prev = curr
  152. end
  153. love.graphics.line(px(prev.x),py(prev.y), pmx,pmy)
  154. elseif shape.mode == 'rectangle' then
  155. local first = drawing.points[shape.vertices[1]]
  156. if #shape.vertices == 1 then
  157. love.graphics.line(px(first.x),py(first.y), pmx,pmy)
  158. return
  159. end
  160. local second = drawing.points[shape.vertices[2]]
  161. local thirdx,thirdy, fourthx,fourthy = Drawing.complete_rectangle(first.x,first.y, second.x,second.y, mx,my)
  162. love.graphics.line(px(first.x),py(first.y), px(second.x),py(second.y))
  163. love.graphics.line(px(second.x),py(second.y), px(thirdx),py(thirdy))
  164. love.graphics.line(px(thirdx),py(thirdy), px(fourthx),py(fourthy))
  165. love.graphics.line(px(fourthx),py(fourthy), px(first.x),py(first.y))
  166. elseif shape.mode == 'square' 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)
  170. return
  171. end
  172. local second = drawing.points[shape.vertices[2]]
  173. local thirdx,thirdy, fourthx,fourthy = Drawing.complete_square(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 == 'circle' then
  179. local center = drawing.points[shape.center]
  180. if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then
  181. return
  182. end
  183. local r = round(geom.dist(center.x, center.y, mx, my))
  184. local cx,cy = px(center.x), py(center.y)
  185. love.graphics.circle('line', cx,cy, Drawing.pixels(r, width))
  186. elseif shape.mode == 'arc' then
  187. local center = drawing.points[shape.center]
  188. if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then
  189. return
  190. end
  191. shape.end_angle = geom.angle_with_hint(center.x,center.y, mx,my, shape.end_angle)
  192. local cx,cy = px(center.x), py(center.y)
  193. love.graphics.arc('line', 'open', cx,cy, Drawing.pixels(shape.radius, width), shape.start_angle, shape.end_angle, 360)
  194. elseif shape.mode == 'move' then
  195. -- nothing pending; changes are immediately committed
  196. elseif shape.mode == 'name' then
  197. -- nothing pending; changes are immediately committed
  198. else
  199. assert(false, ('unknown drawing mode %s'):format(shape.mode))
  200. end
  201. end
  202. function Drawing.in_current_drawing(State, x,y, left,right)
  203. return Drawing.in_drawing(State, State.lines.current_drawing_index, x,y, left,right)
  204. end
  205. function Drawing.in_drawing(State, line_index, x,y, left,right)
  206. assert(State.lines[line_index].mode == 'drawing')
  207. local starty = Text.starty(State, line_index)
  208. if starty == nil then return false end -- outside current page
  209. local drawing = State.lines[line_index]
  210. local width = right-left
  211. return y >= starty and y < starty + Drawing.pixels(drawing.h, width) and x >= left and x < right
  212. end
  213. function Drawing.mouse_press(State, drawing_index, x,y, mouse_button)
  214. local drawing = State.lines[drawing_index]
  215. local starty = Text.starty(State, drawing_index)
  216. local cx = Drawing.coord(x-State.left, State.width)
  217. local cy = Drawing.coord(y-starty, State.width)
  218. if State.current_drawing_mode == 'freehand' then
  219. drawing.pending = {mode=State.current_drawing_mode, points={{x=cx, y=cy}}}
  220. elseif State.current_drawing_mode == 'line' or State.current_drawing_mode == 'manhattan' then
  221. local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width)
  222. drawing.pending = {mode=State.current_drawing_mode, p1=j}
  223. elseif State.current_drawing_mode == 'polygon' or State.current_drawing_mode == 'rectangle' or State.current_drawing_mode == 'square' then
  224. local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width)
  225. drawing.pending = {mode=State.current_drawing_mode, vertices={j}}
  226. elseif State.current_drawing_mode == 'circle' then
  227. local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width)
  228. drawing.pending = {mode=State.current_drawing_mode, center=j}
  229. elseif State.current_drawing_mode == 'move' then
  230. -- all the action is in mouse_release
  231. elseif State.current_drawing_mode == 'name' then
  232. -- nothing
  233. else
  234. assert(false, ('unknown drawing mode %s'):format(State.current_drawing_mode))
  235. end
  236. end
  237. -- a couple of operations on drawings need to constantly check the state of the mouse
  238. function Drawing.update(State)
  239. if State.lines.current_drawing == nil then return end
  240. local drawing = State.lines.current_drawing
  241. local starty = Text.starty(State, State.lines.current_drawing_index)
  242. if starty == nil then
  243. -- some event cleared starty just this frame
  244. -- draw in this frame will soon set starty
  245. -- just skip this frame
  246. return
  247. end
  248. assert(drawing.mode == 'drawing', 'Drawing.update: line is not a drawing')
  249. local pmx, pmy = App.mouse_x(), App.mouse_y()
  250. local mx = Drawing.coord(pmx-State.left, State.width)
  251. local my = Drawing.coord(pmy-starty, State.width)
  252. if App.mouse_down(1) then
  253. if Drawing.in_current_drawing(State, pmx,pmy, State.left,State.right) then
  254. if drawing.pending.mode == 'freehand' then
  255. table.insert(drawing.pending.points, {x=mx, y=my})
  256. elseif drawing.pending.mode == 'move' then
  257. drawing.pending.target_point.x = mx
  258. drawing.pending.target_point.y = my
  259. Drawing.relax_constraints(drawing, drawing.pending.target_point_index)
  260. end
  261. end
  262. elseif State.current_drawing_mode == 'move' then
  263. if Drawing.in_current_drawing(State, pmx, pmy, State.left,State.right) then
  264. drawing.pending.target_point.x = mx
  265. drawing.pending.target_point.y = my
  266. Drawing.relax_constraints(drawing, drawing.pending.target_point_index)
  267. end
  268. else
  269. -- do nothing
  270. end
  271. end
  272. function Drawing.relax_constraints(drawing, p)
  273. for _,shape in ipairs(drawing.shapes) do
  274. if shape.mode == 'manhattan' then
  275. if shape.p1 == p then
  276. shape.mode = 'line'
  277. elseif shape.p2 == p then
  278. shape.mode = 'line'
  279. end
  280. elseif shape.mode == 'rectangle' or shape.mode == 'square' then
  281. for _,v in ipairs(shape.vertices) do
  282. if v == p then
  283. shape.mode = 'polygon'
  284. end
  285. end
  286. end
  287. end
  288. end
  289. function Drawing.mouse_release(State, x,y, mouse_button)
  290. if State.current_drawing_mode == 'move' then
  291. State.current_drawing_mode = State.previous_drawing_mode
  292. State.previous_drawing_mode = nil
  293. if State.lines.current_drawing then
  294. State.lines.current_drawing.pending = {}
  295. State.lines.current_drawing = nil
  296. end
  297. elseif State.lines.current_drawing then
  298. local drawing = State.lines.current_drawing
  299. local starty = Text.starty(State, State.lines.current_drawing_index)
  300. if drawing.pending then
  301. if drawing.pending.mode == nil then
  302. -- nothing pending
  303. elseif drawing.pending.mode == 'freehand' then
  304. -- the last point added during update is good enough
  305. Drawing.smoothen(drawing.pending)
  306. table.insert(drawing.shapes, drawing.pending)
  307. elseif drawing.pending.mode == 'line' then
  308. local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-starty, State.width)
  309. if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
  310. drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, mx,my, State.width)
  311. table.insert(drawing.shapes, drawing.pending)
  312. end
  313. elseif drawing.pending.mode == 'manhattan' then
  314. local p1 = drawing.points[drawing.pending.p1]
  315. local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-starty, State.width)
  316. if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
  317. if math.abs(mx-p1.x) > math.abs(my-p1.y) then
  318. drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, mx, p1.y, State.width)
  319. else
  320. drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, p1.x, my, State.width)
  321. end
  322. local p2 = drawing.points[drawing.pending.p2]
  323. App.mouse_move(State.left+Drawing.pixels(p2.x, State.width), starty+Drawing.pixels(p2.y, State.width))
  324. table.insert(drawing.shapes, drawing.pending)
  325. end
  326. elseif drawing.pending.mode == 'polygon' then
  327. local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-starty, State.width)
  328. if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
  329. table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, mx,my, State.width))
  330. table.insert(drawing.shapes, drawing.pending)
  331. end
  332. elseif drawing.pending.mode == 'rectangle' then
  333. assert(#drawing.pending.vertices <= 2, 'Drawing.mouse_release: rectangle has too many pending vertices')
  334. if #drawing.pending.vertices == 2 then
  335. local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-starty, State.width)
  336. if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
  337. local first = drawing.points[drawing.pending.vertices[1]]
  338. local second = drawing.points[drawing.pending.vertices[2]]
  339. local thirdx,thirdy, fourthx,fourthy = Drawing.complete_rectangle(first.x,first.y, second.x,second.y, mx,my)
  340. table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, thirdx,thirdy, State.width))
  341. table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, fourthx,fourthy, State.width))
  342. table.insert(drawing.shapes, drawing.pending)
  343. end
  344. else
  345. -- too few points; draw nothing
  346. end
  347. elseif drawing.pending.mode == 'square' then
  348. assert(#drawing.pending.vertices <= 2, 'Drawing.mouse_release: square has too many pending vertices')
  349. if #drawing.pending.vertices == 2 then
  350. local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-starty, State.width)
  351. if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
  352. local first = drawing.points[drawing.pending.vertices[1]]
  353. local second = drawing.points[drawing.pending.vertices[2]]
  354. local thirdx,thirdy, fourthx,fourthy = Drawing.complete_square(first.x,first.y, second.x,second.y, mx,my)
  355. table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, thirdx,thirdy, State.width))
  356. table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, fourthx,fourthy, State.width))
  357. table.insert(drawing.shapes, drawing.pending)
  358. end
  359. end
  360. elseif drawing.pending.mode == 'circle' then
  361. local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-starty, State.width)
  362. if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
  363. local center = drawing.points[drawing.pending.center]
  364. drawing.pending.radius = round(geom.dist(center.x,center.y, mx,my))
  365. table.insert(drawing.shapes, drawing.pending)
  366. end
  367. elseif drawing.pending.mode == 'arc' then
  368. local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-starty, State.width)
  369. if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then
  370. local center = drawing.points[drawing.pending.center]
  371. drawing.pending.end_angle = geom.angle_with_hint(center.x,center.y, mx,my, drawing.pending.end_angle)
  372. table.insert(drawing.shapes, drawing.pending)
  373. end
  374. elseif drawing.pending.mode == 'name' then
  375. -- drop it
  376. else
  377. assert(false, ('unknown drawing mode %s'):format(drawing.pending.mode))
  378. end
  379. State.lines.current_drawing.pending = {}
  380. State.lines.current_drawing = nil
  381. end
  382. end
  383. end
  384. function Drawing.keychord_press(State, chord)
  385. if chord == 'C-p' and not App.mouse_down(1) then
  386. State.current_drawing_mode = 'freehand'
  387. elseif App.mouse_down(1) and chord == 'l' then
  388. State.current_drawing_mode = 'line'
  389. local _,drawing = Drawing.current_drawing(State)
  390. if drawing.pending.mode == 'freehand' then
  391. drawing.pending.p1 = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)
  392. elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then
  393. drawing.pending.p1 = drawing.pending.vertices[1]
  394. elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then
  395. drawing.pending.p1 = drawing.pending.center
  396. end
  397. drawing.pending.mode = 'line'
  398. elseif chord == 'C-l' and not App.mouse_down(1) then
  399. State.current_drawing_mode = 'line'
  400. elseif App.mouse_down(1) and chord == 'm' then
  401. State.current_drawing_mode = 'manhattan'
  402. local drawing = Drawing.select_drawing_at_mouse(State)
  403. if drawing.pending.mode == 'freehand' then
  404. drawing.pending.p1 = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)
  405. elseif drawing.pending.mode == 'line' then
  406. -- do nothing
  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
  411. end
  412. drawing.pending.mode = 'manhattan'
  413. elseif chord == 'C-m' and not App.mouse_down(1) then
  414. State.current_drawing_mode = 'manhattan'
  415. elseif chord == 'C-g' and not App.mouse_down(1) then
  416. State.current_drawing_mode = 'polygon'
  417. elseif App.mouse_down(1) and chord == 'g' then
  418. State.current_drawing_mode = 'polygon'
  419. local _,drawing = Drawing.current_drawing(State)
  420. if drawing.pending.mode == 'freehand' then
  421. drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)}
  422. elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then
  423. if drawing.pending.vertices == nil then
  424. drawing.pending.vertices = {drawing.pending.p1}
  425. end
  426. elseif drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then
  427. -- reuse existing vertices
  428. elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then
  429. drawing.pending.vertices = {drawing.pending.center}
  430. end
  431. drawing.pending.mode = 'polygon'
  432. elseif chord == 'C-r' and not App.mouse_down(1) then
  433. State.current_drawing_mode = 'rectangle'
  434. elseif App.mouse_down(1) and chord == 'r' then
  435. State.current_drawing_mode = 'rectangle'
  436. local _,drawing = Drawing.current_drawing(State)
  437. if drawing.pending.mode == 'freehand' then
  438. drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)}
  439. elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then
  440. if drawing.pending.vertices == nil then
  441. drawing.pending.vertices = {drawing.pending.p1}
  442. end
  443. elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'square' then
  444. -- reuse existing (1-2) vertices
  445. elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then
  446. drawing.pending.vertices = {drawing.pending.center}
  447. end
  448. drawing.pending.mode = 'rectangle'
  449. elseif chord == 'C-s' and not App.mouse_down(1) then
  450. State.current_drawing_mode = 'square'
  451. elseif App.mouse_down(1) and chord == 's' then
  452. State.current_drawing_mode = 'square'
  453. local _,drawing = Drawing.current_drawing(State)
  454. if drawing.pending.mode == 'freehand' then
  455. drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)}
  456. elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then
  457. if drawing.pending.vertices == nil then
  458. drawing.pending.vertices = {drawing.pending.p1}
  459. end
  460. elseif drawing.pending.mode == 'polygon' then
  461. while #drawing.pending.vertices > 2 do
  462. table.remove(drawing.pending.vertices)
  463. end
  464. elseif drawing.pending.mode == 'rectangle' then
  465. -- reuse existing (1-2) vertices
  466. elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then
  467. drawing.pending.vertices = {drawing.pending.center}
  468. end
  469. drawing.pending.mode = 'square'
  470. elseif App.mouse_down(1) and chord == 'p' and State.current_drawing_mode == 'polygon' then
  471. local drawing_index,drawing = Drawing.current_drawing(State)
  472. local starty = Text.starty(State, drawing_index)
  473. local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-starty, State.width)
  474. local j = Drawing.find_or_insert_point(drawing.points, mx,my, State.width)
  475. table.insert(drawing.pending.vertices, j)
  476. elseif App.mouse_down(1) and chord == 'p' and (State.current_drawing_mode == 'rectangle' or State.current_drawing_mode == 'square') then
  477. local drawing_index,drawing = Drawing.current_drawing(State)
  478. local starty = Text.starty(State, drawing_index)
  479. local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-starty, State.width)
  480. local j = Drawing.find_or_insert_point(drawing.points, mx,my, State.width)
  481. while #drawing.pending.vertices >= 2 do
  482. table.remove(drawing.pending.vertices)
  483. end
  484. table.insert(drawing.pending.vertices, j)
  485. elseif chord == 'C-o' and not App.mouse_down(1) then
  486. State.current_drawing_mode = 'circle'
  487. elseif App.mouse_down(1) and chord == 'a' and State.current_drawing_mode == 'circle' then
  488. local drawing_index,drawing = Drawing.current_drawing(State)
  489. local starty = Text.starty(State, drawing_index)
  490. drawing.pending.mode = 'arc'
  491. local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-starty, State.width)
  492. local center = drawing.points[drawing.pending.center]
  493. drawing.pending.radius = round(geom.dist(center.x,center.y, mx,my))
  494. drawing.pending.start_angle = geom.angle(center.x,center.y, mx,my)
  495. elseif App.mouse_down(1) and chord == 'o' then
  496. State.current_drawing_mode = 'circle'
  497. local _,drawing = Drawing.current_drawing(State)
  498. if drawing.pending.mode == 'freehand' then
  499. drawing.pending.center = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)
  500. elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then
  501. drawing.pending.center = drawing.pending.p1
  502. elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then
  503. drawing.pending.center = drawing.pending.vertices[1]
  504. end
  505. drawing.pending.mode = 'circle'
  506. elseif chord == 'C-u' and not App.mouse_down(1) then
  507. local drawing_index,drawing,_,i,p = Drawing.select_point_at_mouse(State)
  508. if drawing then
  509. if State.previous_drawing_mode == nil then
  510. State.previous_drawing_mode = State.current_drawing_mode
  511. end
  512. State.current_drawing_mode = 'move'
  513. drawing.pending = {mode=State.current_drawing_mode, target_point=p, target_point_index=i}
  514. State.lines.current_drawing_index = drawing_index
  515. State.lines.current_drawing = drawing
  516. end
  517. elseif chord == 'C-n' and not App.mouse_down(1) then
  518. local drawing_index,drawing,_,point_index,p = Drawing.select_point_at_mouse(State)
  519. if drawing then
  520. if State.previous_drawing_mode == nil then
  521. -- don't clobber
  522. State.previous_drawing_mode = State.current_drawing_mode
  523. end
  524. State.current_drawing_mode = 'name'
  525. p.name = ''
  526. drawing.pending = {mode=State.current_drawing_mode, target_point=point_index}
  527. State.lines.current_drawing_index = drawing_index
  528. State.lines.current_drawing = drawing
  529. end
  530. elseif chord == 'C-d' and not App.mouse_down(1) then
  531. local _,drawing,_,i,p = Drawing.select_point_at_mouse(State)
  532. if drawing then
  533. for _,shape in ipairs(drawing.shapes) do
  534. if Drawing.contains_point(shape, i) then
  535. if shape.mode == 'polygon' then
  536. local idx = table.find(shape.vertices, i)
  537. assert(idx, 'point to delete is not in vertices')
  538. table.remove(shape.vertices, idx)
  539. if #shape.vertices < 3 then
  540. shape.mode = 'deleted'
  541. end
  542. else
  543. shape.mode = 'deleted'
  544. end
  545. end
  546. end
  547. drawing.points[i].deleted = true
  548. end
  549. local drawing,_,_,shape = Drawing.select_shape_at_mouse(State)
  550. if drawing then
  551. shape.mode = 'deleted'
  552. end
  553. elseif chord == 'C-h' and not App.mouse_down(1) then
  554. local drawing = Drawing.select_drawing_at_mouse(State)
  555. if drawing then
  556. drawing.show_help = true
  557. end
  558. elseif chord == 'escape' and App.mouse_down(1) then
  559. local _,drawing = Drawing.current_drawing(State)
  560. drawing.pending = {}
  561. end
  562. end
  563. function Drawing.complete_rectangle(firstx,firsty, secondx,secondy, x,y)
  564. if firstx == secondx then
  565. return x,secondy, x,firsty
  566. end
  567. if firsty == secondy then
  568. return secondx,y, firstx,y
  569. end
  570. local first_slope = (secondy-firsty)/(secondx-firstx)
  571. -- slope of second edge:
  572. -- -1/first_slope
  573. -- equation of line containing the second edge:
  574. -- y-secondy = -1/first_slope*(x-secondx)
  575. -- => 1/first_slope*x + y + (- secondy - secondx/first_slope) = 0
  576. -- now we want to find the point on this line that's closest to the mouse pointer.
  577. -- https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_an_equation
  578. local a = 1/first_slope
  579. local c = -secondy - secondx/first_slope
  580. local thirdx = round(((x-a*y) - a*c) / (a*a + 1))
  581. local thirdy = round((a*(-x + a*y) - c) / (a*a + 1))
  582. -- slope of third edge = first_slope
  583. -- equation of line containing third edge:
  584. -- y - thirdy = first_slope*(x-thirdx)
  585. -- => -first_slope*x + y + (-thirdy + thirdx*first_slope) = 0
  586. -- now we want to find the point on this line that's closest to the first point
  587. local a = -first_slope
  588. local c = -thirdy + thirdx*first_slope
  589. local fourthx = round(((firstx-a*firsty) - a*c) / (a*a + 1))
  590. local fourthy = round((a*(-firstx + a*firsty) - c) / (a*a + 1))
  591. return thirdx,thirdy, fourthx,fourthy
  592. end
  593. function Drawing.complete_square(firstx,firsty, secondx,secondy, x,y)
  594. -- use x,y only to decide which side of the first edge to complete the square on
  595. local deltax = secondx-firstx
  596. local deltay = secondy-firsty
  597. local thirdx = secondx+deltay
  598. local thirdy = secondy-deltax
  599. if not geom.same_side(firstx,firsty, secondx,secondy, thirdx,thirdy, x,y) then
  600. deltax = -deltax
  601. deltay = -deltay
  602. thirdx = secondx+deltay
  603. thirdy = secondy-deltax
  604. end
  605. local fourthx = firstx+deltay
  606. local fourthy = firsty-deltax
  607. return thirdx,thirdy, fourthx,fourthy
  608. end
  609. function Drawing.current_drawing(State)
  610. local x, y = App.mouse_x(), App.mouse_y()
  611. for drawing_index,drawing in ipairs(State.lines) do
  612. if drawing.mode == 'drawing' then
  613. if Drawing.in_drawing(State, drawing_index, x,y, State.left,State.right) then
  614. return drawing_index,drawing
  615. end
  616. end
  617. end
  618. return nil
  619. end
  620. function Drawing.select_shape_at_mouse(State)
  621. for drawing_index,drawing in ipairs(State.lines) do
  622. if drawing.mode == 'drawing' then
  623. local x, y = App.mouse_x(), App.mouse_y()
  624. local starty = Text.starty(State, drawing_index)
  625. if Drawing.in_drawing(State, drawing_index, x,y, State.left,State.right) then
  626. local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-starty, State.width)
  627. for i,shape in ipairs(drawing.shapes) do
  628. if geom.on_shape(mx,my, drawing, shape) then
  629. return drawing,starty,i,shape
  630. end
  631. end
  632. end
  633. end
  634. end
  635. end
  636. function Drawing.select_point_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 starty = Text.starty(State, drawing_index)
  641. if Drawing.in_drawing(State, drawing_index, x,y, State.left,State.right) then
  642. local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-starty, State.width)
  643. for i,point in ipairs(drawing.points) do
  644. if Drawing.near(point, mx,my, State.width) then
  645. return drawing_index,drawing,starty,i,point
  646. end
  647. end
  648. end
  649. end
  650. end
  651. end
  652. function Drawing.select_drawing_at_mouse(State)
  653. for drawing_index,drawing in ipairs(State.lines) do
  654. if drawing.mode == 'drawing' then
  655. local x, y = App.mouse_x(), App.mouse_y()
  656. if Drawing.in_drawing(State, drawing_index, x,y, State.left,State.right) then
  657. return drawing
  658. end
  659. end
  660. end
  661. end
  662. function Drawing.contains_point(shape, p)
  663. if shape.mode == 'freehand' then
  664. -- not supported
  665. elseif shape.mode == 'line' or shape.mode == 'manhattan' then
  666. return shape.p1 == p or shape.p2 == p
  667. elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then
  668. return table.find(shape.vertices, p)
  669. elseif shape.mode == 'circle' then
  670. return shape.center == p
  671. elseif shape.mode == 'arc' then
  672. return shape.center == p
  673. -- ugh, how to support angles
  674. elseif shape.mode == 'deleted' then
  675. -- already done
  676. else
  677. assert(false, ('unknown drawing mode %s'):format(shape.mode))
  678. end
  679. end
  680. function Drawing.smoothen(shape)
  681. assert(shape.mode == 'freehand', 'can only smoothen freehand shapes')
  682. for _=1,7 do
  683. for i=2,#shape.points-1 do
  684. local a = shape.points[i-1]
  685. local b = shape.points[i]
  686. local c = shape.points[i+1]
  687. b.x = round((a.x + b.x + c.x)/3)
  688. b.y = round((a.y + b.y + c.y)/3)
  689. end
  690. end
  691. end
  692. function round(num)
  693. return math.floor(num+.5)
  694. end
  695. function Drawing.find_or_insert_point(points, x,y, width)
  696. -- check if UI would snap the two points together
  697. for i,point in ipairs(points) do
  698. if Drawing.near(point, x,y, width) then
  699. return i
  700. end
  701. end
  702. table.insert(points, {x=x, y=y})
  703. return #points
  704. end
  705. function Drawing.near(point, x,y, width)
  706. local px,py = Drawing.pixels(x, width),Drawing.pixels(y, width)
  707. local cx,cy = Drawing.pixels(point.x, width), Drawing.pixels(point.y, width)
  708. return (cx-px)*(cx-px) + (cy-py)*(cy-py) < Same_point_distance*Same_point_distance
  709. end
  710. function Drawing.pixels(n, width) -- parts to pixels
  711. return math.floor(n*width/256)
  712. end
  713. function Drawing.coord(n, width) -- pixels to parts
  714. return math.floor(n*256/width)
  715. end
  716. function table.find(h, x)
  717. for k,v in pairs(h) do
  718. if v == x then
  719. return k
  720. end
  721. end
  722. end