diff options
-rw-r--r-- | README.md | 70 | ||||
-rw-r--r-- | drawing.lua | 750 | ||||
-rw-r--r-- | drawing_tests.lua | 785 | ||||
-rw-r--r-- | edit.lua | 236 | ||||
-rw-r--r-- | file.lua | 133 | ||||
-rw-r--r-- | geom.lua | 168 | ||||
-rw-r--r-- | help.lua | 156 | ||||
-rw-r--r-- | icons.lua | 58 | ||||
-rw-r--r-- | main.lua | 10 | ||||
-rw-r--r-- | select.lua | 10 | ||||
-rw-r--r-- | text.lua | 149 | ||||
-rw-r--r-- | text_tests.lua | 60 | ||||
-rw-r--r-- | undo.lua | 16 |
13 files changed, 119 insertions, 2482 deletions
diff --git a/README.md b/README.md index 31c2658..c089fdb 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,12 @@ -# Plain text with lines +# An editor for plain text. + +Not very useful by itself, but it's a fork of [lines.love](http://akkartik.name/lines.html) +that you can take in other directions besides line drawings, while easily +sharing patches between forks. -An editor for plain text where you can also seamlessly insert line drawings. Designed above all to be easy to modify and give you early warning if your modifications break something. -http://akkartik.name/lines.html - ## Invocation To run from the terminal, [pass this directory to LÖVE](https://love2d.org/wiki/Getting_Started#Running_Games), @@ -13,13 +14,13 @@ optionally with a file path to edit. Alternatively, turn it into a .love file you can double-click on: ``` -$ zip -r /tmp/lines.love *.lua +$ zip -r /tmp/text.love *.lua ``` -By default, lines.love reads/writes the file `lines.txt` in your default +By default, it reads/writes the file `lines.txt` in your default user/home directory (`https://love2d.org/wiki/love.filesystem.getUserDirectory`). -To open a different file, drop it on the lines.love window. +To open a different file, drop it on the app window. ## Keyboard shortcuts @@ -30,12 +31,7 @@ While editing text: * `ctrl+=` to zoom in, `ctrl+-` to zoom out, `ctrl+0` to reset zoom * `alt+right`/`alt+left` to jump to the next/previous word, respectively -For shortcuts while editing drawings, consult the online help. Either: -* hover on a drawing and hit `ctrl+h`, or -* click on a drawing to start a stroke and then press and hold `h` to see your - options at any point during a stroke. - -lines.love has been exclusively tested so far with a US keyboard layout. If +Exclusively tested so far with a US keyboard layout. If you use a different layout, please let me know if things worked, or if you found anything amiss: http://akkartik.name/contact @@ -46,26 +42,11 @@ found anything amiss: http://akkartik.name/contact * No support yet for right-to-left languages. * Undo/redo may be sluggish in large files. Large files may grow sluggish in - other ways. lines.love works well in all circumstances with files under - 50KB. + other ways. Works well in all circumstances with files under 50KB. * If you kill the process, say by force-quitting because things things get sluggish, you can lose data. -* The text cursor will always stay on the screen. This can have some strange - implications: - - * A long series of drawings will get silently skipped when you hit - page-down, until a line of text can be showed on screen. - * If there's no line of text at the top of the file, you may not be able - to scroll back up to the top with page-up. - - So far this app isn't really designed for drawing-heavy files. For now I'm - targeting mostly-text files with a few drawings mixed in. - -* No clipping yet for drawings. In particular, circles/squares/rectangles and - point labels can overflow a drawing. - * Long wrapping lines can't yet distinguish between the cursor at end of one screen line and start of the next, so clicking the mouse to position the cursor can very occasionally do the wrong thing. @@ -80,28 +61,19 @@ found anything amiss: http://akkartik.name/contact ## Mirrors and Forks -Updates to lines.love can be downloaded from the following mirrors in addition -to the website above: -* https://github.com/akkartik/lines.love -* https://repo.or.cz/lines.love.git -* https://codeberg.org/akkartik/lines.love -* https://tildegit.org/akkartik/lines.love -* https://git.tilde.institute/akkartik/lines.love -* https://git.sr.ht/~akkartik/lines.love -* https://notabug.org/akkartik/lines.love -* https://pagure.io/lines.love - -Forks of lines.love are encouraged. If you show me your fork, I'll link to it -here. - -* https://github.com/akkartik/lines-polygon-experiment -- an experiment that - uses separate shortcuts for regular polygons. `ctrl+3` for triangles, - `ctrl+4` for squares, etc. +This repo is a fork of lines.love at [http://akkartik.name/lines.html](http://akkartik.name/lines.html). +Updates to it can be downloaded from the following mirrors: -## Associated tools +* https://codeberg.org/akkartik/text.love +* https://repo.or.cz/text.love.git +* https://tildegit.org/akkartik/text.love +* https://git.tilde.institute/akkartik/text.love +* https://git.sr.ht/~akkartik/text.love +* https://notabug.org/akkartik/text.love +* https://github.com/akkartik/text.love +* https://pagure.io/text.love -* https://codeberg.org/akkartik/lines2md exports lines.love files to Markdown - and (non-editable) SVG. +Further forks are encouraged. If you show me your fork, I'll link to it here. ## Feedback diff --git a/drawing.lua b/drawing.lua deleted file mode 100644 index 8263dd9..0000000 --- a/drawing.lua +++ /dev/null @@ -1,750 +0,0 @@ --- primitives for editing drawings -Drawing = {} -require 'drawing_tests' - --- All drawings span 100% of some conceptual 'page width' and divide it up --- into 256 parts. -function Drawing.draw(State, line_index, y) - local line = State.lines[line_index] - local line_cache = State.line_cache[line_index] - line_cache.starty = y - local pmx,pmy = App.mouse_x(), App.mouse_y() - if pmx < State.right and pmy > line_cache.starty and pmy < line_cache.starty+Drawing.pixels(line.h, State.width) then - App.color(Icon_color) - love.graphics.rectangle('line', State.left,line_cache.starty, State.width,Drawing.pixels(line.h, State.width)) - if icon[State.current_drawing_mode] then - icon[State.current_drawing_mode](State.right-22, line_cache.starty+4) - else - icon[State.previous_drawing_mode](State.right-22, line_cache.starty+4) - end - - if App.mouse_down(1) and love.keyboard.isDown('h') then - draw_help_with_mouse_pressed(State, line_index) - return - end - end - - if line.show_help then - draw_help_without_mouse_pressed(State, line_index) - return - end - - local mx = Drawing.coord(pmx-State.left, State.width) - local my = Drawing.coord(pmy-line_cache.starty, State.width) - - for _,shape in ipairs(line.shapes) do - assert(shape) - if geom.on_shape(mx,my, line, shape) then - App.color(Focus_stroke_color) - else - App.color(Stroke_color) - end - Drawing.draw_shape(line, shape, line_cache.starty, State.left,State.right) - end - - local function px(x) return Drawing.pixels(x, State.width)+State.left end - local function py(y) return Drawing.pixels(y, State.width)+line_cache.starty end - for i,p in ipairs(line.points) do - if p.deleted == nil then - if Drawing.near(p, mx,my, State.width) then - App.color(Focus_stroke_color) - love.graphics.circle('line', px(p.x),py(p.y), Same_point_distance) - else - App.color(Stroke_color) - love.graphics.circle('fill', px(p.x),py(p.y), 2) - end - if p.name then - -- TODO: clip - local x,y = px(p.x)+5, py(p.y)+5 - love.graphics.print(p.name, x,y) - if State.current_drawing_mode == 'name' and i == line.pending.target_point then - -- create a faint red box for the name - App.color(Current_name_background_color) - local name_text - -- TODO: avoid computing name width on every repaint - if p.name == '' then - name_text = State.em - else - name_text = App.newText(love.graphics.getFont(), p.name) - end - love.graphics.rectangle('fill', x,y, App.width(name_text), State.line_height) - end - end - end - end - App.color(Current_stroke_color) - Drawing.draw_pending_shape(line, line_cache.starty, State.left,State.right) -end - -function Drawing.draw_shape(drawing, shape, top, left,right) - local width = right-left - local function px(x) return Drawing.pixels(x, width)+left end - local function py(y) return Drawing.pixels(y, width)+top end - if shape.mode == 'freehand' then - local prev = nil - for _,point in ipairs(shape.points) do - if prev then - love.graphics.line(px(prev.x),py(prev.y), px(point.x),py(point.y)) - end - prev = point - end - elseif shape.mode == 'line' or shape.mode == 'manhattan' then - local p1 = drawing.points[shape.p1] - local p2 = drawing.points[shape.p2] - love.graphics.line(px(p1.x),py(p1.y), px(p2.x),py(p2.y)) - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - local prev = nil - for _,point in ipairs(shape.vertices) do - local curr = drawing.points[point] - if prev then - love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y)) - end - prev = curr - end - -- close the loop - local curr = drawing.points[shape.vertices[1]] - love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y)) - elseif shape.mode == 'circle' then - -- TODO: clip - local center = drawing.points[shape.center] - love.graphics.circle('line', px(center.x),py(center.y), Drawing.pixels(shape.radius, width)) - elseif shape.mode == 'arc' then - local center = drawing.points[shape.center] - love.graphics.arc('line', 'open', px(center.x),py(center.y), Drawing.pixels(shape.radius, width), shape.start_angle, shape.end_angle, 360) - elseif shape.mode == 'deleted' then - -- ignore - else - print(shape.mode) - assert(false) - end -end - -function Drawing.draw_pending_shape(drawing, top, left,right) - local width = right-left - local pmx,pmy = App.mouse_x(), App.mouse_y() - local function px(x) return Drawing.pixels(x, width)+left end - local function py(y) return Drawing.pixels(y, width)+top end - local mx = Drawing.coord(pmx-left, width) - local my = Drawing.coord(pmy-top, width) - -- recreate pixels from coords to precisely mimic how the drawing will look - -- after mouse_released - pmx,pmy = px(mx), py(my) - local shape = drawing.pending - if shape.mode == nil then - -- nothing pending - elseif shape.mode == 'freehand' then - local shape_copy = deepcopy(shape) - Drawing.smoothen(shape_copy) - Drawing.draw_shape(drawing, shape_copy, top, left,right) - elseif shape.mode == 'line' then - if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then - return - end - local p1 = drawing.points[shape.p1] - love.graphics.line(px(p1.x),py(p1.y), pmx,pmy) - elseif shape.mode == 'manhattan' then - if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then - return - end - local p1 = drawing.points[shape.p1] - if math.abs(mx-p1.x) > math.abs(my-p1.y) then - love.graphics.line(px(p1.x),py(p1.y), pmx, py(p1.y)) - else - love.graphics.line(px(p1.x),py(p1.y), px(p1.x),pmy) - end - elseif shape.mode == 'polygon' then - -- don't close the loop on a pending polygon - local prev = nil - for _,point in ipairs(shape.vertices) do - local curr = drawing.points[point] - if prev then - love.graphics.line(px(prev.x),py(prev.y), px(curr.x),py(curr.y)) - end - prev = curr - end - love.graphics.line(px(prev.x),py(prev.y), pmx,pmy) - elseif shape.mode == 'rectangle' then - local first = drawing.points[shape.vertices[1]] - if #shape.vertices == 1 then - love.graphics.line(px(first.x),py(first.y), pmx,pmy) - return - end - local second = drawing.points[shape.vertices[2]] - local thirdx,thirdy, fourthx,fourthy = Drawing.complete_rectangle(first.x,first.y, second.x,second.y, mx,my) - love.graphics.line(px(first.x),py(first.y), px(second.x),py(second.y)) - love.graphics.line(px(second.x),py(second.y), px(thirdx),py(thirdy)) - love.graphics.line(px(thirdx),py(thirdy), px(fourthx),py(fourthy)) - love.graphics.line(px(fourthx),py(fourthy), px(first.x),py(first.y)) - elseif shape.mode == 'square' then - local first = drawing.points[shape.vertices[1]] - if #shape.vertices == 1 then - love.graphics.line(px(first.x),py(first.y), pmx,pmy) - return - end - local second = drawing.points[shape.vertices[2]] - local thirdx,thirdy, fourthx,fourthy = Drawing.complete_square(first.x,first.y, second.x,second.y, mx,my) - love.graphics.line(px(first.x),py(first.y), px(second.x),py(second.y)) - love.graphics.line(px(second.x),py(second.y), px(thirdx),py(thirdy)) - love.graphics.line(px(thirdx),py(thirdy), px(fourthx),py(fourthy)) - love.graphics.line(px(fourthx),py(fourthy), px(first.x),py(first.y)) - elseif shape.mode == 'circle' then - local center = drawing.points[shape.center] - if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then - return - end - local cx,cy = px(center.x), py(center.y) - love.graphics.circle('line', cx,cy, geom.dist(cx,cy, App.mouse_x(),App.mouse_y())) - elseif shape.mode == 'arc' then - local center = drawing.points[shape.center] - if mx < 0 or mx >= 256 or my < 0 or my >= drawing.h then - return - end - shape.end_angle = geom.angle_with_hint(center.x,center.y, mx,my, shape.end_angle) - local cx,cy = px(center.x), py(center.y) - love.graphics.arc('line', 'open', cx,cy, Drawing.pixels(shape.radius, width), shape.start_angle, shape.end_angle, 360) - elseif shape.mode == 'move' then - -- nothing pending; changes are immediately committed - elseif shape.mode == 'name' then - -- nothing pending; changes are immediately committed - else - print(shape.mode) - assert(false) - end -end - -function Drawing.in_drawing(drawing, line_cache, x,y, left,right) - if line_cache.starty == nil then return false end -- outside current page - local width = right-left - return y >= line_cache.starty and y < line_cache.starty + Drawing.pixels(drawing.h, width) and x >= left and x < right -end - -function Drawing.mouse_pressed(State, drawing_index, x,y, button) - local drawing = State.lines[drawing_index] - local line_cache = State.line_cache[drawing_index] - local cx = Drawing.coord(x-State.left, State.width) - local cy = Drawing.coord(y-line_cache.starty, State.width) - if State.current_drawing_mode == 'freehand' then - drawing.pending = {mode=State.current_drawing_mode, points={{x=cx, y=cy}}} - elseif State.current_drawing_mode == 'line' or State.current_drawing_mode == 'manhattan' then - local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width) - drawing.pending = {mode=State.current_drawing_mode, p1=j} - elseif State.current_drawing_mode == 'polygon' or State.current_drawing_mode == 'rectangle' or State.current_drawing_mode == 'square' then - local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width) - drawing.pending = {mode=State.current_drawing_mode, vertices={j}} - elseif State.current_drawing_mode == 'circle' then - local j = Drawing.find_or_insert_point(drawing.points, cx, cy, State.width) - drawing.pending = {mode=State.current_drawing_mode, center=j} - elseif State.current_drawing_mode == 'move' then - -- all the action is in mouse_released - elseif State.current_drawing_mode == 'name' then - -- nothing - else - print(State.current_drawing_mode) - assert(false) - end -end - --- a couple of operations on drawings need to constantly check the state of the mouse -function Drawing.update(State) - if State.lines.current_drawing == nil then return end - local drawing = State.lines.current_drawing - local line_cache = State.line_cache[State.lines.current_drawing_index] - assert(drawing.mode == 'drawing') - local pmx, pmy = App.mouse_x(), App.mouse_y() - local mx = Drawing.coord(pmx-State.left, State.width) - local my = Drawing.coord(pmy-line_cache.starty, State.width) - if App.mouse_down(1) then - if Drawing.in_drawing(drawing, line_cache, pmx,pmy, State.left,State.right) then - if drawing.pending.mode == 'freehand' then - table.insert(drawing.pending.points, {x=mx, y=my}) - elseif drawing.pending.mode == 'move' then - drawing.pending.target_point.x = mx - drawing.pending.target_point.y = my - Drawing.relax_constraints(drawing, drawing.pending.target_point_index) - end - end - elseif State.current_drawing_mode == 'move' then - if Drawing.in_drawing(drawing, line_cache, pmx, pmy, State.left,State.right) then - drawing.pending.target_point.x = mx - drawing.pending.target_point.y = my - Drawing.relax_constraints(drawing, drawing.pending.target_point_index) - end - else - -- do nothing - end -end - -function Drawing.relax_constraints(drawing, p) - for _,shape in ipairs(drawing.shapes) do - if shape.mode == 'manhattan' then - if shape.p1 == p then - shape.mode = 'line' - elseif shape.p2 == p then - shape.mode = 'line' - end - elseif shape.mode == 'rectangle' or shape.mode == 'square' then - for _,v in ipairs(shape.vertices) do - if v == p then - shape.mode = 'polygon' - end - end - end - end -end - -function Drawing.mouse_released(State, x,y, button) - if State.current_drawing_mode == 'move' then - State.current_drawing_mode = State.previous_drawing_mode - State.previous_drawing_mode = nil - if State.lines.current_drawing then - State.lines.current_drawing.pending = {} - State.lines.current_drawing = nil - end - elseif State.lines.current_drawing then - local drawing = State.lines.current_drawing - local line_cache = State.line_cache[State.lines.current_drawing_index] - if drawing.pending then - if drawing.pending.mode == nil then - -- nothing pending - elseif drawing.pending.mode == 'freehand' then - -- the last point added during update is good enough - Drawing.smoothen(drawing.pending) - table.insert(drawing.shapes, drawing.pending) - elseif drawing.pending.mode == 'line' then - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then - drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, mx,my, State.width) - table.insert(drawing.shapes, drawing.pending) - end - elseif drawing.pending.mode == 'manhattan' then - local p1 = drawing.points[drawing.pending.p1] - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then - if math.abs(mx-p1.x) > math.abs(my-p1.y) then - drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, mx, p1.y, State.width) - else - drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, p1.x, my, State.width) - end - local p2 = drawing.points[drawing.pending.p2] - App.mouse_move(State.left+Drawing.pixels(p2.x, State.width), line_cache.starty+Drawing.pixels(p2.y, State.width)) - table.insert(drawing.shapes, drawing.pending) - end - elseif drawing.pending.mode == 'polygon' then - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then - table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, mx,my, State.width)) - table.insert(drawing.shapes, drawing.pending) - end - elseif drawing.pending.mode == 'rectangle' then - assert(#drawing.pending.vertices <= 2) - if #drawing.pending.vertices == 2 then - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then - local first = drawing.points[drawing.pending.vertices[1]] - local second = drawing.points[drawing.pending.vertices[2]] - local thirdx,thirdy, fourthx,fourthy = Drawing.complete_rectangle(first.x,first.y, second.x,second.y, mx,my) - table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, thirdx,thirdy, State.width)) - table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, fourthx,fourthy, State.width)) - table.insert(drawing.shapes, drawing.pending) - end - else - -- too few points; draw nothing - end - elseif drawing.pending.mode == 'square' then - assert(#drawing.pending.vertices <= 2) - if #drawing.pending.vertices == 2 then - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then - local first = drawing.points[drawing.pending.vertices[1]] - local second = drawing.points[drawing.pending.vertices[2]] - local thirdx,thirdy, fourthx,fourthy = Drawing.complete_square(first.x,first.y, second.x,second.y, mx,my) - table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, thirdx,thirdy, State.width)) - table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, fourthx,fourthy, State.width)) - table.insert(drawing.shapes, drawing.pending) - end - end - elseif drawing.pending.mode == 'circle' then - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then - local center = drawing.points[drawing.pending.center] - drawing.pending.radius = round(geom.dist(center.x,center.y, mx,my)) - table.insert(drawing.shapes, drawing.pending) - end - elseif drawing.pending.mode == 'arc' then - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - if mx >= 0 and mx < 256 and my >= 0 and my < drawing.h then - local center = drawing.points[drawing.pending.center] - drawing.pending.end_angle = geom.angle_with_hint(center.x,center.y, mx,my, drawing.pending.end_angle) - table.insert(drawing.shapes, drawing.pending) - end - elseif drawing.pending.mode == 'name' then - -- drop it - else - print(drawing.pending.mode) - assert(false) - end - State.lines.current_drawing.pending = {} - State.lines.current_drawing = nil - end - end -end - -function Drawing.keychord_pressed(State, chord) - if chord == 'C-p' and not App.mouse_down(1) then - State.current_drawing_mode = 'freehand' - elseif App.mouse_down(1) and chord == 'l' then - State.current_drawing_mode = 'line' - local _,drawing = Drawing.current_drawing(State) - if drawing.pending.mode == 'freehand' then - drawing.pending.p1 = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width) - elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then - drawing.pending.p1 = drawing.pending.vertices[1] - elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then - drawing.pending.p1 = drawing.pending.center - end - drawing.pending.mode = 'line' - elseif chord == 'C-l' and not App.mouse_down(1) then - State.current_drawing_mode = 'line' - elseif App.mouse_down(1) and chord == 'm' then - State.current_drawing_mode = 'manhattan' - local drawing = Drawing.select_drawing_at_mouse(State) - if drawing.pending.mode == 'freehand' then - drawing.pending.p1 = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width) - elseif drawing.pending.mode == 'line' then - -- do nothing - elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then - drawing.pending.p1 = drawing.pending.vertices[1] - elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then - drawing.pending.p1 = drawing.pending.center - end - drawing.pending.mode = 'manhattan' - elseif chord == 'C-m' and not App.mouse_down(1) then - State.current_drawing_mode = 'manhattan' - elseif chord == 'C-g' and not App.mouse_down(1) then - State.current_drawing_mode = 'polygon' - elseif App.mouse_down(1) and chord == 'g' then - State.current_drawing_mode = 'polygon' - local _,drawing = Drawing.current_drawing(State) - if drawing.pending.mode == 'freehand' then - drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)} - elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then - if drawing.pending.vertices == nil then - drawing.pending.vertices = {drawing.pending.p1} - end - elseif drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then - -- reuse existing vertices - elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then - drawing.pending.vertices = {drawing.pending.center} - end - drawing.pending.mode = 'polygon' - elseif chord == 'C-r' and not App.mouse_down(1) then - State.current_drawing_mode = 'rectangle' - elseif App.mouse_down(1) and chord == 'r' then - State.current_drawing_mode = 'rectangle' - local _,drawing = Drawing.current_drawing(State) - if drawing.pending.mode == 'freehand' then - drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)} - elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then - if drawing.pending.vertices == nil then - drawing.pending.vertices = {drawing.pending.p1} - end - elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'square' then - -- reuse existing (1-2) vertices - elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then - drawing.pending.vertices = {drawing.pending.center} - end - drawing.pending.mode = 'rectangle' - elseif chord == 'C-s' and not App.mouse_down(1) then - State.current_drawing_mode = 'square' - elseif App.mouse_down(1) and chord == 's' then - State.current_drawing_mode = 'square' - local _,drawing = Drawing.current_drawing(State) - if drawing.pending.mode == 'freehand' then - drawing.pending.vertices = {Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width)} - elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then - if drawing.pending.vertices == nil then - drawing.pending.vertices = {drawing.pending.p1} - end - elseif drawing.pending.mode == 'polygon' then - while #drawing.pending.vertices > 2 do - table.remove(drawing.pending.vertices) - end - elseif drawing.pending.mode == 'rectangle' then - -- reuse existing (1-2) vertices - elseif drawing.pending.mode == 'circle' or drawing.pending.mode == 'arc' then - drawing.pending.vertices = {drawing.pending.center} - end - drawing.pending.mode = 'square' - elseif App.mouse_down(1) and chord == 'p' and State.current_drawing_mode == 'polygon' then - local _,drawing,line_cache = Drawing.current_drawing(State) - local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-line_cache.starty, State.width) - local j = Drawing.find_or_insert_point(drawing.points, mx,my, State.width) - table.insert(drawing.pending.vertices, j) - elseif App.mouse_down(1) and chord == 'p' and (State.current_drawing_mode == 'rectangle' or State.current_drawing_mode == 'square') then - local _,drawing,line_cache = Drawing.current_drawing(State) - local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-line_cache.starty, State.width) - local j = Drawing.find_or_insert_point(drawing.points, mx,my, State.width) - while #drawing.pending.vertices >= 2 do - table.remove(drawing.pending.vertices) - end - table.insert(drawing.pending.vertices, j) - elseif chord == 'C-o' and not App.mouse_down(1) then - State.current_drawing_mode = 'circle' - elseif App.mouse_down(1) and chord == 'a' and State.current_drawing_mode == 'circle' then - local _,drawing,line_cache = Drawing.current_drawing(State) - drawing.pending.mode = 'arc' - local mx,my = Drawing.coord(App.mouse_x()-State.left, State.width), Drawing.coord(App.mouse_y()-line_cache.starty, State.width) - local center = drawing.points[drawing.pending.center] - drawing.pending.radius = round(geom.dist(center.x,center.y, mx,my)) - drawing.pending.start_angle = geom.angle(center.x,center.y, mx,my) - elseif App.mouse_down(1) and chord == 'o' then - State.current_drawing_mode = 'circle' - local _,drawing = Drawing.current_drawing(State) - if drawing.pending.mode == 'freehand' then - drawing.pending.center = Drawing.find_or_insert_point(drawing.points, drawing.pending.points[1].x, drawing.pending.points[1].y, State.width) - elseif drawing.pending.mode == 'line' or drawing.pending.mode == 'manhattan' then - drawing.pending.center = drawing.pending.p1 - elseif drawing.pending.mode == 'polygon' or drawing.pending.mode == 'rectangle' or drawing.pending.mode == 'square' then - drawing.pending.center = drawing.pending.vertices[1] - end - drawing.pending.mode = 'circle' - elseif chord == 'C-u' and not App.mouse_down(1) then - local drawing_index,drawing,line_cache,i,p = Drawing.select_point_at_mouse(State) - if drawing then - if State.previous_drawing_mode == nil then - State.previous_drawing_mode = State.current_drawing_mode - end - State.current_drawing_mode = 'move' - drawing.pending = {mode=State.current_drawing_mode, target_point=p, target_point_index=i} - State.lines.current_drawing_index = drawing_index - State.lines.current_drawing = drawing - end - elseif chord == 'C-n' and not App.mouse_down(1) then - local drawing_index,drawing,line_cache,point_index,p = Drawing.select_point_at_mouse(State) - if drawing then - if State.previous_drawing_mode == nil then - -- don't clobber - State.previous_drawing_mode = State.current_drawing_mode - end - State.current_drawing_mode = 'name' - p.name = '' - drawing.pending = {mode=State.current_drawing_mode, target_point=point_index} - State.lines.current_drawing_index = drawing_index - State.lines.current_drawing = drawing - end - elseif chord == 'C-d' and not App.mouse_down(1) then - local _,drawing,_,i,p = Drawing.select_point_at_mouse(State) - if drawing then - for _,shape in ipairs(drawing.shapes) do - if Drawing.contains_point(shape, i) then - if shape.mode == 'polygon' then - local idx = table.find(shape.vertices, i) - assert(idx) - table.remove(shape.vertices, idx) - if #shape.vertices < 3 then - shape.mode = 'deleted' - end - else - shape.mode = 'deleted' - end - end - end - drawing.points[i].deleted = true - end - local drawing,_,_,shape = Drawing.select_shape_at_mouse(State) - if drawing then - shape.mode = 'deleted' - end - elseif chord == 'C-h' and not App.mouse_down(1) then - local drawing = Drawing.select_drawing_at_mouse(State) - if drawing then - drawing.show_help = true - end - elseif chord == 'escape' and App.mouse_down(1) then - local _,drawing = Drawing.current_drawing(State) - drawing.pending = {} - end -end - -function Drawing.complete_rectangle(firstx,firsty, secondx,secondy, x,y) - if firstx == secondx then - return x,secondy, x,firsty - end - if firsty == secondy then - return secondx,y, firstx,y - end - local first_slope = (secondy-firsty)/(secondx-firstx) - -- slope of second edge: - -- -1/first_slope - -- equation of line containing the second edge: - -- y-secondy = -1/first_slope*(x-secondx) - -- => 1/first_slope*x + y + (- secondy - secondx/first_slope) = 0 - -- now we want to find the point on this line that's closest to the mouse pointer. - -- https://en.wikipedia.org/wiki/Distance_from_a_point_to_a_line#Line_defined_by_an_equation - local a = 1/first_slope - local c = -secondy - secondx/first_slope - local thirdx = round(((x-a*y) - a*c) / (a*a + 1)) - local thirdy = round((a*(-x + a*y) - c) / (a*a + 1)) - -- slope of third edge = first_slope - -- equation of line containing third edge: - -- y - thirdy = first_slope*(x-thirdx) - -- => -first_slope*x + y + (-thirdy + thirdx*first_slope) = 0 - -- now we want to find the point on this line that's closest to the first point - local a = -first_slope - local c = -thirdy + thirdx*first_slope - local fourthx = round(((firstx-a*firsty) - a*c) / (a*a + 1)) - local fourthy = round((a*(-firstx + a*firsty) - c) / (a*a + 1)) - return thirdx,thirdy, fourthx,fourthy -end - -function Drawing.complete_square(firstx,firsty, secondx,secondy, x,y) - -- use x,y only to decide which side of the first edge to complete the square on - local deltax = secondx-firstx - local deltay = secondy-firsty - local thirdx = secondx+deltay - local thirdy = secondy-deltax - if not geom.same_side(firstx,firsty, secondx,secondy, thirdx,thirdy, x,y) then - deltax = -deltax - deltay = -deltay - thirdx = secondx+deltay - thirdy = secondy-deltax - end - local fourthx = firstx+deltay - local fourthy = firsty-deltax - return thirdx,thirdy, fourthx,fourthy -end - -function Drawing.current_drawing(State) - local x, y = App.mouse_x(), App.mouse_y() - for drawing_index,drawing in ipairs(State.lines) do - if drawing.mode == 'drawing' then - local line_cache = State.line_cache[drawing_index] - if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then - return drawing_index,drawing,line_cache - end - end - end - return nil -end - -function Drawing.select_shape_at_mouse(State) - for drawing_index,drawing in ipairs(State.lines) do - if drawing.mode == 'drawing' then - local x, y = App.mouse_x(), App.mouse_y() - local line_cache = State.line_cache[drawing_index] - if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - for i,shape in ipairs(drawing.shapes) do - assert(shape) - if geom.on_shape(mx,my, drawing, shape) then - return drawing,line_cache,i,shape - end - end - end - end - end -end - -function Drawing.select_point_at_mouse(State) - for drawing_index,drawing in ipairs(State.lines) do - if drawing.mode == 'drawing' then - local x, y = App.mouse_x(), App.mouse_y() - local line_cache = State.line_cache[drawing_index] - if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then - local mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width) - for i,point in ipairs(drawing.points) do - assert(point) - if Drawing.near(point, mx,my, State.width) then - return drawing_index,drawing,line_cache,i,point - end - end - end - end - end -end - -function Drawing.select_drawing_at_mouse(State) - for drawing_index,drawing in ipairs(State.lines) do - if drawing.mode == 'drawing' then - local x, y = App.mouse_x(), App.mouse_y() - local line_cache = State.line_cache[drawing_index] - if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) then - return drawing - end - end - end -end - -function Drawing.contains_point(shape, p) - if shape.mode == 'freehand' then - -- not supported - elseif shape.mode == 'line' or shape.mode == 'manhattan' then - return shape.p1 == p or shape.p2 == p - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - return table.find(shape.vertices, p) - elseif shape.mode == 'circle' then - return shape.center == p - elseif shape.mode == 'arc' then - return shape.center == p - -- ugh, how to support angles - elseif shape.mode == 'deleted' then - -- already done - else - print(shape.mode) - assert(false) - end -end - -function Drawing.smoothen(shape) - assert(shape.mode == 'freehand') - for _=1,7 do - for i=2,#shape.points-1 do - local a = shape.points[i-1] - local b = shape.points[i] - local c = shape.points[i+1] - b.x = round((a.x + b.x + c.x)/3) - b.y = round((a.y + b.y + c.y)/3) - end - end -end - -function round(num) - return math.floor(num+.5) -end - -function Drawing.insert_point(points, x,y) - table.insert(points, {x=x, y=y}) - return #points -end - -function Drawing.find_or_insert_point(points, x,y, width) - -- check if UI would snap the two points together - for i,point in ipairs(points) do - if Drawing.near(point, x,y, width) then - return i - end - end - table.insert(points, {x=x, y=y}) - return #points -end - -function Drawing.near(point, x,y, width) - local px,py = Drawing.pixels(x, width),Drawing.pixels(y, width) - local cx,cy = Drawing.pixels(point.x, width), Drawing.pixels(point.y, width) - return (cx-px)*(cx-px) + (cy-py)*(cy-py) < Same_point_distance*Same_point_distance -end - -function Drawing.pixels(n, width) -- parts to pixels - return math.floor(n*width/256) -end -function Drawing.coord(n, width) -- pixels to parts - return math.floor(n*256/width) -end - -function table.find(h, x) - for k,v in pairs(h) do - if v == x then - return k - end - end -end diff --git a/drawing_tests.lua b/drawing_tests.lua deleted file mode 100644 index f1e39a6..0000000 --- a/drawing_tests.lua +++ /dev/null @@ -1,785 +0,0 @@ --- major tests for drawings --- We minimize assumptions about specific pixels, and try to test at the level --- of specific shapes. In particular, no tests of freehand drawings. - -function test_creating_drawing_saves() - io.write('\ntest_creating_drawing_saves') - App.screen.init{width=120, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{} - Text.redraw_all(Editor_state) - edit.draw(Editor_state) - -- click on button to create drawing - edit.run_after_mouse_click(Editor_state, 8,Editor_state.top+8, 1) - -- file not immediately saved - edit.update(Editor_state, 0.01) - check_nil(App.filesystem['foo'], 'F - test_creating_drawing_saves/early') - -- wait until save - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- filesystem contains drawing and an empty line of text - check_eq(App.filesystem['foo'], '```lines\n```\n\n', 'F - test_creating_drawing_saves') -end - -function test_draw_line() - io.write('\ntest_draw_line') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - check_eq(#Editor_state.lines, 2, 'F - test_draw_line/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_line/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_line/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_line/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_line/baseline/#shapes') - -- draw a line - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_line/#shapes') - check_eq(#drawing.points, 2, 'F - test_draw_line/#points') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_draw_line/shape:1') - local p1 = drawing.points[drawing.shapes[1].p1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p1.x, 5, 'F - test_draw_line/p1:x') - check_eq(p1.y, 6, 'F - test_draw_line/p1:y') - check_eq(p2.x, 35, 'F - test_draw_line/p2:x') - check_eq(p2.y, 36, 'F - test_draw_line/p2:y') - -- wait until save - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- The format on disk isn't perfectly stable. Table fields can be reordered. - -- So just reload from disk to verify. - load_from_disk(Editor_state) - Text.redraw_all(Editor_state) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_line/save/#shapes') - check_eq(#drawing.points, 2, 'F - test_draw_line/save/#points') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_draw_line/save/shape:1') - local p1 = drawing.points[drawing.shapes[1].p1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p1.x, 5, 'F - test_draw_line/save/p1:x') - check_eq(p1.y, 6, 'F - test_draw_line/save/p1:y') - check_eq(p2.x, 35, 'F - test_draw_line/save/p2:x') - check_eq(p2.y, 36, 'F - test_draw_line/save/p2:y') -end - -function test_draw_horizontal_line() - io.write('\ntest_draw_horizontal_line') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'manhattan' - edit.draw(Editor_state) - check_eq(#Editor_state.lines, 2, 'F - test_draw_horizontal_line/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_horizontal_line/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_horizontal_line/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_horizontal_line/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_horizontal_line/baseline/#shapes') - -- draw a line that is more horizontal than vertical - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+26, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_horizontal_line/#shapes') - check_eq(#drawing.points, 2, 'F - test_draw_horizontal_line/#points') - check_eq(drawing.shapes[1].mode, 'manhattan', 'F - test_draw_horizontal_line/shape_mode') - local p1 = drawing.points[drawing.shapes[1].p1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p1.x, 5, 'F - test_draw_horizontal_line/p1:x') - check_eq(p1.y, 6, 'F - test_draw_horizontal_line/p1:y') - check_eq(p2.x, 35, 'F - test_draw_horizontal_line/p2:x') - check_eq(p2.y, p1.y, 'F - test_draw_horizontal_line/p2:y') -end - -function test_draw_circle() - io.write('\ntest_draw_circle') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - check_eq(#Editor_state.lines, 2, 'F - test_draw_circle/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_circle/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_circle/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_circle/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_circle/baseline/#shapes') - -- draw a circle - App.mouse_move(Editor_state.left+4, Editor_state.top+Drawing_padding_top+4) -- hover on drawing - edit.run_after_keychord(Editor_state, 'C-o') - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35+30, Editor_state.top+Drawing_padding_top+36, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_circle/#shapes') - check_eq(#drawing.points, 1, 'F - test_draw_circle/#points') - check_eq(drawing.shapes[1].mode, 'circle', 'F - test_draw_horizontal_line/shape_mode') - check_eq(drawing.shapes[1].radius, 30, 'F - test_draw_circle/radius') - local center = drawing.points[drawing.shapes[1].center] - check_eq(center.x, 35, 'F - test_draw_circle/center:x') - check_eq(center.y, 36, 'F - test_draw_circle/center:y') -end - -function test_cancel_stroke() - io.write('\ntest_cancel_stroke') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - check_eq(#Editor_state.lines, 2, 'F - test_cancel_stroke/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_cancel_stroke/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_cancel_stroke/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_cancel_stroke/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_cancel_stroke/baseline/#shapes') - -- start drawing a line - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - -- cancel - edit.run_after_keychord(Editor_state, 'escape') - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 0, 'F - test_cancel_stroke/#shapes') -end - -function test_keys_do_not_affect_shape_when_mouse_up() - io.write('\ntest_keys_do_not_affect_shape_when_mouse_up') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - -- hover over drawing and press 'o' without holding mouse - App.mouse_move(Editor_state.left+4, Editor_state.top+Drawing_padding_top+4) -- hover on drawing - edit.run_after_keychord(Editor_state, 'o') - -- no change to drawing mode - check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_keys_do_not_affect_shape_when_mouse_up/drawing_mode') - -- no change to text either because we didn't run the textinput event -end - -function test_draw_circle_mid_stroke() - io.write('\ntest_draw_circle_mid_stroke') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - check_eq(#Editor_state.lines, 2, 'F - test_draw_circle_mid_stroke/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_circle_mid_stroke/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_circle_mid_stroke/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_circle_mid_stroke/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_circle_mid_stroke/baseline/#shapes') - -- draw a circle - App.mouse_move(Editor_state.left+4, Editor_state.top+Drawing_padding_top+4) -- hover on drawing - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_keychord(Editor_state, 'o') - edit.run_after_mouse_release(Editor_state, Editor_state.left+35+30, Editor_state.top+Drawing_padding_top+36, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_circle_mid_stroke/#shapes') - check_eq(#drawing.points, 1, 'F - test_draw_circle_mid_stroke/#points') - check_eq(drawing.shapes[1].mode, 'circle', 'F - test_draw_horizontal_line/shape_mode') - check_eq(drawing.shapes[1].radius, 30, 'F - test_draw_circle_mid_stroke/radius') - local center = drawing.points[drawing.shapes[1].center] - check_eq(center.x, 35, 'F - test_draw_circle_mid_stroke/center:x') - check_eq(center.y, 36, 'F - test_draw_circle_mid_stroke/center:y') -end - -function test_draw_arc() - io.write('\ntest_draw_arc') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'circle' - edit.draw(Editor_state) - check_eq(#Editor_state.lines, 2, 'F - test_draw_arc/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_arc/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_arc/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_arc/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_arc/baseline/#shapes') - -- draw an arc - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - App.mouse_move(Editor_state.left+35+30, Editor_state.top+Drawing_padding_top+36) - edit.run_after_keychord(Editor_state, 'a') -- arc mode - edit.run_after_mouse_release(Editor_state, Editor_state.left+35+50, Editor_state.top+Drawing_padding_top+36+50, 1) -- 45° - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_arc/#shapes') - check_eq(#drawing.points, 1, 'F - test_draw_arc/#points') - check_eq(drawing.shapes[1].mode, 'arc', 'F - test_draw_horizontal_line/shape_mode') - local arc = drawing.shapes[1] - check_eq(arc.radius, 30, 'F - test_draw_arc/radius') - local center = drawing.points[arc.center] - check_eq(center.x, 35, 'F - test_draw_arc/center:x') - check_eq(center.y, 36, 'F - test_draw_arc/center:y') - check_eq(arc.start_angle, 0, 'F - test_draw_arc/start:angle') - check_eq(arc.end_angle, math.pi/4, 'F - test_draw_arc/end:angle') -end - -function test_draw_polygon() - io.write('\ntest_draw_polygon') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - edit.draw(Editor_state) - check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_polygon/baseline/drawing_mode') - check_eq(#Editor_state.lines, 2, 'F - test_draw_polygon/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_polygon/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_polygon/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_polygon/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_polygon/baseline/#shapes') - -- first point - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_keychord(Editor_state, 'g') -- polygon mode - -- second point - App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36) - edit.run_after_keychord(Editor_state, 'p') -- add point - -- final point - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+26, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_polygon/#shapes') - check_eq(#drawing.points, 3, 'F - test_draw_polygon/vertices') - local shape = drawing.shapes[1] - check_eq(shape.mode, 'polygon', 'F - test_draw_polygon/shape_mode') - check_eq(#shape.vertices, 3, 'F - test_draw_polygon/vertices') - local p = drawing.points[shape.vertices[1]] - check_eq(p.x, 5, 'F - test_draw_polygon/p1:x') - check_eq(p.y, 6, 'F - test_draw_polygon/p1:y') - local p = drawing.points[shape.vertices[2]] - check_eq(p.x, 65, 'F - test_draw_polygon/p2:x') - check_eq(p.y, 36, 'F - test_draw_polygon/p2:y') - local p = drawing.points[shape.vertices[3]] - check_eq(p.x, 35, 'F - test_draw_polygon/p3:x') - check_eq(p.y, 26, 'F - test_draw_polygon/p3:y') -end - -function test_draw_rectangle() - io.write('\ntest_draw_rectangle') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - edit.draw(Editor_state) - check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_rectangle/baseline/drawing_mode') - check_eq(#Editor_state.lines, 2, 'F - test_draw_rectangle/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_rectangle/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_rectangle/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_rectangle/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_rectangle/baseline/#shapes') - -- first point - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_keychord(Editor_state, 'r') -- rectangle mode - -- second point/first edge - App.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45) - edit.run_after_keychord(Editor_state, 'p') - -- override second point/first edge - App.mouse_move(Editor_state.left+75, Editor_state.top+Drawing_padding_top+76) - edit.run_after_keychord(Editor_state, 'p') - -- release (decides 'thickness' of rectangle perpendicular to first edge) - edit.run_after_mouse_release(Editor_state, Editor_state.left+15, Editor_state.top+Drawing_padding_top+26, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_rectangle/#shapes') - check_eq(#drawing.points, 5, 'F - test_draw_rectangle/#points') -- currently includes every point added - local shape = drawing.shapes[1] - check_eq(shape.mode, 'rectangle', 'F - test_draw_rectangle/shape_mode') - check_eq(#shape.vertices, 4, 'F - test_draw_rectangle/vertices') - local p = drawing.points[shape.vertices[1]] - check_eq(p.x, 35, 'F - test_draw_rectangle/p1:x') - check_eq(p.y, 36, 'F - test_draw_rectangle/p1:y') - local p = drawing.points[shape.vertices[2]] - check_eq(p.x, 75, 'F - test_draw_rectangle/p2:x') - check_eq(p.y, 76, 'F - test_draw_rectangle/p2:y') - local p = drawing.points[shape.vertices[3]] - check_eq(p.x, 70, 'F - test_draw_rectangle/p3:x') - check_eq(p.y, 81, 'F - test_draw_rectangle/p3:y') - local p = drawing.points[shape.vertices[4]] - check_eq(p.x, 30, 'F - test_draw_rectangle/p4:x') - check_eq(p.y, 41, 'F - test_draw_rectangle/p4:y') -end - -function test_draw_rectangle_intermediate() - io.write('\ntest_draw_rectangle_intermediate') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - edit.draw(Editor_state) - check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_rectangle_intermediate/baseline/drawing_mode') - check_eq(#Editor_state.lines, 2, 'F - test_draw_rectangle_intermediate/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_rectangle_intermediate/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_rectangle_intermediate/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_rectangle_intermediate/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_rectangle_intermediate/baseline/#shapes') - -- first point - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_keychord(Editor_state, 'r') -- rectangle mode - -- second point/first edge - App.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45) - edit.run_after_keychord(Editor_state, 'p') - -- override second point/first edge - App.mouse_move(Editor_state.left+75, Editor_state.top+Drawing_padding_top+76) - edit.run_after_keychord(Editor_state, 'p') - local drawing = Editor_state.lines[1] - check_eq(#drawing.points, 3, 'F - test_draw_rectangle_intermediate/#points') -- currently includes every point added - local pending = drawing.pending - check_eq(pending.mode, 'rectangle', 'F - test_draw_rectangle_intermediate/shape_mode') - check_eq(#pending.vertices, 2, 'F - test_draw_rectangle_intermediate/vertices') - local p = drawing.points[pending.vertices[1]] - check_eq(p.x, 35, 'F - test_draw_rectangle_intermediate/p1:x') - check_eq(p.y, 36, 'F - test_draw_rectangle_intermediate/p1:y') - local p = drawing.points[pending.vertices[2]] - check_eq(p.x, 75, 'F - test_draw_rectangle_intermediate/p2:x') - check_eq(p.y, 76, 'F - test_draw_rectangle_intermediate/p2:y') - -- outline of rectangle is drawn based on where the mouse is, but we can't check that so far -end - -function test_draw_square() - io.write('\ntest_draw_square') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - edit.draw(Editor_state) - check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_draw_square/baseline/drawing_mode') - check_eq(#Editor_state.lines, 2, 'F - test_draw_square/baseline/#lines') - check_eq(Editor_state.lines[1].mode, 'drawing', 'F - test_draw_square/baseline/mode') - check_eq(Editor_state.line_cache[1].starty, Editor_state.top+Drawing_padding_top, 'F - test_draw_square/baseline/y') - check_eq(Editor_state.lines[1].h, 128, 'F - test_draw_square/baseline/y') - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_draw_square/baseline/#shapes') - -- first point - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_keychord(Editor_state, 's') -- square mode - -- second point/first edge - App.mouse_move(Editor_state.left+42, Editor_state.top+Drawing_padding_top+45) - edit.run_after_keychord(Editor_state, 'p') - -- override second point/first edge - App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+66) - edit.run_after_keychord(Editor_state, 'p') - -- release (decides which side of first edge to draw square on) - edit.run_after_mouse_release(Editor_state, Editor_state.left+15, Editor_state.top+Drawing_padding_top+26, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_draw_square/#shapes') - check_eq(#drawing.points, 5, 'F - test_draw_square/#points') -- currently includes every point added - check_eq(drawing.shapes[1].mode, 'square', 'F - test_draw_square/shape_mode') - check_eq(#drawing.shapes[1].vertices, 4, 'F - test_draw_square/vertices') - local p = drawing.points[drawing.shapes[1].vertices[1]] - check_eq(p.x, 35, 'F - test_draw_square/p1:x') - check_eq(p.y, 36, 'F - test_draw_square/p1:y') - local p = drawing.points[drawing.shapes[1].vertices[2]] - check_eq(p.x, 65, 'F - test_draw_square/p2:x') - check_eq(p.y, 66, 'F - test_draw_square/p2:y') - local p = drawing.points[drawing.shapes[1].vertices[3]] - check_eq(p.x, 35, 'F - test_draw_square/p3:x') - check_eq(p.y, 96, 'F - test_draw_square/p3:y') - local p = drawing.points[drawing.shapes[1].vertices[4]] - check_eq(p.x, 5, 'F - test_draw_square/p4:x') - check_eq(p.y, 66, 'F - test_draw_square/p4:y') -end - -function test_name_point() - io.write('\ntest_name_point') - -- create a drawing with a line - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - -- draw a line - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_name_point/baseline/#shapes') - check_eq(#drawing.points, 2, 'F - test_name_point/baseline/#points') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_name_point/baseline/shape:1') - local p1 = drawing.points[drawing.shapes[1].p1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p1.x, 5, 'F - test_name_point/baseline/p1:x') - check_eq(p1.y, 6, 'F - test_name_point/baseline/p1:y') - check_eq(p2.x, 35, 'F - test_name_point/baseline/p2:x') - check_eq(p2.y, 36, 'F - test_name_point/baseline/p2:y') - check_nil(p2.name, 'F - test_name_point/baseline/p2:name') - -- enter 'name' mode without moving the mouse - edit.run_after_keychord(Editor_state, 'C-n') - check_eq(Editor_state.current_drawing_mode, 'name', 'F - test_name_point/mode:1') - edit.run_after_textinput(Editor_state, 'A') - check_eq(p2.name, 'A', 'F - test_name_point') - -- still in 'name' mode - check_eq(Editor_state.current_drawing_mode, 'name', 'F - test_name_point/mode:2') - -- exit 'name' mode - edit.run_after_keychord(Editor_state, 'return') - check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_name_point/mode:3') - check_eq(p2.name, 'A', 'F - test_name_point') - -- wait until save - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- change is saved - load_from_disk(Editor_state) - Text.redraw_all(Editor_state) - local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2] - check_eq(p2.name, 'A', 'F - test_name_point/save') -end - -function test_move_point() - io.write('\ntest_move_point') - -- create a drawing with a line - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_move_point/baseline/#shapes') - check_eq(#drawing.points, 2, 'F - test_move_point/baseline/#points') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_move_point/baseline/shape:1') - local p1 = drawing.points[drawing.shapes[1].p1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p1.x, 5, 'F - test_move_point/baseline/p1:x') - check_eq(p1.y, 6, 'F - test_move_point/baseline/p1:y') - check_eq(p2.x, 35, 'F - test_move_point/baseline/p2:x') - check_eq(p2.y, 36, 'F - test_move_point/baseline/p2:y') - -- wait until save - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- line is saved to disk - load_from_disk(Editor_state) - Text.redraw_all(Editor_state) - local drawing = Editor_state.lines[1] - local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2] - check_eq(p2.x, 35, 'F - test_move_point/save/x') - check_eq(p2.y, 36, 'F - test_move_point/save/y') - edit.draw(Editor_state) - -- enter 'move' mode without moving the mouse - edit.run_after_keychord(Editor_state, 'C-u') - check_eq(Editor_state.current_drawing_mode, 'move', 'F - test_move_point/mode:1') - -- point is lifted - check_eq(drawing.pending.mode, 'move', 'F - test_move_point/mode:2') - check_eq(drawing.pending.target_point, p2, 'F - test_move_point/target') - -- move point - App.mouse_move(Editor_state.left+26, Editor_state.top+Drawing_padding_top+44) - edit.update(Editor_state, 0.05) - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p2.x, 26, 'F - test_move_point/x') - check_eq(p2.y, 44, 'F - test_move_point/y') - -- exit 'move' mode - edit.run_after_mouse_click(Editor_state, Editor_state.left+26, Editor_state.top+Drawing_padding_top+44, 1) - check_eq(Editor_state.current_drawing_mode, 'line', 'F - test_move_point/mode:3') - check_eq(drawing.pending, {}, 'F - test_move_point/pending') - -- wait until save - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- change is saved - load_from_disk(Editor_state) - Text.redraw_all(Editor_state) - local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2] - check_eq(p2.x, 26, 'F - test_move_point/save/x') - check_eq(p2.y, 44, 'F - test_move_point/save/y') -end - -function test_move_point_on_manhattan_line() - io.write('\ntest_move_point_on_manhattan_line') - -- create a drawing with a manhattan line - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'manhattan' - edit.draw(Editor_state) - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+46, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_move_point_on_manhattan_line/baseline/#shapes') - check_eq(#drawing.points, 2, 'F - test_move_point_on_manhattan_line/baseline/#points') - check_eq(drawing.shapes[1].mode, 'manhattan', 'F - test_move_point_on_manhattan_line/baseline/shape:1') - edit.draw(Editor_state) - -- enter 'move' mode - edit.run_after_keychord(Editor_state, 'C-u') - check_eq(Editor_state.current_drawing_mode, 'move', 'F - test_move_point_on_manhattan_line/mode:1') - -- move point - App.mouse_move(Editor_state.left+26, Editor_state.top+Drawing_padding_top+44) - edit.update(Editor_state, 0.05) - -- line is no longer manhattan - check_eq(drawing.shapes[1].mode, 'line', 'F - test_move_point_on_manhattan_line/baseline/shape:1') -end - -function test_delete_lines_at_point() - io.write('\ntest_delete_lines_at_point') - -- create a drawing with two lines connected at a point - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+55, Editor_state.top+Drawing_padding_top+26, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 2, 'F - test_delete_lines_at_point/baseline/#shapes') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_delete_lines_at_point/baseline/shape:1') - check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_lines_at_point/baseline/shape:2') - -- hover on the common point and delete - App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+36) - edit.run_after_keychord(Editor_state, 'C-d') - check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_lines_at_point/shape:1') - check_eq(drawing.shapes[2].mode, 'deleted', 'F - test_delete_lines_at_point/shape:2') - -- wait for some time - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- deleted points disappear after file is reloaded - load_from_disk(Editor_state) - Text.redraw_all(Editor_state) - check_eq(#Editor_state.lines[1].shapes, 0, 'F - test_delete_lines_at_point/save') -end - -function test_delete_line_under_mouse_pointer() - io.write('\ntest_delete_line_under_mouse_pointer') - -- create a drawing with two lines connected at a point - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+55, Editor_state.top+Drawing_padding_top+26, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 2, 'F - test_delete_line_under_mouse_pointer/baseline/#shapes') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_delete_line_under_mouse_pointer/baseline/shape:1') - check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_line_under_mouse_pointer/baseline/shape:2') - -- hover on one of the lines and delete - App.mouse_move(Editor_state.left+25, Editor_state.top+Drawing_padding_top+26) - edit.run_after_keychord(Editor_state, 'C-d') - -- only that line is deleted - check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_line_under_mouse_pointer/shape:1') - check_eq(drawing.shapes[2].mode, 'line', 'F - test_delete_line_under_mouse_pointer/shape:2') -end - -function test_delete_point_from_polygon() - io.write('\ntest_delete_point_from_polygon') - -- create a drawing with two lines connected at a point - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - -- first point - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_keychord(Editor_state, 'g') -- polygon mode - -- second point - App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36) - edit.run_after_keychord(Editor_state, 'p') -- add point - -- third point - App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+26) - edit.run_after_keychord(Editor_state, 'p') -- add point - -- fourth point - edit.run_after_mouse_release(Editor_state, Editor_state.left+14, Editor_state.top+Drawing_padding_top+16, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_delete_point_from_polygon/baseline/#shapes') - check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/baseline/mode') - check_eq(#drawing.shapes[1].vertices, 4, 'F - test_delete_point_from_polygon/baseline/vertices') - -- hover on a point and delete - App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+26) - edit.run_after_keychord(Editor_state, 'C-d') - -- just the one point is deleted - check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/shape') - check_eq(#drawing.shapes[1].vertices, 3, 'F - test_delete_point_from_polygon/vertices') -end - -function test_delete_point_from_polygon() - io.write('\ntest_delete_point_from_polygon') - -- create a drawing with two lines connected at a point - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - -- first point - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_keychord(Editor_state, 'g') -- polygon mode - -- second point - App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36) - edit.run_after_keychord(Editor_state, 'p') -- add point - -- third point - edit.run_after_mouse_release(Editor_state, Editor_state.left+14, Editor_state.top+Drawing_padding_top+16, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_delete_point_from_polygon/baseline/#shapes') - check_eq(drawing.shapes[1].mode, 'polygon', 'F - test_delete_point_from_polygon/baseline/mode') - check_eq(#drawing.shapes[1].vertices, 3, 'F - test_delete_point_from_polygon/baseline/vertices') - -- hover on a point and delete - App.mouse_move(Editor_state.left+65, Editor_state.top+Drawing_padding_top+36) - edit.run_after_keychord(Editor_state, 'C-d') - -- there's < 3 points left, so the whole polygon is deleted - check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_delete_point_from_polygon') -end - -function test_undo_name_point() - io.write('\ntest_undo_name_point') - -- create a drawing with a line - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - -- draw a line - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_undo_name_point/baseline/#shapes') - check_eq(#drawing.points, 2, 'F - test_undo_name_point/baseline/#points') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_name_point/baseline/shape:1') - local p1 = drawing.points[drawing.shapes[1].p1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p1.x, 5, 'F - test_undo_name_point/baseline/p1:x') - check_eq(p1.y, 6, 'F - test_undo_name_point/baseline/p1:y') - check_eq(p2.x, 35, 'F - test_undo_name_point/baseline/p2:x') - check_eq(p2.y, 36, 'F - test_undo_name_point/baseline/p2:y') - check_nil(p2.name, 'F - test_undo_name_point/baseline/p2:name') - check_eq(#Editor_state.history, 1, 'F - test_undo_name_point/baseline/history:1') ---? print('a', Editor_state.lines.current_drawing) - -- enter 'name' mode without moving the mouse - edit.run_after_keychord(Editor_state, 'C-n') - edit.run_after_textinput(Editor_state, 'A') - edit.run_after_keychord(Editor_state, 'return') - check_eq(p2.name, 'A', 'F - test_undo_name_point/baseline') - check_eq(#Editor_state.history, 3, 'F - test_undo_name_point/baseline/history:2') - check_eq(Editor_state.next_history, 4, 'F - test_undo_name_point/baseline/next_history') ---? print('b', Editor_state.lines.current_drawing) - -- undo - edit.run_after_keychord(Editor_state, 'C-z') - local drawing = Editor_state.lines[1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(Editor_state.next_history, 3, 'F - test_undo_name_point/next_history') - check_eq(p2.name, '', 'F - test_undo_name_point') -- not quite what it was before, but close enough - -- wait until save - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- undo is saved - load_from_disk(Editor_state) - Text.redraw_all(Editor_state) - local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2] - check_eq(p2.name, '', 'F - test_undo_name_point/save') -end - -function test_undo_move_point() - io.write('\ntest_undo_move_point') - -- create a drawing with a line - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 1, 'F - test_undo_move_point/baseline/#shapes') - check_eq(#drawing.points, 2, 'F - test_undo_move_point/baseline/#points') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_move_point/baseline/shape:1') - local p1 = drawing.points[drawing.shapes[1].p1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p1.x, 5, 'F - test_undo_move_point/baseline/p1:x') - check_eq(p1.y, 6, 'F - test_undo_move_point/baseline/p1:y') - check_eq(p2.x, 35, 'F - test_undo_move_point/baseline/p2:x') - check_eq(p2.y, 36, 'F - test_undo_move_point/baseline/p2:y') - check_nil(p2.name, 'F - test_undo_move_point/baseline/p2:name') - -- move p2 - edit.run_after_keychord(Editor_state, 'C-u') - App.mouse_move(Editor_state.left+26, Editor_state.top+Drawing_padding_top+44) - edit.update(Editor_state, 0.05) - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(p2.x, 26, 'F - test_undo_move_point/x') - check_eq(p2.y, 44, 'F - test_undo_move_point/y') - -- exit 'move' mode - edit.run_after_mouse_click(Editor_state, Editor_state.left+26, Editor_state.top+Drawing_padding_top+44, 1) - check_eq(Editor_state.next_history, 4, 'F - test_undo_move_point/next_history') - -- undo - edit.run_after_keychord(Editor_state, 'C-z') - edit.run_after_keychord(Editor_state, 'C-z') -- bug: need to undo twice - local drawing = Editor_state.lines[1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(Editor_state.next_history, 2, 'F - test_undo_move_point/next_history') - check_eq(p2.x, 35, 'F - test_undo_move_point/x') - check_eq(p2.y, 36, 'F - test_undo_move_point/y') - -- wait until save - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- undo is saved - load_from_disk(Editor_state) - Text.redraw_all(Editor_state) - local p2 = Editor_state.lines[1].points[drawing.shapes[1].p2] - check_eq(p2.x, 35, 'F - test_undo_move_point/save/x') - check_eq(p2.y, 36, 'F - test_undo_move_point/save/y') -end - -function test_undo_delete_point() - io.write('\ntest_undo_delete_point') - -- create a drawing with two lines connected at a point - App.screen.init{width=Test_margin_left+256, height=300} -- drawing coordinates 1:1 with pixels - Editor_state = edit.initialize_test_state() - Editor_state.filename = 'foo' - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - Editor_state.current_drawing_mode = 'line' - edit.draw(Editor_state) - edit.run_after_mouse_press(Editor_state, Editor_state.left+5, Editor_state.top+Drawing_padding_top+6, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_mouse_press(Editor_state, Editor_state.left+35, Editor_state.top+Drawing_padding_top+36, 1) - edit.run_after_mouse_release(Editor_state, Editor_state.left+55, Editor_state.top+Drawing_padding_top+26, 1) - local drawing = Editor_state.lines[1] - check_eq(#drawing.shapes, 2, 'F - test_undo_delete_point/baseline/#shapes') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_delete_point/baseline/shape:1') - check_eq(drawing.shapes[2].mode, 'line', 'F - test_undo_delete_point/baseline/shape:2') - -- hover on the common point and delete - App.mouse_move(Editor_state.left+35, Editor_state.top+Drawing_padding_top+36) - edit.run_after_keychord(Editor_state, 'C-d') - check_eq(drawing.shapes[1].mode, 'deleted', 'F - test_undo_delete_point/shape:1') - check_eq(drawing.shapes[2].mode, 'deleted', 'F - test_undo_delete_point/shape:2') - -- undo - edit.run_after_keychord(Editor_state, 'C-z') - local drawing = Editor_state.lines[1] - local p2 = drawing.points[drawing.shapes[1].p2] - check_eq(Editor_state.next_history, 3, 'F - test_undo_move_point/next_history') - check_eq(drawing.shapes[1].mode, 'line', 'F - test_undo_delete_point/shape:1') - check_eq(drawing.shapes[2].mode, 'line', 'F - test_undo_delete_point/shape:2') - -- wait until save - App.wait_fake_time(3.1) - edit.update(Editor_state, 0) - -- undo is saved - load_from_disk(Editor_state) - Text.redraw_all(Editor_state) - check_eq(#Editor_state.lines[1].shapes, 2, 'F - test_undo_delete_point/save') -end diff --git a/edit.lua b/edit.lua index 011f6b9..e8a8f03 100644 --- a/edit.lua +++ b/edit.lua @@ -1,60 +1,24 @@ -- some constants people might like to tweak Text_color = {r=0, g=0, b=0} Cursor_color = {r=1, g=0, b=0} -Stroke_color = {r=0, g=0, b=0} -Current_stroke_color = {r=0.7, g=0.7, b=0.7} -- in process of being drawn -Current_name_background_color = {r=1, g=0, b=0, a=0.1} -- name currently being edited Focus_stroke_color = {r=1, g=0, b=0} -- what mouse is hovering over Highlight_color = {r=0.7, g=0.7, b=0.9} -- selected text -Icon_color = {r=0.7, g=0.7, b=0.7} -- color of current mode icon in drawings -Help_color = {r=0, g=0.5, b=0} -Help_background_color = {r=0, g=0.5, b=0, a=0.1} Margin_top = 15 Margin_left = 25 Margin_right = 25 -Drawing_padding_top = 10 -Drawing_padding_bottom = 10 -Drawing_padding_height = Drawing_padding_top + Drawing_padding_bottom - -Same_point_distance = 4 -- pixel distance at which two points are considered the same - utf8 = require 'utf8' require 'file' require 'text' -require 'drawing' -require 'geom' -require 'help' -require 'icons' edit = {} -- run in both tests and a real run function edit.initialize_state(top, left, right, font_height, line_height) -- currently always draws to bottom of screen local result = { - -- a line is either text or a drawing - -- a text is a table with: - -- mode = 'text', - -- string data, - -- a drawing is a table with: - -- mode = 'drawing' - -- a (y) coord in pixels (updated while painting screen), - -- a (h)eight, - -- an array of points, and - -- an array of shapes - -- a shape is a table containing: - -- a mode - -- an array points for mode 'freehand' (raw x,y coords; freehand drawings don't pollute the points array of a drawing) - -- an array vertices for mode 'polygon', 'rectangle', 'square' - -- p1, p2 for mode 'line' - -- center, radius for mode 'circle' - -- center, radius, start_angle, end_angle for mode 'arc' - -- Unless otherwise specified, coord fields are normalized; a drawing is always 256 units wide - -- The field names are carefully chosen so that switching modes in midstream - -- remembers previously entered points where that makes sense. - lines = {{mode='text', data=''}}, -- array of lines + lines = {{data=''}}, -- array of strings -- Lines can be too long to fit on screen, in which case they _wrap_ into -- multiple _screen lines_. @@ -91,9 +55,6 @@ function edit.initialize_state(top, left, right, font_height, line_height) -- c cursor_x = 0, cursor_y = 0, - current_drawing_mode = 'line', - previous_drawing_mode = nil, -- extra state for some ephemeral modes like moving/deleting/naming points - font_height = font_height, line_height = line_height, em = App.newText(love.graphics.getFont(), 'm'), -- widest possible character width @@ -118,15 +79,6 @@ function edit.initialize_state(top, left, right, font_height, line_height) -- c return result end -- App.initialize_state -function edit.fixup_cursor(State) - for i,line in ipairs(State.lines) do - if line.mode == 'text' then - State.cursor1.line = i - break - end - end -end - function edit.draw(State) App.color(Text_color) assert(#State.lines == #State.line_cache) @@ -143,39 +95,14 @@ function edit.draw(State) --? print('draw:', y, line_index, line) if y + State.line_height > App.screen.height then break end State.screen_bottom1.line = line_index - if line.mode == 'text' then ---? print('text.draw', y, line_index) - local startpos = 1 - if line_index == State.screen_top1.line then - startpos = State.screen_top1.pos - end - if line.data == '' then - -- button to insert new drawing - button('draw', {x=4,y=y+4, w=12,h=12, color={1,1,0}, - icon = icon.insert_drawing, - onpress1 = function() - Drawing.before = snapshot(State, line_index-1, line_index) - table.insert(State.lines, line_index, {mode='drawing', y=y, h=256/2, points={}, shapes={}, pending={}}) - table.insert(State.line_cache, line_index, {}) - if State.cursor1.line >= line_index then - State.cursor1.line = State.cursor1.line+1 - end - schedule_save(State) - record_undo_event(State, {before=Drawing.before, after=snapshot(State, line_index-1, line_index+1)}) - end, - }) - end - y, State.screen_bottom1.pos = Text.draw(State, line_index, y, startpos) - y = y + State.line_height ---? print('=> y', y) - elseif line.mode == 'drawing' then - y = y+Drawing_padding_top - Drawing.draw(State, line_index, y) - y = y + Drawing.pixels(line.h, State.width) + Drawing_padding_bottom - else - print(line.mode) - assert(false) +--? print('text.draw', y, line_index) + local startpos = 1 + if line_index == State.screen_top1.line then + startpos = State.screen_top1.pos end + y, State.screen_bottom1.pos = Text.draw(State, line_index, y, startpos) + y = y + State.line_height +--? print('=> y', y) end --? print('screen bottom: '..tostring(State.screen_bottom1.pos)..' in '..tostring(State.lines[State.screen_bottom1.line].data)) if State.search_term then @@ -184,7 +111,6 @@ function edit.draw(State) end function edit.update(State, dt) - Drawing.update(State, dt) if State.next_save and State.next_save < App.getTime() then save_to_disk(State) State.next_save = nil @@ -210,36 +136,25 @@ function edit.mouse_pressed(State, x,y, mouse_button) propagate_to_button_handlers(x,y, mouse_button) for line_index,line in ipairs(State.lines) do - if line.mode == 'text' then - if Text.in_line(State, line_index, x,y) then - -- delicate dance between cursor, selection and old cursor/selection - -- scenarios: - -- regular press+release: sets cursor, clears selection - -- shift press+release: - -- sets selection to old cursor if not set otherwise leaves it untouched - -- sets cursor - -- press and hold to start a selection: sets selection on press, cursor on release - -- press and hold, then press shift: ignore shift - -- i.e. mouse_released should never look at shift state - State.old_cursor1 = State.cursor1 - State.old_selection1 = State.selection1 - State.mousepress_shift = App.shift_down() - State.selection1 = { - line=line_index, - pos=Text.to_pos_on_line(State, line_index, x, y), - } ---? print('selection', State.selection1.line, State.selection1.pos) - break - end - elseif line.mode == 'drawing' then - local line_cache = State.line_cache[line_index] - if Drawing.in_drawing(line, line_cache, x, y, State.left,State.right) then - State.lines.current_drawing_index = line_index - State.lines.current_drawing = line - Drawing.before = snapshot(State, line_index) - Drawing.mouse_pressed(State, line_index, x,y, mouse_button) - break - end + if Text.in_line(State, line_index, x,y) then + -- delicate dance between cursor, selection and old cursor/selection + -- scenarios: + -- regular press+release: sets cursor, clears selection + -- shift press+release: + -- sets selection to old cursor if not set otherwise leaves it untouched + -- sets cursor + -- press and hold to start a selection: sets selection on press, cursor on release + -- press and hold, then press shift: ignore shift + -- i.e. mouse_released should never look at shift state + State.old_cursor1 = State.cursor1 + State.old_selection1 = State.selection1 + State.mousepress_shift = App.shift_down() + State.selection1 = { + line=line_index, + pos=Text.to_pos_on_line(State, line_index, x, y), + } +--? print('selection', State.selection1.line, State.selection1.pos) + break end end end @@ -247,40 +162,29 @@ end function edit.mouse_released(State, x,y, mouse_button) if State.search_term then return end --? print('release') - if State.lines.current_drawing then - Drawing.mouse_released(State, x,y, mouse_button) - schedule_save(State) - if Drawing.before then - record_undo_event(State, {before=Drawing.before, after=snapshot(State, State.lines.current_drawing_index)}) - Drawing.before = nil - end - else - for line_index,line in ipairs(State.lines) do - if line.mode == 'text' then - if Text.in_line(State, line_index, x,y) then ---? print('reset selection') - State.cursor1 = { - line=line_index, - pos=Text.to_pos_on_line(State, line_index, x, y), - } ---? print('cursor', State.cursor1.line, State.cursor1.pos) - if State.mousepress_shift then - if State.old_selection1.line == nil then - State.selection1 = State.old_cursor1 - else - State.selection1 = State.old_selection1 - end - end - State.old_cursor1, State.old_selection1, State.mousepress_shift = nil - if eq(State.cursor1, State.selection1) then - State.selection1 = {} - end - break + for line_index,line in ipairs(State.lines) do + if Text.in_line(State, line_index, x,y) then +--? print('reset selection') + State.cursor1 = { + line=line_index, + pos=Text.to_pos_on_line(State, line_index, x, y), + } +--? print('cursor', State.cursor1.line, State.cursor1.pos) + if State.mousepress_shift then + if State.old_selection1.line == nil then + State.selection1 = State.old_cursor1 + else + State.selection1 = State.old_selection1 end end + State.old_cursor1, State.old_selection1, State.mousepress_shift = nil + if eq(State.cursor1, State.selection1) then + State.selection1 = {} + end + break end ---? print('selection:', State.selection1.line, State.selection1.pos) end +--? print('selection:', State.selection1.line, State.selection1.pos) end function edit.textinput(State, t) @@ -289,12 +193,6 @@ function edit.textinput(State, t) State.search_term = State.search_term..t State.search_text = nil Text.search_next(State) - elseif State.current_drawing_mode == 'name' then - local before = snapshot(State, State.lines.current_drawing_index) - local drawing = State.lines.current_drawing - local p = drawing.points[drawing.pending.target_point] - p.name = p.name..t - record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)}) else Text.textinput(State, t) end @@ -303,7 +201,6 @@ end function edit.keychord_pressed(State, chord, key) if State.selection1.line and - not State.lines.current_drawing and -- printable character created using shift key => delete selection -- (we're not creating any ctrl-shift- or alt-shift- combinations using regular/printable keys) (not App.shift_down() or utf8.len(key) == 1) and @@ -359,8 +256,6 @@ function edit.keychord_pressed(State, chord, key) State.selection1 = deepcopy(src.selection) patch(State.lines, event.after, event.before) patch_placeholders(State.line_cache, event.after, event.before) - -- invalidate various cached bits of lines - State.lines.current_drawing = nil -- if we're scrolling, reclaim all fragments to avoid memory leaks Text.redraw_all(State) schedule_save(State) @@ -374,8 +269,6 @@ function edit.keychord_pressed(State, chord, key) State.cursor1 = deepcopy(src.cursor) State.selection1 = deepcopy(src.selection) patch(State.lines, event.before, event.after) - -- invalidate various cached bits of lines - State.lines.current_drawing = nil -- if we're scrolling, reclaim all fragments to avoid memory leaks Text.redraw_all(State) schedule_save(State) @@ -413,42 +306,7 @@ function edit.keychord_pressed(State, chord, key) end schedule_save(State) record_undo_event(State, {before=before, after=snapshot(State, before_line, State.cursor1.line)}) - -- dispatch to drawing or text - elseif App.mouse_down(1) or chord:sub(1,2) == 'C-' then - -- DON'T reset line_cache.starty here - local drawing_index, drawing = Drawing.current_drawing(State) - if drawing_index then - local before = snapshot(State, drawing_index) - Drawing.keychord_pressed(State, chord) - record_undo_event(State, {before=before, after=snapshot(State, drawing_index)}) - schedule_save(State) - end - elseif chord == 'escape' and not App.mouse_down(1) then - for _,line in ipairs(State.lines) do - if line.mode == 'drawing' then - line.show_help = false - end - end - elseif State.current_drawing_mode == 'name' then - if chord == 'return' then - State.current_drawing_mode = State.previous_drawing_mode - State.previous_drawing_mode = nil - else - local before = snapshot(State, State.lines.current_drawing_index) - local drawing = State.lines.current_drawing - local p = drawing.points[drawing.pending.target_point] - if chord == 'escape' then - p.name = nil - record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)}) - elseif chord == 'backspace' then - local len = utf8.len(p.name) - local byte_offset = Text.offset(p.name, len-1) - if len == 1 then byte_offset = 0 end - p.name = string.sub(p.name, 1, byte_offset) - record_undo_event(State, {before=before, after=snapshot(State, State.lines.current_drawing_index)}) - end - end - schedule_save(State) + -- dispatch to text else for _,line_cache in ipairs(State.line_cache) do line_cache.starty = nil end -- just in case we scroll Text.keychord_pressed(State, chord) diff --git a/file.lua b/file.lua index 9440ae8..94514b2 100644 --- a/file.lua +++ b/file.lua @@ -12,15 +12,11 @@ function load_from_file(infile) while true do local line = infile_next_line() if line == nil then break end - if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated - table.insert(result, load_drawing(infile_next_line)) - else - table.insert(result, {mode='text', data=line}) - end + table.insert(result, {data=line}) end end if #result == 0 then - table.insert(result, {mode='text', data=''}) + table.insert(result, {data=''}) end return result end @@ -31,82 +27,11 @@ function save_to_disk(State) error('failed to write to "'..State.filename..'"') end for _,line in ipairs(State.lines) do - if line.mode == 'drawing' then - store_drawing(outfile, line) - else - outfile:write(line.data, '\n') - end + outfile:write(line.data, '\n') end outfile:close() end -json = require 'json' -function load_drawing(infile_next_line) - local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}} - while true do - local line = infile_next_line() - assert(line) - if line == '```' then break end - local shape = json.decode(line) - if shape.mode == 'freehand' then - -- no changes needed - elseif shape.mode == 'line' or shape.mode == 'manhattan' then - local name = shape.p1.name - shape.p1 = Drawing.insert_point(drawing.points, shape.p1.x, shape.p1.y) - drawing.points[shape.p1].name = name - name = shape.p2.name - shape.p2 = Drawing.insert_point(drawing.points, shape.p2.x, shape.p2.y) - drawing.points[shape.p2].name = name - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - for i,p in ipairs(shape.vertices) do - local name = p.name - shape.vertices[i] = Drawing.insert_point(drawing.points, p.x,p.y) - drawing.points[shape.vertices[i]].name = name - end - elseif shape.mode == 'circle' or shape.mode == 'arc' then - local name = shape.center.name - shape.center = Drawing.insert_point(drawing.points, shape.center.x,shape.center.y) - drawing.points[shape.center].name = name - elseif shape.mode == 'deleted' then - -- ignore - else - print(shape.mode) - assert(false) - end - table.insert(drawing.shapes, shape) - end - return drawing -end - -function store_drawing(outfile, drawing) - outfile:write('```lines\n') - for _,shape in ipairs(drawing.shapes) do - if shape.mode == 'freehand' then - outfile:write(json.encode(shape), '\n') - elseif shape.mode == 'line' or shape.mode == 'manhattan' then - local line = json.encode({mode=shape.mode, p1=drawing.points[shape.p1], p2=drawing.points[shape.p2]}) - outfile:write(line, '\n') - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - local obj = {mode=shape.mode, vertices={}} - for _,p in ipairs(shape.vertices) do - table.insert(obj.vertices, drawing.points[p]) - end - local line = json.encode(obj) - outfile:write(line, '\n') - elseif shape.mode == 'circle' then - outfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius}), '\n') - elseif shape.mode == 'arc' then - outfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius, start_angle=shape.start_angle, end_angle=shape.end_angle}), '\n') - elseif shape.mode == 'deleted' then - -- ignore - else - print(shape.mode) - assert(false) - end - end - outfile:write('```\n') -end - -- for tests function load_array(a) local result = {} @@ -115,58 +40,10 @@ function load_array(a) while true do i,line = next_line(a, i) if i == nil then break end ---? print(line) - if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated ---? print('inserting drawing') - i, drawing = load_drawing_from_array(next_line, a, i) ---? print('i now', i) - table.insert(result, drawing) - else ---? print('inserting text') - table.insert(result, {mode='text', data=line}) - end + table.insert(result, {data=line}) end if #result == 0 then - table.insert(result, {mode='text', data=''}) + table.insert(result, {data=''}) end return result end - -function load_drawing_from_array(iter, a, i) - local drawing = {mode='drawing', h=256/2, points={}, shapes={}, pending={}} - local line - while true do - i, line = iter(a, i) - assert(i) ---? print(i) - if line == '```' then break end - local shape = json.decode(line) - if shape.mode == 'freehand' then - -- no changes needed - elseif shape.mode == 'line' or shape.mode == 'manhattan' then - local name = shape.p1.name - shape.p1 = Drawing.insert_point(drawing.points, shape.p1.x, shape.p1.y) - drawing.points[shape.p1].name = name - name = shape.p2.name - shape.p2 = Drawing.insert_point(drawing.points, shape.p2.x, shape.p2.y) - drawing.points[shape.p2].name = name - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - for i,p in ipairs(shape.vertices) do - local name = p.name - shape.vertices[i] = Drawing.insert_point(drawing.points, p.x,p.y) - drawing.points[shape.vertices[i]].name = name - end - elseif shape.mode == 'circle' or shape.mode == 'arc' then - local name = shape.center.name - shape.center = Drawing.insert_point(drawing.points, shape.center.x,shape.center.y) - drawing.points[shape.center].name = name - elseif shape.mode == 'deleted' then - -- ignore - else - print(shape.mode) - assert(false) - end - table.insert(drawing.shapes, shape) - end - return i, drawing -end diff --git a/geom.lua b/geom.lua deleted file mode 100644 index 891e98d..0000000 --- a/geom.lua +++ /dev/null @@ -1,168 +0,0 @@ -geom = {} - -function geom.on_shape(x,y, drawing, shape) - if shape.mode == 'freehand' then - return geom.on_freehand(x,y, drawing, shape) - elseif shape.mode == 'line' then - return geom.on_line(x,y, drawing, shape) - elseif shape.mode == 'manhattan' then - local p1 = drawing.points[shape.p1] - local p2 = drawing.points[shape.p2] - if p1.x == p2.x then - if x ~= p1.x then return false end - local y1,y2 = p1.y, p2.y - if y1 > y2 then - y1,y2 = y2,y1 - end - return y >= y1-2 and y <= y2+2 - elseif p1.y == p2.y then - if y ~= p1.y then return false end - local x1,x2 = p1.x, p2.x - if x1 > x2 then - x1,x2 = x2,x1 - end - return x >= x1-2 and x <= x2+2 - end - elseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' then - return geom.on_polygon(x,y, drawing, shape) - elseif shape.mode == 'circle' then - local center = drawing.points[shape.center] - local dist = geom.dist(center.x,center.y, x,y) - return dist > shape.radius*0.95 and dist < shape.radius*1.05 - elseif shape.mode == 'arc' then - local center = drawing.points[shape.center] - local dist = geom.dist(center.x,center.y, x,y) - if dist < shape.radius*0.95 or dist > shape.radius*1.05 then - return false - end - return geom.angle_between(center.x,center.y, x,y, shape.start_angle,shape.end_angle) - elseif shape.mode == 'deleted' then - else - print(shape.mode) - assert(false) - end -end - -function geom.on_freehand(x,y, drawing, shape) - local prev - for _,p in ipairs(shape.points) do - if prev then - if geom.on_line(x,y, drawing, {p1=prev, p2=p}) then - return true - end - end - prev = p - end - return false -end - -function geom.on_line(x,y, drawing, shape) - local p1,p2 - if type(shape.p1) == 'number' then - p1 = drawing.points[shape.p1] - p2 = drawing.points[shape.p2] - else - p1 = shape.p1 - p2 = shape.p2 - end - if p1.x == p2.x then - if math.abs(p1.x-x) > 2 then - return false - end - local y1,y2 = p1.y,p2.y - if y1 > y2 then - y1,y2 = y2,y1 - end - return y >= y1-2 and y <= y2+2 - end - -- has the right slope and intercept - local m = (p2.y - p1.y) / (p2.x - p1.x) - local yp = p1.y + m*(x-p1.x) - if yp < y-2 or yp > y+2 then - return false - end - -- between endpoints - local k = (x-p1.x) / (p2.x-p1.x) - return k > -0.005 and k < 1.005 -end - -function geom.on_polygon(x,y, drawing, shape) - local prev - for _,p in ipairs(shape.vertices) do - if prev then - if geom.on_line(x,y, drawing, {p1=prev, p2=p}) then - return true - end - end - prev = p - end - return geom.on_line(x,y, drawing, {p1=shape.vertices[1], p2=shape.vertices[#shape.vertices]}) -end - --- are (x3,y3) and (x4,y4) on the same side of the line between (x1,y1) and (x2,y2) -function geom.same_side(x1,y1, x2,y2, x3,y3, x4,y4) - if x1 == x2 then - return math.sign(x3-x1) == math.sign(x4-x1) - end - if y1 == y2 then - return math.sign(y3-y1) == math.sign(y4-y1) - end - local m = (y2-y1)/(x2-x1) - return math.sign(m*(x3-x1) + y1-y3) == math.sign(m*(x4-x1) + y1-y4) -end - -function math.sign(x) - if x > 0 then - return 1 - elseif x == 0 then - return 0 - elseif x < 0 then - return -1 - end -end - -function geom.angle_with_hint(x1, y1, x2, y2, hint) - local result = geom.angle(x1,y1, x2,y2) - if hint then - -- Smooth the discontinuity where angle goes from positive to negative. - -- The hint is a memory of which way we drew it last time. - while result > hint+math.pi/10 do - result = result-math.pi*2 - end - while result < hint-math.pi/10 do - result = result+math.pi*2 - end - end - return result -end - --- result is from -π/2 to 3π/2, approximately adding math.atan2 from Lua 5.3 --- (LÖVE is Lua 5.1) -function geom.angle(x1,y1, x2,y2) - local result = math.atan((y2-y1)/(x2-x1)) - if x2 < x1 then - result = result+math.pi - end - return result -end - --- is the line between x,y and cx,cy at an angle between s and e? -function geom.angle_between(ox,oy, x,y, s,e) - local angle = geom.angle(ox,oy, x,y) - if s > e then - s,e = e,s - end - -- I'm not sure this is right or ideal.. - angle = angle-math.pi*2 - if s <= angle and angle <= e then - return true - end - angle = angle+math.pi*2 - if s <= angle and angle <= e then - return true - end - angle = angle+math.pi*2 - return s <= angle and angle <= e -end - -function geom.dist(x1,y1, x2,y2) return ((x2-x1)^2+(y2-y1)^2)^0.5 end diff --git a/help.lua b/help.lua deleted file mode 100644 index 2abeb00..0000000 --- a/help.lua +++ /dev/null @@ -1,156 +0,0 @@ -function draw_help_without_mouse_pressed(State, drawing_index) - local drawing = State.lines[drawing_index] - local line_cache = State.line_cache[drawing_index] - App.color(Help_color) - local y = line_cache.starty+10 - love.graphics.print("Things you can do:", State.left+30,y) - y = y + State.line_height - love.graphics.print("* Press the mouse button to start drawing a "..current_shape(State), State.left+30,y) - y = y + State.line_height - love.graphics.print("* Hover on a point and press 'ctrl+u' to pick it up and start moving it,", State.left+30,y) - y = y + State.line_height - love.graphics.print("then press the mouse button to drop it", State.left+30+bullet_indent(),y) - y = y + State.line_height - love.graphics.print("* Hover on a point and press 'ctrl+n', type a name, then press 'enter'", State.left+30,y) - y = y + State.line_height - love.graphics.print("* Hover on a point or shape and press 'ctrl+d' to delete it", State.left+30,y) - y = y + State.line_height - if State.current_drawing_mode ~= 'freehand' then - love.graphics.print("* Press 'ctrl+p' to switch to drawing freehand strokes", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'line' then - love.graphics.print("* Press 'ctrl+l' to switch to drawing lines", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'manhattan' then - love.graphics.print("* Press 'ctrl+m' to switch to drawing horizontal/vertical lines", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'circle' then - love.graphics.print("* Press 'ctrl+o' to switch to drawing circles/arcs", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'polygon' then - love.graphics.print("* Press 'ctrl+g' to switch to drawing polygons", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'rectangle' then - love.graphics.print("* Press 'ctrl+r' to switch to drawing rectangles", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'square' then - love.graphics.print("* Press 'ctrl+s' to switch to drawing squares", State.left+30,y) - y = y + State.line_height - end - love.graphics.print("* Press 'ctrl+=' or 'ctrl+-' to zoom in or out, ctrl+0 to reset zoom", State.left+30,y) - y = y + State.line_height - love.graphics.print("Press 'esc' now to hide this message", State.left+30,y) - y = y + State.line_height - App.color(Help_background_color) - love.graphics.rectangle('fill', State.left,line_cache.starty, State.width, math.max(Drawing.pixels(drawing.h, State.width),y-line_cache.starty)) -end - -function draw_help_with_mouse_pressed(State, drawing_index) - local drawing = State.lines[drawing_index] - local line_cache = State.line_cache[drawing_index] - App.color(Help_color) - local y = line_cache.starty+10 - love.graphics.print("You're currently drawing a "..current_shape(State, drawing.pending), State.left+30,y) - y = y + State.line_height - love.graphics.print('Things you can do now:', State.left+30,y) - y = y + State.line_height - if State.current_drawing_mode == 'freehand' then - love.graphics.print('* Release the mouse button to finish drawing the stroke', State.left+30,y) - y = y + State.line_height - elseif State.current_drawing_mode == 'line' or State.current_drawing_mode == 'manhattan' then - love.graphics.print('* Release the mouse button to finish drawing the line', State.left+30,y) - y = y + State.line_height - elseif State.current_drawing_mode == 'circle' then - if drawing.pending.mode == 'circle' then - love.graphics.print('* Release the mouse button to finish drawing the circle', State.left+30,y) - y = y + State.line_height - love.graphics.print("* Press 'a' to draw just an arc of a circle", State.left+30,y) - else - love.graphics.print('* Release the mouse button to finish drawing the arc', State.left+30,y) - end - y = y + State.line_height - elseif State.current_drawing_mode == 'polygon' then - love.graphics.print('* Release the mouse button to finish drawing the polygon', State.left+30,y) - y = y + State.line_height - love.graphics.print("* Press 'p' to add a vertex to the polygon", State.left+30,y) - y = y + State.line_height - elseif State.current_drawing_mode == 'rectangle' then - if #drawing.pending.vertices < 2 then - love.graphics.print("* Press 'p' to add a vertex to the rectangle", State.left+30,y) - y = y + State.line_height - else - love.graphics.print('* Release the mouse button to finish drawing the rectangle', State.left+30,y) - y = y + State.line_height - love.graphics.print("* Press 'p' to replace the second vertex of the rectangle", State.left+30,y) - y = y + State.line_height - end - elseif State.current_drawing_mode == 'square' then - if #drawing.pending.vertices < 2 then - love.graphics.print("* Press 'p' to add a vertex to the square", State.left+30,y) - y = y + State.line_height - else - love.graphics.print('* Release the mouse button to finish drawing the square', State.left+30,y) - y = y + State.line_height - love.graphics.print("* Press 'p' to replace the second vertex of the square", State.left+30,y) - y = y + State.line_height - end - end - love.graphics.print("* Press 'esc' then release the mouse button to cancel the current shape", State.left+30,y) - y = y + State.line_height - y = y + State.line_height - if State.current_drawing_mode ~= 'line' then - love.graphics.print("* Press 'l' to switch to drawing lines", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'manhattan' then - love.graphics.print("* Press 'm' to switch to drawing horizontal/vertical lines", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'circle' then - love.graphics.print("* Press 'o' to switch to drawing circles/arcs", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'polygon' then - love.graphics.print("* Press 'g' to switch to drawing polygons", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'rectangle' then - love.graphics.print("* Press 'r' to switch to drawing rectangles", State.left+30,y) - y = y + State.line_height - end - if State.current_drawing_mode ~= 'square' then - love.graphics.print("* Press 's' to switch to drawing squares", State.left+30,y) - y = y + State.line_height - end - App.color(Help_background_color) - love.graphics.rectangle('fill', State.left,line_cache.starty, State.width, math.max(Drawing.pixels(drawing.h, State.width),y-line_cache.starty)) -end - -function current_shape(State, shape) - if State.current_drawing_mode == 'freehand' then - return 'freehand stroke' - elseif State.current_drawing_mode == 'line' then - return 'straight line' - elseif State.current_drawing_mode == 'manhattan' then - return 'horizontal/vertical line' - elseif State.current_drawing_mode == 'circle' and shape and shape.start_angle then - return 'arc' - else - return State.current_drawing_mode - end -end - -_bullet_indent = nil -function bullet_indent() - if _bullet_indent == nil then - local text = love.graphics.newText(love.graphics.getFont(), '* ') - _bullet_indent = text:getWidth() - end - return _bullet_indent -end diff --git a/icons.lua b/icons.lua deleted file mode 100644 index 19939cf..0000000 --- a/icons.lua +++ /dev/null @@ -1,58 +0,0 @@ -icon = {} - -function icon.insert_drawing(x, y) - App.color(Icon_color) - love.graphics.rectangle('line', x,y, 12,12) - love.graphics.line(4,y+6, 16,y+6) - love.graphics.line(10,y, 10,y+12) -end - -function icon.freehand(x, y) - love.graphics.line(x+4,y+7,x+5,y+5) - love.graphics.line(x+5,y+5,x+7,y+4) - love.graphics.line(x+7,y+4,x+9,y+3) - love.graphics.line(x+9,y+3,x+10,y+5) - love.graphics.line(x+10,y+5,x+12,y+6) - love.graphics.line(x+12,y+6,x+13,y+8) - love.graphics.line(x+13,y+8,x+13,y+10) - love.graphics.line(x+13,y+10,x+14,y+12) - love.graphics.line(x+14,y+12,x+15,y+14) - love.graphics.line(x+15,y+14,x+15,y+16) -end - -function icon.line(x, y) - love.graphics.line(x+4,y+2, x+16,y+18) -end - -function icon.manhattan(x, y) - love.graphics.line(x+4,y+20, x+4,y+2) - love.graphics.line(x+4,y+2, x+10,y+2) - love.graphics.line(x+10,y+2, x+10,y+10) - love.graphics.line(x+10,y+10, x+18,y+10) -end - -function icon.polygon(x, y) - love.graphics.line(x+8,y+2, x+14,y+2) - love.graphics.line(x+14,y+2, x+18,y+10) - love.graphics.line(x+18,y+10, x+10,y+18) - love.graphics.line(x+10,y+18, x+4,y+12) - love.graphics.line(x+4,y+12, x+8,y+2) -end - -function icon.rectangle(x, y) - love.graphics.line(x+4,y+8, x+4,y+16) - love.graphics.line(x+4,y+16, x+16,y+16) - love.graphics.line(x+16,y+16, x+16,y+8) - love.graphics.line(x+16,y+8, x+4,y+8) -end - -function icon.square(x, y) - love.graphics.line(x+6,y+6, x+6,y+16) - love.graphics.line(x+6,y+16, x+16,y+16) - love.graphics.line(x+16,y+16, x+16,y+6) - love.graphics.line(x+16,y+6, x+6,y+6) -end - -function icon.circle(x, y) - love.graphics.circle('line', x+10,y+10, 8) -end diff --git a/main.lua b/main.lua index de75045..709e535 100644 --- a/main.lua +++ b/main.lua @@ -1,4 +1,5 @@ utf8 = require 'utf8' +json = require 'json' require 'app' require 'test' @@ -43,15 +44,11 @@ function App.initialize(arg) Text.redraw_all(Editor_state) Editor_state.screen_top1 = {line=1, pos=1} Editor_state.cursor1 = {line=1, pos=1} - edit.fixup_cursor(Editor_state) else load_from_disk(Editor_state) Text.redraw_all(Editor_state) - if Editor_state.cursor1.line > #Editor_state.lines or Editor_state.lines[Editor_state.cursor1.line].mode ~= 'text' then - edit.fixup_cursor(Editor_state) - end end - love.window.setTitle('lines.love - '..Editor_state.filename) + love.window.setTitle('text.love - '..Editor_state.filename) if #arg > 1 then print('ignoring commandline args after '..arg[1]) @@ -127,8 +124,7 @@ function App.filedropped(file) file:open('r') Editor_state.lines = load_from_file(file) file:close() - edit.fixup_cursor(Editor_state) - love.window.setTitle('lines.love - '..Editor_state.filename) + love.window.setTitle('text.love - '..Editor_state.filename) end function App.draw() diff --git a/select.lua b/select.lua index bae9504..b7cf65f 100644 --- a/select.lua +++ b/select.lua @@ -89,10 +89,8 @@ end function Text.to_pos(State, x,y) for line_index,line in ipairs(State.lines) do - if line.mode == 'text' then - if Text.in_line(State, line_index, x,y) then - return line_index, Text.to_pos_on_line(State, line_index, x,y) - end + if Text.in_line(State, line_index, x,y) then + return line_index, Text.to_pos_on_line(State, line_index, x,y) end end end @@ -172,9 +170,7 @@ function Text.selection(State) assert(minl < maxl) local result = {State.lines[minl].data:sub(min_offset)} for i=minl+1,maxl-1 do - if State.lines[i].mode == 'text' then - table.insert(result, State.lines[i].data) - end + table.insert(result, State.lines[i].data) end table.insert(result, State.lines[maxl].data:sub(1, max_offset-1)) return table.concat(result, '\n') diff --git a/text.lua b/text.lua index 90a139c..377efb3 100644 --- a/text.lua +++ b/text.lua @@ -82,7 +82,6 @@ end function Text.populate_screen_line_starting_pos(State, line_index) local line = State.lines[line_index] - if line.mode ~= 'text' then return end local line_cache = State.line_cache[line_index] if line_cache.screen_line_starting_pos then return @@ -109,7 +108,6 @@ end function Text.compute_fragments(State, line_index) --? print('compute_fragments', line_index, 'between', State.left, State.right) local line = State.lines[line_index] - if line.mode ~= 'text' then return end local line_cache = State.line_cache[line_index] if line_cache.fragments then return @@ -219,16 +217,11 @@ function Text.keychord_pressed(State, chord) end elseif State.cursor1.line > 1 then before = snapshot(State, State.cursor1.line-1, State.cursor1.line) - if State.lines[State.cursor1.line-1].mode == 'drawing' then - table.remove(State.lines, State.cursor1.line-1) - table.remove(State.line_cache, State.cursor1.line-1) - else - -- join lines - State.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1].data)+1 - State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].data - table.remove(State.lines, State.cursor1.line) - table.remove(State.line_cache, State.cursor1.line) - end + -- join lines + State.cursor1.pos = utf8.len(State.lines[State.cursor1.line-1].data)+1 + State.lines[State.cursor1.line-1].data = State.lines[State.cursor1.line-1].data..State.lines[State.cursor1.line].data + table.remove(State.lines, State.cursor1.line) + table.remove(State.line_cache, State.cursor1.line) State.cursor1.line = State.cursor1.line-1 end if State.screen_top1.line > #State.lines then @@ -269,10 +262,8 @@ function Text.keychord_pressed(State, chord) -- no change to State.cursor1.pos end elseif State.cursor1.line < #State.lines then - if State.lines[State.cursor1.line+1].mode == 'text' then - -- join lines - State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data - end + -- join lines + State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data table.remove(State.lines, State.cursor1.line+1) table.remove(State.line_cache, State.cursor1.line+1) end @@ -366,7 +357,7 @@ end function Text.insert_return(State) local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) - table.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset)}) + table.insert(State.lines, State.cursor1.line+1, {data=string.sub(State.lines[State.cursor1.line].data, byte_offset)}) table.insert(State.line_cache, State.cursor1.line+1, {}) State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1) Text.clear_screen_line_cache(State, State.cursor1.line) @@ -383,11 +374,7 @@ function Text.pageup(State) while y >= State.top do --? print(y, top2.line, top2.screen_line, top2.screen_pos) if State.screen_top1.line == 1 and State.screen_top1.pos == 1 then break end - if State.lines[State.screen_top1.line].mode == 'text' then - y = y - State.line_height - elseif State.lines[State.screen_top1.line].mode == 'drawing' then - y = y - Drawing_padding_height - Drawing.pixels(State.lines[State.screen_top1.line].h, State.width) - end + y = y - State.line_height top2 = Text.previous_screen_line(State, top2) end State.screen_top1 = Text.to1(State, top2) @@ -424,29 +411,24 @@ function Text.pagedown(State) end function Text.up(State) - assert(State.lines[State.cursor1.line].mode == 'text') --? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos) local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1) if screen_line_starting_pos == 1 then --? print('cursor is at first screen line of its line') -- line is done; skip to previous text line - local new_cursor_line = State.cursor1.line - while new_cursor_line > 1 do - new_cursor_line = new_cursor_line-1 - if State.lines[new_cursor_line].mode == 'text' then ---? print('found previous text line') - State.cursor1.line = new_cursor_line - Text.populate_screen_line_starting_pos(State, State.cursor1.line) - -- previous text line found, pick its final screen line ---? print('has multiple screen lines') - local screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos ---? print(#screen_line_starting_pos) - screen_line_starting_pos = screen_line_starting_pos[#screen_line_starting_pos] - local screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, screen_line_starting_pos) - local s = string.sub(State.lines[State.cursor1.line].data, screen_line_starting_byte_offset) - State.cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1 - break - end + if State.cursor1.line > 1 then + local new_cursor_line = State.cursor1.line-1 +--? print('found previous text line') + State.cursor1.line = new_cursor_line + Text.populate_screen_line_starting_pos(State, State.cursor1.line) + -- previous text line found, pick its final screen line +--? print('has multiple screen lines') + local screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos +--? print(#screen_line_starting_pos) + screen_line_starting_pos = screen_line_starting_pos[#screen_line_starting_pos] + local screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, screen_line_starting_pos) + local s = string.sub(State.lines[State.cursor1.line].data, screen_line_starting_byte_offset) + State.cursor1.pos = screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1 end else -- move up one screen line in current line @@ -465,20 +447,15 @@ function Text.up(State) end function Text.down(State) - assert(State.lines[State.cursor1.line].mode == 'text') --? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos) if Text.cursor_at_final_screen_line(State) then -- line is done, skip to next text line --? print('cursor at final screen line of its line') - local new_cursor_line = State.cursor1.line - while new_cursor_line < #State.lines do - new_cursor_line = new_cursor_line+1 - if State.lines[new_cursor_line].mode == 'text' then - State.cursor1.line = new_cursor_line - State.cursor1.pos = Text.nearest_cursor_pos(State.lines[State.cursor1.line].data, State.cursor_x, State.left) ---? print(State.cursor1.pos) - break - end + if State.cursor1.line < #State.lines then + local new_cursor_line = State.cursor1.line+1 + State.cursor1.line = new_cursor_line + State.cursor1.pos = Text.nearest_cursor_pos(State.lines[State.cursor1.line].data, State.cursor_x, State.left) +--? print(State.cursor1.pos) end if State.cursor1.line > State.screen_bottom1.line then --? print('screen top before:', State.screen_top1.line, State.screen_top1.pos) @@ -579,19 +556,11 @@ function Text.match(s, pos, pat) end function Text.left(State) - assert(State.lines[State.cursor1.line].mode == 'text') if State.cursor1.pos > 1 then State.cursor1.pos = State.cursor1.pos-1 - else - local new_cursor_line = State.cursor1.line - while new_cursor_line > 1 do - new_cursor_line = new_cursor_line-1 - if State.lines[new_cursor_line].mode == 'text' then - State.cursor1.line = new_cursor_line - State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1 - break - end - end + elseif State.cursor1.line > 1 then + State.cursor1.line = State.cursor1.line-1 + State.cursor1.pos = utf8.len(State.lines[State.cursor1.line].data) + 1 end if Text.lt1(State.cursor1, State.screen_top1) then local top2 = Text.to2(State, State.screen_top1) @@ -608,19 +577,11 @@ function Text.right(State) end function Text.right_without_scroll(State) - assert(State.lines[State.cursor1.line].mode == 'text') if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then State.cursor1.pos = State.cursor1.pos+1 - else - local new_cursor_line = State.cursor1.line - while new_cursor_line <= #State.lines-1 do - new_cursor_line = new_cursor_line+1 - if State.lines[new_cursor_line].mode == 'text' then - State.cursor1.line = new_cursor_line - State.cursor1.pos = 1 - break - end - end + elseif State.cursor1.line <= #State.lines-1 then + State.cursor1.line = State.cursor1.line+1 + State.cursor1.pos = 1 end end @@ -644,23 +605,7 @@ function Text.cursor_at_final_screen_line(State) end function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State) - local y = State.top - while State.cursor1.line <= #State.lines do - if State.lines[State.cursor1.line].mode == 'text' then - break - end ---? print('cursor skips', State.cursor1.line) - y = y + Drawing_padding_height + Drawing.pixels(State.lines[State.cursor1.line].h, State.width) - State.cursor1.line = State.cursor1.line + 1 - end - -- hack: insert a text line at bottom of file if necessary - if State.cursor1.line > #State.lines then - assert(State.cursor1.line == #State.lines+1) - table.insert(State.lines, {mode='text', data=''}) - table.insert(State.line_cache, {}) - end ---? print(y, App.screen.height, App.screen.height-State.line_height) - if y > App.screen.height - State.line_height then + if State.top > App.screen.height - State.line_height then --? print('scroll up') Text.snap_cursor_to_bottom_of_screen(State) end @@ -676,24 +621,11 @@ function Text.snap_cursor_to_bottom_of_screen(State) while true do --? print(y, 'top2:', top2.line, top2.screen_line, top2.screen_pos) if top2.line == 1 and top2.screen_line == 1 then break end - if top2.screen_line > 1 or State.lines[top2.line-1].mode == 'text' then - local h = State.line_height - if y - h < State.top then - break - end - y = y - h - else - assert(top2.line > 1) - assert(State.lines[top2.line-1].mode == 'drawing') - -- We currently can't draw partial drawings, so either skip it entirely - -- or not at all. - local h = Drawing_padding_height + Drawing.pixels(State.lines[top2.line-1].h, State.width) - if y - h < State.top then - break - end ---? print('skipping drawing of height', h) - y = y - h + local h = State.line_height + if y - h < State.top then + break end + y = y - h top2 = Text.previous_screen_line(State, top2) end --? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos) @@ -855,9 +787,6 @@ function Text.x(s, pos) end function Text.to2(State, loc1) - if State.lines[loc1.line].mode == 'drawing' then - return {line=loc1.line, screen_line=1, screen_pos=1} - end local result = {line=loc1.line, screen_line=1} local line_cache = State.line_cache[loc1.line] Text.populate_screen_line_starting_pos(State, loc1.line) @@ -920,8 +849,6 @@ function Text.previous_screen_line(State, loc2) return {line=loc2.line, screen_line=loc2.screen_line-1, screen_pos=1} elseif loc2.line == 1 then return loc2 - elseif State.lines[loc2.line-1].mode == 'drawing' then - return {line=loc2.line-1, screen_line=1, screen_pos=1} else local l = State.lines[loc2.line-1] Text.populate_screen_line_starting_pos(State, loc2.line-1) diff --git a/text_tests.lua b/text_tests.lua index fa4c173..7f9b2e4 100644 --- a/text_tests.lua +++ b/text_tests.lua @@ -14,34 +14,6 @@ function test_initial_state() check_eq(Editor_state.screen_top1.pos, 1, 'F - test_initial_state/screen_top:pos') end -function test_click_to_create_drawing() - io.write('\ntest_click_to_create_drawing') - App.screen.init{width=120, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{} - Text.redraw_all(Editor_state) - edit.draw(Editor_state) - edit.run_after_mouse_click(Editor_state, 8,Editor_state.top+8, 1) - -- cursor skips drawing to always remain on text - check_eq(#Editor_state.lines, 2, 'F - test_click_to_create_drawing/#lines') - check_eq(Editor_state.cursor1.line, 2, 'F - test_click_to_create_drawing/cursor') -end - -function test_backspace_to_delete_drawing() - io.write('\ntest_backspace_to_delete_drawing') - -- display a drawing followed by a line of text (you shouldn't ever have a drawing right at the end) - App.screen.init{width=120, height=60} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', ''} - Text.redraw_all(Editor_state) - -- cursor is on text as always (outside tests this will get initialized correctly) - Editor_state.cursor1.line = 2 - -- backspacing deletes the drawing - edit.run_after_keychord(Editor_state, 'backspace') - check_eq(#Editor_state.lines, 1, 'F - test_backspace_to_delete_drawing/#lines') - check_eq(Editor_state.cursor1.line, 1, 'F - test_backspace_to_delete_drawing/cursor') -end - function test_backspace_from_start_of_final_line() io.write('\ntest_backspace_from_start_of_final_line') -- display final line of text with cursor at start of it @@ -994,36 +966,6 @@ function test_pagedown() App.screen.check(y, 'ghi', 'F - test_pagedown/screen:2') end -function test_pagedown_skips_drawings() - io.write('\ntest_pagedown_skips_drawings') - -- some lines of text with a drawing intermixed - local drawing_width = 50 - App.screen.init{width=Editor_state.left+drawing_width, height=80} - Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'abc', -- height 15 - '```lines', '```', -- height 25 - 'def', -- height 15 - 'ghi'} -- height 15 - Text.redraw_all(Editor_state) - check_eq(Editor_state.lines[2].mode, 'drawing', 'F - test_pagedown_skips_drawings/baseline/lines') - Editor_state.cursor1 = {line=1, pos=1} - Editor_state.screen_top1 = {line=1, pos=1} - Editor_state.screen_bottom1 = {} - local drawing_height = Drawing_padding_height + drawing_width/2 -- default - -- initially the screen displays the first line and the drawing - -- 15px margin + 15px line1 + 10px margin + 25px drawing + 10px margin = 75px < screen height 80px - edit.draw(Editor_state) - local y = Editor_state.top - App.screen.check(y, 'abc', 'F - test_pagedown_skips_drawings/baseline/screen:1') - -- after pagedown the screen draws the drawing up top - -- 15px margin + 10px margin + 25px drawing + 10px margin + 15px line3 = 75px < screen height 80px - edit.run_after_keychord(Editor_state, 'pagedown') - check_eq(Editor_state.screen_top1.line, 2, 'F - test_pagedown_skips_drawings/screen_top') - check_eq(Editor_state.cursor1.line, 3, 'F - test_pagedown_skips_drawings/cursor') - y = Editor_state.top + drawing_height - App.screen.check(y, 'def', 'F - test_pagedown_skips_drawings/screen:1') -end - function test_pagedown_often_shows_start_of_wrapping_line() io.write('\ntest_pagedown_often_shows_start_of_wrapping_line') -- draw a few lines ending in part of a wrapping line @@ -2022,7 +1964,7 @@ function test_search() io.write('\ntest_search') App.screen.init{width=120, height=60} Editor_state = edit.initialize_test_state() - Editor_state.lines = load_array{'```lines', '```', 'def', 'ghi', 'deg'} + Editor_state.lines = load_array{'abc', 'def', 'ghi', 'deg'} Text.redraw_all(Editor_state) Editor_state.cursor1 = {line=1, pos=1} Editor_state.screen_top1 = {line=1, pos=1} diff --git a/undo.lua b/undo.lua index d699211..912c949 100644 --- a/undo.lua +++ b/undo.lua @@ -50,8 +50,6 @@ function snapshot(State, s,e) screen_top=deepcopy(State.screen_top1), selection=deepcopy(State.selection1), cursor=deepcopy(State.cursor1), - current_drawing_mode=Drawing_mode, - previous_drawing_mode=State.previous_drawing_mode, lines={}, start_line=s, end_line=e, @@ -60,19 +58,7 @@ function snapshot(State, s,e) -- deep copy lines without cached stuff like text fragments for i=s,e do local line = State.lines[i] - if line.mode == 'text' then - table.insert(event.lines, {mode='text', data=line.data}) - elseif line.mode == 'drawing' then - local points=deepcopy(line.points) ---? print('copying', line.points, 'with', #line.points, 'points into', points) - local shapes=deepcopy(line.shapes) ---? print('copying', line.shapes, 'with', #line.shapes, 'shapes into', shapes) - table.insert(event.lines, {mode='drawing', h=line.h, points=points, shapes=shapes, pending={}}) ---? table.insert(event.lines, {mode='drawing', h=line.h, points=deepcopy(line.points), shapes=deepcopy(line.shapes), pending={}}) - else - print(line.mode) - assert(false) - end + table.insert(event.lines, {data=line.data}) end return event end |