diff options
author | Kartik K. Agaram <vc@akkartik.com> | 2022-09-05 12:47:27 -0700 |
---|---|---|
committer | Kartik K. Agaram <vc@akkartik.com> | 2022-09-05 12:47:27 -0700 |
commit | e0f750913fa653c03a666203e04c59e847e73bff (patch) | |
tree | 037a1062d40b54331ee0cbe797b5bd8ede551180 | |
parent | fc98407c930f1ba48a48657031bae06991666449 (diff) | |
parent | fdb35ce12bd8b2e30522ada07ec756a626d01a09 (diff) | |
download | view.love-e0f750913fa653c03a666203e04c59e847e73bff.tar.gz |
Merge lines.love
-rw-r--r-- | app.lua | 28 | ||||
-rw-r--r-- | commands.lua | 2 | ||||
-rw-r--r-- | drawing.lua | 745 | ||||
-rw-r--r-- | drawing_tests.lua | 785 | ||||
-rw-r--r-- | edit.lua | 8 | ||||
-rw-r--r-- | file.lua | 3 | ||||
-rw-r--r-- | geom.lua | 168 | ||||
-rw-r--r-- | help.lua | 151 | ||||
-rw-r--r-- | icons.lua | 59 | ||||
-rw-r--r-- | main.lua | 9 | ||||
-rw-r--r-- | source.lua | 6 | ||||
-rw-r--r-- | source_edit.lua | 178 | ||||
-rw-r--r-- | source_file.lua | 156 | ||||
-rw-r--r-- | source_text.lua | 197 | ||||
-rw-r--r-- | source_text_tests.lua | 60 | ||||
-rw-r--r-- | source_undo.lua | 16 | ||||
-rw-r--r-- | text.lua | 22 |
17 files changed, 2474 insertions, 119 deletions
diff --git a/app.lua b/app.lua index 2135fb0..69507ec 100644 --- a/app.lua +++ b/app.lua @@ -383,8 +383,32 @@ function App.disable_tests() App.newText = love.graphics.newText App.screen.draw = love.graphics.draw App.width = function(text) return text:getWidth() end - App.open_for_reading = function(filename) return io.open(filename, 'r') end - App.open_for_writing = function(filename) return io.open(filename, 'w') end + if Current_app == nil or Current_app == 'run' then + App.open_for_reading = function(filename) return io.open(filename, 'r') end + App.open_for_writing = function(filename) return io.open(filename, 'w') end + elseif Current_app == 'source' then + -- HACK: source editor requires a couple of different foundational definitions + App.open_for_reading = + function(filename) + local result = love.filesystem.newFile(filename) + local ok, err = result:open('r') + if ok then + return result + else + return ok, err + end + end + App.open_for_writing = + function(filename) + local result = love.filesystem.newFile(filename) + local ok, err = result:open('w') + if ok then + return result + else + return ok, err + end + end + end App.getTime = love.timer.getTime App.getClipboardText = love.system.getClipboardText App.setClipboardText = love.system.setClipboardText diff --git a/commands.lua b/commands.lua index 037205f..730fffc 100644 --- a/commands.lua +++ b/commands.lua @@ -28,7 +28,7 @@ function source.draw_menu_bar() else add_hotkey_to_menu('ctrl+b: expand debug prints') end - add_hotkey_to_menu('ctrl+d: create/edit debug print') + add_hotkey_to_menu('ctrl+i: create/edit debug print') add_hotkey_to_menu('ctrl+f: find in file') add_hotkey_to_menu('alt+left alt+right: prev/next word') elseif Focus == 'log_browser' then diff --git a/drawing.lua b/drawing.lua new file mode 100644 index 0000000..0343f85 --- /dev/null +++ b/drawing.lua @@ -0,0 +1,745 @@ +-- 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, mouse_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, mouse_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.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 new file mode 100644 index 0000000..f1e39a6 --- /dev/null +++ b/drawing_tests.lua @@ -0,0 +1,785 @@ +-- 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 768313f..bd1efa0 100644 --- a/edit.lua +++ b/edit.lua @@ -90,7 +90,7 @@ function edit.draw(State) local line = State.lines[line_index] --? print('draw:', y, line_index, line) if y + State.line_height > App.screen.height then break end - State.screen_bottom1.line = line_index + State.screen_bottom1 = {line=line_index, pos=nil} --? print('text.draw', y, line_index) local startpos = 1 if line_index == State.screen_top1.line then @@ -100,7 +100,6 @@ function edit.draw(State) 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 Text.draw_search_bar(State) end @@ -232,7 +231,10 @@ function edit.keychord_pressed(State, chord, key) return elseif chord == 'C-f' then State.search_term = '' - State.search_backup = {cursor={line=State.cursor1.line, pos=State.cursor1.pos}, screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos}} + State.search_backup = { + cursor={line=State.cursor1.line, pos=State.cursor1.pos}, + screen_top={line=State.screen_top1.line, pos=State.screen_top1.pos}, + } assert(State.search_text == nil) -- zoom elseif chord == 'C-=' then diff --git a/file.lua b/file.lua index e95c14d..98311da 100644 --- a/file.lua +++ b/file.lua @@ -37,7 +37,8 @@ function save_to_disk(State) error('failed to write to "'..State.filename..'"') end for _,line in ipairs(State.lines) do - outfile:write(line.data, '\n') + outfile:write(line.data) + outfile:write('\n') end outfile:close() end diff --git a/geom.lua b/geom.lua new file mode 100644 index 0000000..891e98d --- /dev/null +++ b/geom.lua @@ -0,0 +1,168 @@ +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 new file mode 100644 index 0000000..145692f --- /dev/null +++ b/help.lua @@ -0,0 +1,151 @@ +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 + +function bullet_indent() + return App.width(to_text('* ')) +end diff --git a/icons.lua b/icons.lua new file mode 100644 index 0000000..175fb13 --- /dev/null +++ b/icons.lua @@ -0,0 +1,59 @@ +icon = {} + +function icon.insert_drawing(button_params) + local x,y = button_params.x, button_params.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 544b5db..75c140a 100644 --- a/main.lua +++ b/main.lua @@ -45,7 +45,7 @@ function App.load() load_file_from_source_or_save_directory('undo.lua') load_file_from_source_or_save_directory('text_tests.lua') load_file_from_source_or_save_directory('run_tests.lua') - else + elseif Current_app == 'source' then load_file_from_source_or_save_directory('source_file.lua') load_file_from_source_or_save_directory('source.lua') load_file_from_source_or_save_directory('commands.lua') @@ -57,7 +57,14 @@ function App.load() load_file_from_source_or_save_directory('source_undo.lua') load_file_from_source_or_save_directory('colorize.lua') load_file_from_source_or_save_directory('source_text_tests.lua') + load_file_from_source_or_save_directory('icons.lua') + load_file_from_source_or_save_directory('drawing.lua') + load_file_from_source_or_save_directory('geom.lua') + load_file_from_source_or_save_directory('help.lua') + load_file_from_source_or_save_directory('drawing_tests.lua') load_file_from_source_or_save_directory('source_tests.lua') + else + assert(false, 'unknown app "'..Current_app..'"') end end diff --git a/source.lua b/source.lua index 8f09549..8653a91 100644 --- a/source.lua +++ b/source.lua @@ -15,11 +15,15 @@ function source.initialize_globals() 'run_tests', 'log', 'edit', + 'drawing', + 'help', 'text', 'search', 'select', 'undo', 'text_tests', + 'geom', + 'drawing_tests', 'file', 'source', 'source_tests', @@ -273,7 +277,7 @@ function source.mouse_pressed(x,y, mouse_button) --? print('mouse click', x, y) --? print(Editor_state.left, Editor_state.right) --? print(Log_browser_state.left, Log_browser_state.right) - if Editor_state.left <= x and x < Editor_state.right then + if x < Editor_state.right + Margin_right then --? print('click on edit side') if Focus ~= 'edit' then Focus = 'edit' diff --git a/source_edit.lua b/source_edit.lua index d454467..65f00a2 100644 --- a/source_edit.lua +++ b/source_edit.lua @@ -1,8 +1,14 @@ -- 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} Fold_color = {r=0, g=0.6, b=0} Fold_background_color = {r=0, g=0.7, b=0} @@ -10,14 +16,40 @@ 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 + 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 of bifold text consists of an A side and an optional B side, each of which is a string - -- expanded: whether to show B side - lines = {{data='', dataB=nil, expanded=nil}}, -- array of lines + -- a line is either bifold text or a drawing + -- a line of bifold text consists of an A side and an optional B side + -- mode = 'text', + -- string data, + -- string dataB, + -- expanded: whether to show B side + -- 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='', dataB=nil, expanded=nil}}, -- array of lines -- Lines can be too long to fit on screen, in which case they _wrap_ into -- multiple _screen lines_. @@ -47,6 +79,9 @@ 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 @@ -71,6 +106,15 @@ 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) State.button_handlers = {} App.color(Text_color) @@ -85,21 +129,46 @@ function edit.draw(State) --? print('== draw') for line_index = State.screen_top1.line,#State.lines do local line = State.lines[line_index] ---? print('draw:', y, line_index, line) +--? print('draw:', y, line_index, line, line.mode) if y + State.line_height > App.screen.height then break end State.screen_bottom1 = {line=line_index, pos=nil, posB=nil} + if line.mode == 'text' then --? print('text.draw', y, line_index) - local startpos, startposB = 1, nil - if line_index == State.screen_top1.line then - if State.screen_top1.pos then - startpos = State.screen_top1.pos - else - startpos, startposB = nil, State.screen_top1.posB + local startpos, startposB = 1, nil + if line_index == State.screen_top1.line then + if State.screen_top1.pos then + startpos = State.screen_top1.pos + else + startpos, startposB = nil, State.screen_top1.posB + end end + if line.data == '' then + -- button to insert new drawing + button(State, '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, State.screen_bottom1.posB = Text.draw(State, line_index, y, startpos, startposB) + 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) end - y, State.screen_bottom1.pos, State.screen_bottom1.posB = Text.draw(State, line_index, y, startpos, startposB) - y = y + State.line_height ---? print('=> y', y) end if State.search_term then Text.draw_search_bar(State) @@ -107,6 +176,7 @@ 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 @@ -128,23 +198,44 @@ end function edit.mouse_pressed(State, x,y, mouse_button) if State.search_term then return end ---? print('press', State.selection1.line, State.selection1.pos) +--? print('press') if mouse_press_consumed_by_any_button_handler(State, x,y, mouse_button) then -- press on a button and it returned 'true' to short-circuit return end for line_index,line in ipairs(State.lines) do - if Text.in_line(State, line_index, x,y) then - local pos,posB = Text.to_pos_on_line(State, line_index, x, y) ---? print(x,y, 'setting cursor:', line_index, pos, posB) - State.cursor1 = {line=line_index, pos=pos, posB=posB} - break + if line.mode == 'text' then + if Text.in_line(State, line_index, x,y) then + local pos,posB = Text.to_pos_on_line(State, line_index, x, y) + --? print(x,y, 'setting cursor:', line_index, pos, posB) + State.cursor1 = {line=line_index, pos=pos, posB=posB} + 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 end end 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 + end end function edit.textinput(State, t) @@ -153,6 +244,12 @@ 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 @@ -205,7 +302,7 @@ function edit.keychord_pressed(State, chord, key) end edit.eradicate_locations_after_the_fold(State) end - elseif chord == 'C-d' then + elseif chord == 'C-i' then if State.cursor1.posB == nil then local before = snapshot(State, State.cursor1.line) if State.lines[State.cursor1.line].dataB == nil then @@ -240,6 +337,8 @@ function edit.keychord_pressed(State, chord, key) State.cursor1 = deepcopy(src.cursor) 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) @@ -252,6 +351,8 @@ function edit.keychord_pressed(State, chord, key) State.screen_top1 = deepcopy(src.screen_top) State.cursor1 = deepcopy(src.cursor) 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) @@ -289,7 +390,42 @@ 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 text + -- 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) 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/source_file.lua b/source_file.lua index 9c0b8a4..6552667 100644 --- a/source_file.lua +++ b/source_file.lua @@ -25,17 +25,21 @@ function load_from_file(infile) while true do local line = infile_next_line() if line == nil then break end - local line_info = {} - if line:find(Fold) then - _, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)') + if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated + table.insert(result, load_drawing(infile_next_line)) else - line_info.data = line + local line_info = {mode='text'} + if line:find(Fold) then + _, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)') + else + line_info.data = line + end + table.insert(result, line_info) end - table.insert(result, line_info) end end if #result == 0 then - table.insert(result, {data=''}) + table.insert(result, {mode='text', data=''}) end return result end @@ -46,16 +50,86 @@ function save_to_disk(State) error('failed to write to "'..State.filename..'"') end for _,line in ipairs(State.lines) do - outfile:write(line.data) - if line.dataB and #line.dataB > 0 then - outfile:write(Fold) - outfile:write(line.dataB) + if line.mode == 'drawing' then + store_drawing(outfile, line) + else + outfile:write(line.data) + if line.dataB and #line.dataB > 0 then + outfile:write(Fold) + outfile:write(line.dataB) + end + outfile:write('\n') end - outfile:write('\n') end outfile:close() end +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.find_or_insert_point(drawing.points, shape.p1.x, shape.p1.y, --[[large width to minimize overlap]] 1600) + drawing.points[shape.p1].name = name + name = shape.p2.name + shape.p2 = Drawing.find_or_insert_point(drawing.points, shape.p2.x, shape.p2.y, --[[large width to minimize overlap]] 1600) + 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.find_or_insert_point(drawing.points, p.x,p.y, --[[large width to minimize overlap]] 1600) + 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.find_or_insert_point(drawing.points, shape.center.x,shape.center.y, --[[large width to minimize overlap]] 1600) + 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 = {} @@ -64,16 +138,64 @@ function load_array(a) while true do i,line = next_line(a, i) if i == nil then break end - local line_info = {} - if line:find(Fold) then - _, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)') +--? 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 - line_info.data = line +--? print('inserting text') + local line_info = {mode='text'} + if line:find(Fold) then + _, _, line_info.data, line_info.dataB = line:find('([^'..Fold..']*)'..Fold..'([^'..Fold..']*)') + else + line_info.data = line + end + table.insert(result, line_info) end - table.insert(result, line_info) end if #result == 0 then - table.insert(result, {data=''}) + table.insert(result, {mode='text', 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.find_or_insert_point(drawing.points, shape.p1.x, shape.p1.y, --[[large width to minimize overlap]] 1600) + drawing.points[shape.p1].name = name + name = shape.p2.name + shape.p2 = Drawing.find_or_insert_point(drawing.points, shape.p2.x, shape.p2.y, --[[large width to minimize overlap]] 1600) + 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.find_or_insert_point(drawing.points, p.x,p.y, --[[large width to minimize overlap]] 1600) + 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.find_or_insert_point(drawing.points, shape.center.x,shape.center.y, --[[large width to minimize overlap]] 1600) + 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/source_text.lua b/source_text.lua index e491dac..9c1279a 100644 --- a/source_text.lua +++ b/source_text.lua @@ -53,9 +53,6 @@ function Text.draw(State, line_index, y, startpos, startposB) -- draw B side --? if line_index == 8 then print('drawing B side') end App.color(Fold_color) ---? if Foo then ---? print('draw:', State.lines[line_index].data, "=====", State.lines[line_index].dataB, 'starting from x', x+AB_padding) ---? end if startposB then overflows_screen, x, y, pos, screen_line_starting_pos = Text.draw_wrapping_lineB(State, line_index, x,y, startposB) else @@ -196,6 +193,7 @@ 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 @@ -222,6 +220,7 @@ 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 @@ -416,11 +415,16 @@ function Text.keychord_pressed(State, chord) end elseif State.cursor1.line > 1 then before = snapshot(State, State.cursor1.line-1, State.cursor1.line) - -- 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) + 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 State.cursor1.line = State.cursor1.line-1 end if State.screen_top1.line > #State.lines then @@ -471,10 +475,12 @@ function Text.keychord_pressed(State, chord) -- refuse to delete past end of side B end elseif State.cursor1.line < #State.lines then - -- join lines - State.lines[State.cursor1.line].data = State.lines[State.cursor1.line].data..State.lines[State.cursor1.line+1].data - -- delete side B on first line - State.lines[State.cursor1.line].dataB = State.lines[State.cursor1.line+1].dataB + 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 + -- delete side B on first line + State.lines[State.cursor1.line].dataB = State.lines[State.cursor1.line+1].dataB + end table.remove(State.lines, State.cursor1.line+1) table.remove(State.line_cache, State.cursor1.line+1) end @@ -530,7 +536,7 @@ function Text.insert_return(State) if State.cursor1.pos then -- when inserting a newline, move any B side to the new line local byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos) - table.insert(State.lines, State.cursor1.line+1, {data=string.sub(State.lines[State.cursor1.line].data, byte_offset), dataB=State.lines[State.cursor1.line].dataB}) + table.insert(State.lines, State.cursor1.line+1, {mode='text', data=string.sub(State.lines[State.cursor1.line].data, byte_offset), dataB=State.lines[State.cursor1.line].dataB}) 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) State.lines[State.cursor1.line].dataB = nil @@ -550,7 +556,11 @@ 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 and State.screen_top1.pos == 1 then break end - y = y - State.line_height + 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 top2 = Text.previous_screen_line(State, top2) end State.screen_top1 = Text.to1(State, top2) @@ -567,7 +577,7 @@ function Text.pagedown(State) if Text.lt1(State.screen_top1, new_top1) then State.screen_top1 = new_top1 else - State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos} + State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos, posB=State.screen_bottom1.posB} end --? print('setting top to', State.screen_top1.line, State.screen_top1.pos) State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos, posB=State.screen_top1.posB} @@ -578,6 +588,7 @@ function Text.pagedown(State) end function Text.up(State) + assert(State.lines[State.cursor1.line].mode == 'text') if State.cursor1.pos then Text.upA(State) else @@ -591,18 +602,23 @@ function Text.upA(State) 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 - if State.cursor1.line > 1 then ---? print('found previous text line') - State.cursor1 = {line=State.cursor1.line-1, pos=nil} - 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 + 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=State.cursor1.line-1, pos=nil} + 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 end else -- move up one screen line in current line @@ -626,15 +642,19 @@ function Text.upB(State) assert(screen_line_indexB >= 1) if screen_line_indexB == 1 then -- move to A side of previous line - if State.cursor1.line > 1 then - State.cursor1.line = State.cursor1.line-1 - State.cursor1.posB = nil - Text.populate_screen_line_starting_pos(State, State.cursor1.line) - local prev_line_cache = State.line_cache[State.cursor1.line] - local prev_screen_line_starting_pos = prev_line_cache.screen_line_starting_pos[#prev_line_cache.screen_line_starting_pos] - local prev_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, prev_screen_line_starting_pos) - local s = string.sub(State.lines[State.cursor1.line].data, prev_screen_line_starting_byte_offset) - State.cursor1.pos = prev_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1 + 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=State.cursor1.line-1, posB=nil} + Text.populate_screen_line_starting_pos(State, State.cursor1.line) + local prev_line_cache = State.line_cache[State.cursor1.line] + local prev_screen_line_starting_pos = prev_line_cache.screen_line_starting_pos[#prev_line_cache.screen_line_starting_pos] + local prev_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, prev_screen_line_starting_pos) + local s = string.sub(State.lines[State.cursor1.line].data, prev_screen_line_starting_byte_offset) + State.cursor1.pos = prev_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1 + break + end end elseif screen_line_indexB == 2 then -- all-B screen-line to potentially A+B screen-line @@ -673,16 +693,22 @@ end -- cursor on A side => move down one screen line (A side) in current line -- cursor on B side => move down one screen line (B side) in current line 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') - if State.cursor1.line < #State.lines then - State.cursor1 = { - line = State.cursor1.line+1, - pos = Text.nearest_cursor_pos(State.lines[State.cursor1.line+1].data, State.cursor_x, State.left) - } ---? print(State.cursor1.pos) + 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, + pos = Text.nearest_cursor_pos(State.lines[new_cursor_line].data, State.cursor_x, State.left), + } +--? print(State.cursor1.pos) + break + end end if State.cursor1.line > State.screen_bottom1.line then --? print('screen top before:', State.screen_top1.line, State.screen_top1.pos) @@ -737,7 +763,7 @@ function Text.start_of_line(State) State.cursor1.posB = 1 end if Text.lt1(State.cursor1, State.screen_top1) then - State.screen_top1 = {line=State.cursor1.line, pos=1} -- copy + State.screen_top1 = {line=State.cursor1.line, pos=State.cursor1.pos, posB=State.cursor1.posB} -- copy end end @@ -919,11 +945,18 @@ end function Text.leftA(State) if State.cursor1.pos > 1 then State.cursor1.pos = State.cursor1.pos-1 - elseif State.cursor1.line > 1 then - State.cursor1 = { - line = State.cursor1.line-1, - pos = utf8.len(State.lines[State.cursor1.line-1].data) + 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, + pos = utf8.len(State.lines[new_cursor_line].data) + 1, + } + break + end + end end if Text.lt1(State.cursor1, State.screen_top1) then local top2 = Text.to2(State, State.screen_top1) @@ -955,6 +988,7 @@ function Text.right(State) end function Text.right_without_scroll(State) + assert(State.lines[State.cursor1.line].mode == 'text') if State.cursor1.pos then Text.right_without_scrollA(State) else @@ -965,17 +999,31 @@ end function Text.right_without_scrollA(State) if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then State.cursor1.pos = State.cursor1.pos+1 - elseif State.cursor1.line <= #State.lines-1 then - State.cursor1 = {line=State.cursor1.line+1, 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, pos=1} + break + end + end end end function Text.right_without_scrollB(State) if State.cursor1.posB <= utf8.len(State.lines[State.cursor1.line].dataB) then State.cursor1.posB = State.cursor1.posB+1 - elseif State.cursor1.line <= #State.lines-1 then + else -- overflow back into A side - State.cursor1 = {line=State.cursor1.line+1, pos=1} + 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, pos=1} + break + end + end end end @@ -1027,7 +1075,23 @@ function Text.cursor_at_final_screen_line(State) end function Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State) - if State.top > App.screen.height - State.line_height then + 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 --? print('scroll up') Text.snap_cursor_to_bottom_of_screen(State) end @@ -1052,11 +1116,24 @@ function Text.snap_cursor_to_bottom_of_screen(State) while true do --? print(y, 'top2:', State.lines[top2.line].data, top2.line, top2.screen_line, top2.screen_pos, top2.screen_lineB, top2.screen_posB) if top2.line == 1 and top2.screen_line == 1 then break end - local h = State.line_height - if y - h < State.top then - break + 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 end - y = y - h top2 = Text.previous_screen_line(State, top2) end --? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos) @@ -1064,7 +1141,6 @@ function Text.snap_cursor_to_bottom_of_screen(State) --? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos) --? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.cursor1.posB, State.screen_bottom1.line, State.screen_bottom1.pos, State.screen_bottom1.posB) Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks - Foo = true end function Text.in_line(State, line_index, x,y) @@ -1338,6 +1414,9 @@ 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 if loc1.pos then return Text.to2A(State, loc1) else @@ -1448,10 +1527,8 @@ end function Text.previous_screen_lineA(State, loc2) if loc2.screen_line > 1 then ---? print('a') return {line=loc2.line, screen_line=loc2.screen_line-1, screen_pos=1} elseif loc2.line == 1 then ---? print('b') return loc2 else Text.populate_screen_line_starting_pos(State, loc2.line-1) diff --git a/source_text_tests.lua b/source_text_tests.lua index ecffb13..89ad1ce 100644 --- a/source_text_tests.lua +++ b/source_text_tests.lua @@ -14,6 +14,34 @@ 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 @@ -695,6 +723,36 @@ 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_can_start_from_middle_of_long_wrapping_line() io.write('\ntest_pagedown_can_start_from_middle_of_long_wrapping_line') -- draw a few lines starting from a very long wrapping line @@ -1527,7 +1585,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{'abc', 'def', 'ghi', 'deg'} + Editor_state.lines = load_array{'```lines', '```', '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/source_undo.lua b/source_undo.lua index 0aa6755..6023324 100644 --- a/source_undo.lua +++ b/source_undo.lua @@ -50,6 +50,8 @@ 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, @@ -58,7 +60,19 @@ function snapshot(State, s,e) -- deep copy lines without cached stuff like text fragments for i=s,e do local line = State.lines[i] - table.insert(event.lines, {data=line.data, dataB=line.dataB}) + if line.mode == 'text' then + table.insert(event.lines, {mode='text', data=line.data, dataB=line.dataB}) + 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 end return event end diff --git a/text.lua b/text.lua index d6d44a4..06ba584 100644 --- a/text.lua +++ b/text.lua @@ -113,7 +113,7 @@ function Text.compute_fragments(State, line_index) for frag in line.data:gmatch('%S*%s*') do local frag_text = App.newText(love.graphics.getFont(), frag) local frag_width = App.width(frag_text) ---? print('x: '..tostring(x)..'; '..tostring(State.right-x)..'px to go') +--? print('x: '..tostring(x)..'; frag_width: '..tostring(frag_width)..'; '..tostring(State.right-x)..'px to go') while x + frag_width > State.right do --? print(('checking whether to split fragment ^%s$ of width %d when rendering from %d'):format(frag, frag_width, x)) if (x-State.left) < 0.8 * (State.right-State.left) then @@ -356,8 +356,7 @@ function Text.insert_return(State) 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) - State.cursor1.line = State.cursor1.line+1 - State.cursor1.pos = 1 + State.cursor1 = {line=State.cursor1.line+1, pos=1} end function Text.pageup(State) @@ -373,8 +372,7 @@ function Text.pageup(State) top2 = Text.previous_screen_line(State, top2) end State.screen_top1 = Text.to1(State, top2) - State.cursor1.line = State.screen_top1.line - State.cursor1.pos = State.screen_top1.pos + State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos} Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State) --? print(State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos) --? print('pageup end') @@ -393,12 +391,10 @@ function Text.pagedown(State) if Text.lt1(State.screen_top1, new_top1) then State.screen_top1 = new_top1 else - State.screen_top1.line = State.screen_bottom1.line - State.screen_top1.pos = State.screen_bottom1.pos + State.screen_top1 = {line=State.screen_bottom1.line, pos=State.screen_bottom1.pos} end --? print('setting top to', State.screen_top1.line, State.screen_top1.pos) - State.cursor1.line = State.screen_top1.line - State.cursor1.pos = State.screen_top1.pos + State.cursor1 = {line=State.screen_top1.line, pos=State.screen_top1.pos} Text.move_cursor_down_to_next_text_line_while_scrolling_again_if_necessary(State) --? print('top now', State.screen_top1.line) Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks @@ -463,6 +459,7 @@ function Text.down(State) local scroll_down = Text.le1(State.screen_bottom1, State.cursor1) --? print('cursor is NOT at final screen line of its line') local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1) + Text.populate_screen_line_starting_pos(State, State.cursor1.line) local new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index+1] --? print('switching pos of screen line at cursor from '..tostring(screen_line_starting_pos)..' to '..tostring(new_screen_line_starting_pos)) local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos) @@ -608,8 +605,12 @@ end -- should never modify State.cursor1 function Text.snap_cursor_to_bottom_of_screen(State) +--? print('to2:', State.cursor1.line, State.cursor1.pos) local top2 = Text.to2(State, State.cursor1) +--? print('to2: =>', top2.line, top2.screen_line, top2.screen_pos) + -- slide to start of screen line top2.screen_pos = 1 -- start of screen line +--? print('snap', State.screen_top1.line, State.screen_top1.pos, State.screen_top1.posB, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos) --? print('cursor pos '..tostring(State.cursor1.pos)..' is on the #'..tostring(top2.screen_line)..' screen line down') local y = App.screen.height - State.line_height -- duplicate some logic from love.draw @@ -626,6 +627,7 @@ function Text.snap_cursor_to_bottom_of_screen(State) --? print('top2 finally:', top2.line, top2.screen_line, top2.screen_pos) State.screen_top1 = Text.to1(State, top2) --? print('top1 finally:', State.screen_top1.line, State.screen_top1.pos) +--? print('snap =>', State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos, State.screen_bottom1.line, State.screen_bottom1.pos) Text.redraw_all(State) -- if we're scrolling, reclaim all fragments to avoid memory leaks end @@ -782,7 +784,7 @@ function Text.x(s, pos) end function Text.to2(State, loc1) - local result = {line=loc1.line, screen_line=1} + local result = {line=loc1.line} local line_cache = State.line_cache[loc1.line] Text.populate_screen_line_starting_pos(State, loc1.line) for i=#line_cache.screen_line_starting_pos,1,-1 do |