VLTU33KW2BVZFSNENNEGWMFZNWSWSTOXS6NHG7NWW43N3NDCNLTAC RO35V4H43CW3DC6W5UBL7BXSY5ATSO2FOBJUVUC72I6DTCRLINDQC NUJMQXN64UTFVNRFCJFNVJ7J4G5NVCQA4MFIFNBS626TRHCTESMQC CAG7PP5YB4GZLDUIYLOESJIY5D5NMMGDQ5SCY6HQ7VF2TCK3JLQQC KKQKPGCIHAG2JESQAWEMCBTAKBDC5AVIQ6LCZ2ORQM2AUCFQYLSQC R5QXEHUIZLELJGGCZAE7ATNS3CLRJ7JFRENMGH4XXH24C5WABZDQC VHQCNMARPMNBSIUFLJG7HVK4QGDNPCGNVFLHS3I4IGNVSV5MRLYQC G3DLS5OUO77V4MC6754KTETRCTVUBYBHMGR7MTV52IYYM7QA3ROQC BLWAYPKV3MLDZ4ALXLUJ25AIR6PCIL4RFYNRYLB26GFVC2KQBYBAC M5JXTW56XOOHCPAXPRERPQHHJUNTLGYTDIKTRGQZVW2HVIBKSI5QC ORKN6EOBUFVAD2TXYW5OIKSL55RU24LOFDTTTXHDZUZ57QRDCY7QC 34BZ5ZKNAB4XQGXOPVBZHBDYD5D3X4V6T72XZSR5LJXF4UIVSWQAC 2CK5QI7WA7M4IVSACFGOJYAIDKRUTZVMMPSFWEJTUNMWTN7AX4NAC KYNGDE2CKNOKUC2XMAS5MEU6YT2C3IW5SIZLOJE64G3ERT7BSWFAC JOPVPUSAMMU6RFVDQR4NJC4GNNUFB7GPKVH7OS5FKCYS5QZ53VLQC QD4LOFQRYRXS5GCJLR4EKQPQBFYZA5CGRVJYVFT2U7GXZDAIUJNQC 3PSFWAILGRA4OYXWS2DX7VF332AIBPYBXHEA4GIQY2XEJVD65UMAC KMSL74GAMFNTAKGDKZFP2AMQXUMOC3XH373BO4IABZWBEP3YAXKAC X43ZIKR3WHHUHTIBN76H25FLACL7SMKXXHFX7GDD552UF3V2UDPAC ONHKBLLCD5NDO3HSSUMAMGJ7HDT53JYVV56DI42AEYI3W63GKACQC 2TQUKHBC2EB3WDBD5UL62DQYV7CV6B7OJYK7CHOEDNOZENSOG42AC KWIVKQQ7AANRG6R4ZRB5TDBZ2TZTXAXIR2P6JNT362KIAJ7JQ4VQC 2344TV56YERMZVRV4NPBVJSJWDPNUF43FFQAZ22EYUAZDUQU3JAQC ZLJYLPOTXIVBVWJ4NTRM2YCQPT2FCSN7446P56MJFEFY45QTB7IAC ATQO62TFDZ7J4RCOSB3K2QCCB5R6PNYQIIGNXTLZMEFG5UG5PUJQC NFI42KGXEHTNACH2CATBLJIAXJYNV2C3QBPR2ORPJZKOVEREVM7QC 36Z442IVPXHZ7D2QI26YLN3TDDEMDRQ2GKBYQAD6NUHQZVCCY4VAC JCXL74WVQ23V53EOCKA2NXQIYA5NNOXGN5WYC7ZW42EI2I6D5IJAC geom = {}function geom.on_shape(x,y, drawing, shape)if shape.mode == 'freehand' thenreturn geom.on_freehand(x,y, drawing, shape)elseif shape.mode == 'line' thenreturn geom.on_line(x,y, drawing, shape)elseif shape.mode == 'manhattan' thenlocal p1 = drawing.points[shape.p1]local p2 = drawing.points[shape.p2]if p1.x == p2.x thenif x ~= p1.x then return false endlocal y1,y2 = p1.y, p2.yif y1 > y2 theny1,y2 = y2,y1endreturn y >= y1-2 and y <= y2+2elseif p1.y == p2.y thenif y ~= p1.y then return false endlocal x1,x2 = p1.x, p2.xif x1 > x2 thenx1,x2 = x2,x1endreturn x >= x1-2 and x <= x2+2endelseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' thenreturn geom.on_polygon(x,y, drawing, shape)elseif shape.mode == 'circle' thenlocal 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.05elseif shape.mode == 'arc' thenlocal 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 thenreturn falseendreturn geom.angle_between(center.x,center.y, x,y, shape.start_angle,shape.end_angle)elseif shape.mode == 'deleted' thenelseassert(false, ('unknown drawing mode %s'):format(shape.mode))endendfunction geom.on_freehand(x,y, drawing, shape)local prevfor _,p in ipairs(shape.points) doif prev thenif geom.on_line(x,y, drawing, {p1=prev, p2=p}) thenreturn trueendendprev = pendreturn falseendfunction geom.on_line(x,y, drawing, shape)local p1,p2if type(shape.p1) == 'number' thenp1 = drawing.points[shape.p1]p2 = drawing.points[shape.p2]elsep1 = shape.p1p2 = shape.p2endif p1.x == p2.x thenif math.abs(p1.x-x) > 2 thenreturn falseendlocal y1,y2 = p1.y,p2.yif y1 > y2 theny1,y2 = y2,y1endreturn y >= y1-2 and y <= y2+2end-- has the right slope and interceptlocal 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 thenreturn falseend-- between endpointslocal k = (x-p1.x) / (p2.x-p1.x)return k > -0.005 and k < 1.005endfunction geom.on_polygon(x,y, drawing, shape)local prevfor _,p in ipairs(shape.vertices) doif prev thenif geom.on_line(x,y, drawing, {p1=prev, p2=p}) thenreturn trueendendprev = pendreturn 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 thenreturn math.sign(x3-x1) == math.sign(x4-x1)endif y1 == y2 thenreturn math.sign(y3-y1) == math.sign(y4-y1)endlocal m = (y2-y1)/(x2-x1)return math.sign(m*(x3-x1) + y1-y3) == math.sign(m*(x4-x1) + y1-y4)endfunction math.sign(x)if x > 0 thenreturn 1elseif x == 0 thenreturn 0elseif x < 0 thenreturn -1endendfunction 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 doresult = result-math.pi*2endwhile result < hint-math.pi/10 doresult = result+math.pi*2endendreturn resultend-- 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 thenresult = result+math.piendreturn resultend-- 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 thens,e = e,send-- I'm not sure this is right or ideal..angle = angle-math.pi*2if s <= angle and angle <= e thenreturn trueendangle = angle+math.pi*2if s <= angle and angle <= e thenreturn trueendangle = angle+math.pi*2return s <= angle and angle <= eendfunction geom.dist(x1,y1, x2,y2) return ((x2-x1)^2+(y2-y1)^2)^0.5 endendendfunction geom.on_freehand(x,y, drawing, shape)local prevfor _,p in ipairs(shape.points) doif prev thenif geom.on_line(x,y, drawing, {p1=prev, p2=p}) thenreturn trueendendprev = pendreturn falseendfunction geom.on_line(x,y, drawing, shape)local p1,p2if type(shape.p1) == 'number' thenp1 = drawing.points[shape.p1]p2 = drawing.points[shape.p2]elsep1 = shape.p1p2 = shape.p2endif p1.x == p2.x then
assert(false, ('unknown drawing mode %s'):format(shape.mode))assert(false, ('unknown drawing mode %s'):format(shape.mode))assert(false, ('unknown drawing mode %s'):format(State.current_drawing_mode))assert(drawing.mode == 'drawing', 'Drawing.update: line is not a drawing')assert(#drawing.pending.vertices <= 2, 'Drawing.mouse_release: rectangle has too many pending vertices')assert(#drawing.pending.vertices <= 2, 'Drawing.mouse_release: square has too many pending vertices')assert(false, ('unknown drawing mode %s'):format(drawing.pending.mode))assert(idx, 'point to delete is not in vertices')assert(false, ('unknown drawing mode %s'):format(shape.mode))assert(shape.mode == 'freehand', 'can only smoothen freehand shapes')for _=1,7 dofor i=2,#shape.points-1 dolocal 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)endendendfunction round(num)return math.floor(num+.5)endfunction Drawing.find_or_insert_point(points, x,y, width)-- check if UI would snap the two points togetherfor i,point in ipairs(points) doif Drawing.near(point, x,y, width) thenreturn iendendtable.insert(points, {x=x, y=y})return #pointsendfunction 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_distanceendfunction Drawing.pixels(n, width) -- parts to pixelsreturn math.floor(n*width/256)endfunction Drawing.coord(n, width) -- pixels to partsreturn math.floor(n*256/width)endfunction table.find(h, x)for k,v in pairs(h) doif v == x thenreturn kendendendendendfunction Drawing.smoothen(shape)table.remove(shape.vertices, idx)if #shape.vertices < 3 thenshape.mode = 'deleted'endelseshape.mode = 'deleted'endendenddrawing.points[i].deleted = trueendlocal drawing,_,_,shape = Drawing.select_shape_at_mouse(State)if drawing thenshape.mode = 'deleted'endelseif chord == 'C-h' and not App.mouse_down(1) thenlocal drawing = Drawing.select_drawing_at_mouse(State)if drawing thendrawing.show_help = trueendelseif chord == 'escape' and App.mouse_down(1) thenlocal _,drawing = Drawing.current_drawing(State)drawing.pending = {}endendfunction Drawing.complete_rectangle(firstx,firsty, secondx,secondy, x,y)if firstx == secondx thenreturn x,secondy, x,firstyendif firsty == secondy thenreturn secondx,y, firstx,yendlocal 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_equationlocal a = 1/first_slopelocal c = -secondy - secondx/first_slopelocal 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 pointlocal a = -first_slopelocal c = -thirdy + thirdx*first_slopelocal 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,fourthyendfunction 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 onlocal deltax = secondx-firstxlocal deltay = secondy-firstylocal thirdx = secondx+deltaylocal thirdy = secondy-deltaxif not geom.same_side(firstx,firsty, secondx,secondy, thirdx,thirdy, x,y) thendeltax = -deltaxdeltay = -deltaythirdx = secondx+deltaythirdy = secondy-deltaxendlocal fourthx = firstx+deltaylocal fourthy = firsty-deltaxreturn thirdx,thirdy, fourthx,fourthyendfunction Drawing.current_drawing(State)local x, y = App.mouse_x(), App.mouse_y()for drawing_index,drawing in ipairs(State.lines) doif drawing.mode == 'drawing' thenlocal line_cache = State.line_cache[drawing_index]if Drawing.in_drawing(drawing, line_cache, x,y, State.left,State.right) thenreturn drawing_index,drawing,line_cacheendendendreturn nilendfunction Drawing.select_shape_at_mouse(State)for drawing_index,drawing in ipairs(State.lines) doif drawing.mode == 'drawing' thenlocal 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) thenlocal mx,my = Drawing.coord(x-State.left, State.width), Drawing.coord(y-line_cache.starty, State.width)for i,shape in ipairs(drawing.shapes) dofunction Drawing.keychord_press(State, chord)endState.lines.current_drawing.pending = {}State.lines.current_drawing = nilendendendif #drawing.pending.vertices == 2 thenlocal 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 thenlocal 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)endendelseif drawing.pending.mode == 'circle' thenlocal 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 thenlocal center = drawing.points[drawing.pending.center]drawing.pending.radius = round(geom.dist(center.x,center.y, mx,my))table.insert(drawing.shapes, drawing.pending)endelseif drawing.pending.mode == 'arc' thenlocal 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 thenlocal 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)endelseif drawing.pending.mode == 'name' then-- drop itelseif #drawing.pending.vertices == 2 thenlocal 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 thenlocal 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)endelse-- too few points; draw nothingendelseif drawing.pending.mode == 'square' thenfunction Drawing.mouse_release(State, x,y, mouse_button)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) thenif Drawing.in_drawing(drawing, line_cache, pmx,pmy, State.left,State.right) thenif drawing.pending.mode == 'freehand' thentable.insert(drawing.pending.points, {x=mx, y=my})elseif drawing.pending.mode == 'move' thendrawing.pending.target_point.x = mxdrawing.pending.target_point.y = myDrawing.relax_constraints(drawing, drawing.pending.target_point_index)endendelseif State.current_drawing_mode == 'move' thenif Drawing.in_drawing(drawing, line_cache, pmx, pmy, State.left,State.right) thendrawing.pending.target_point.x = mxdrawing.pending.target_point.y = myDrawing.relax_constraints(drawing, drawing.pending.target_point_index)endelse-- do nothingendendfunction Drawing.relax_constraints(drawing, p)for _,shape in ipairs(drawing.shapes) doif shape.mode == 'manhattan' thenif shape.p1 == p thenshape.mode = 'line'elseif shape.p2 == p thenshape.mode = 'line'endelseif shape.mode == 'rectangle' or shape.mode == 'square' thenfor _,v in ipairs(shape.vertices) doif v == p thenshape.mode = 'polygon'endendendendendendend-- a couple of operations on drawings need to constantly check the state of the mousefunction Drawing.update(State)if State.lines.current_drawing == nil then return endlocal drawing = State.lines.current_drawinglocal line_cache = State.line_cache[State.lines.current_drawing_index]function Drawing.mouse_press(State, drawing_index, x,y, mouse_button)endendfunction Drawing.in_drawing(drawing, line_cache, x,y, left,right)if line_cache.starty == nil then return false end -- outside current pagelocal width = right-leftreturn y >= line_cache.starty and y < line_cache.starty + Drawing.pixels(drawing.h, width) and x >= left and x < rightend-- after mouse_releaseendendfunction Drawing.draw_pending_shape(drawing, top, left,right)local width = right-leftlocal pmx,pmy = App.mouse_x(), App.mouse_y()local function px(x) return Drawing.pixels(x, width)+left endlocal function py(y) return Drawing.pixels(y, width)+top endlocal 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
assert(s, 'failed to snapshot operation for undo history')assert(#State.lines > 0, 'failed to snapshot operation for undo history')assert(false, ('unknown line mode %s'):format(line.mode))assert(from.start_line == to.start_line, 'failed to patch undo operation')assert(#to.lines == to.end_line-to.start_line+1, 'failed to patch undo operation')assert(from.start_line == to.start_line, 'failed to patch undo operation')assert(#to.lines == to.end_line-to.start_line+1, 'failed to patch undo operation')for i=1,#to.lines dotable.insert(line_cache, to.start_line+i-1, {})endend-- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080function deepcopy(obj, seen)if type(obj) ~= 'table' then return obj endif seen and seen[obj] then return seen[obj] endlocal s = seen or {}local result = setmetatable({}, getmetatable(obj))s[obj] = resultfor k,v in pairs(obj) doresult[deepcopy(k, s)] = deepcopy(v, s)endreturn resultendfunction minmax(a, b)return math.min(a,b), math.max(a,b)endfor i=from.end_line,from.start_line,-1 dotable.remove(line_cache, i)endfor i=1,#to.lines dotable.insert(lines, to.start_line+i-1, to.lines[i])endendfunction patch_placeholders(line_cache, from, to)for i=from.end_line,from.start_line,-1 dotable.remove(lines, i)endendif s < 1 then s = 1 endif s > #State.lines then s = #State.lines endif e < 1 then e = 1 endif e > #State.lines then e = #State.lines end-- compare with App.initialize_globalslocal event = {screen_top=deepcopy(State.screen_top1),selection=deepcopy(State.selection1),cursor=deepcopy(State.cursor1),if e == nil thene = send
assert(maxl == line_index, ('maxl %d not equal to line_index %d'):format(maxl, line_index))assert(minl == line_index, ('minl %d not equal to line_index %d'):format(minl, line_index))assert(minl == maxl and minl == line_index, ('minl %d, maxl %d and line_index %d are not all equal'):format(minl, maxl, line_index))assert(minl < maxl, ('minl %d not < maxl %d'):format(minl, maxl))assert(minl < maxl, ('minl %d not < maxl %d'):format(minl, maxl))local result = {State.lines[minl].data:sub(min_offset)}for i=minl+1,maxl-1 doif State.lines[i].mode == 'text' thentable.insert(result, State.lines[i].data)endendtable.insert(result, State.lines[maxl].data:sub(1, max_offset-1))return table.concat(result, '\n')endlocal rhs = State.lines[maxl].data:sub(max_offset)for i=maxl,minl+1,-1 dotable.remove(State.lines, i)table.remove(State.line_cache, i)endState.lines[minl].data = State.lines[minl].data:sub(1, min_offset-1)..rhsendfunction Text.selection(State)if State.selection1.line == nil then return end-- min,max = sorted(State.selection1,State.cursor1)local minl,minp = State.selection1.line,State.selection1.poslocal maxl,maxp = State.cursor1.line,State.cursor1.posif minl > maxl thenminl,maxl = maxl,minlminp,maxp = maxp,minpelseif minl == maxl thenif minp > maxp thenminp,maxp = maxp,minpendendlocal min_offset = Text.offset(State.lines[minl].data, minp)local max_offset = Text.offset(State.lines[maxl].data, maxp)if minl == maxl thenreturn State.lines[minl].data:sub(min_offset, max_offset-1)endreturn minp,maxpendend-- draw highlight for line corresponding to (lo,hi) given an approximate x,y and pos on the same screen line-- Creates text objects every time, so use this sparingly.-- Returns some intermediate computation useful elsewhere.function Text.draw_highlight(State, line, x,y, pos, lo,hi)if lo thenlocal lo_offset = Text.offset(line.data, lo)local hi_offset = Text.offset(line.data, hi)local pos_offset = Text.offset(line.data, pos)local lo_pxif pos == lo thenlo_px = 0elselocal before = line.data:sub(pos_offset, lo_offset-1)return minp,bposelsereturn apos,maxpelseif b_lt then
assert(#line_cache.screen_line_starting_pos >= 1, 'line cache missing screen line info')assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')assert(Text.le1(State.screen_top1, State.cursor1), ('screen_top (line=%d,pos=%d) is below cursor (line=%d,pos=%d)'):format(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos))assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')assert(screen_line_index > 1, 'bumped up against top screen line in line')assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')assert(State.cursor1.pos, 'cursor has no pos')assert(State.cursor1.pos > 1, 'bumped up against start of line')assert(end_offset > start_offset, ('end_offset %d not > start_offset %d'):format(end_offset, start_offset))assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')assert(State.lines[State.cursor1.line].mode == 'text', 'line is not text')assert(false, ('invalid pos %d'):format(loc1.pos))assert(false, ('invalid pos %d'):format(loc1.pos))assert(State.cursor1.line == #State.lines+1, 'tried to ensure bottom line of file is text, but failed')assert(top2.line > 1, 'tried to snap cursor to buttom of screen but failed')assert(State.lines[top2.line-1].mode == 'drawing', "expected a drawing but it's not")assert(my >= line_cache.starty, 'failed to map y pixel to line')assert(false, 'failed to map y pixel to line')assert(false, 'failed to map x pixel to pos')assert(false, 'failed to map x pixel to pos')assert(result.screen_pos, 'failed to convert schema-1 coordinate to schema-2')assert(result, "Text.offset returned nil; this is likely a failure to handle utf8")return resultendfunction Text.previous_screen_line(State, loc2)return resultendfunction Text.to1(State, loc2)endfunction Text.x_after(s, pos)local offset = Text.offset(s, math.min(pos+1, #s+1))local s_before = s:sub(1, offset-1)--? print('^'..s_before..'$')end-- return the nearest index of line (in utf8 code points) which lies entirely-- within x pixels of the left margin-- result: 0 to len+1function Text.nearest_pos_less_than(line, x)--? print('', '-- nearest_pos_less_than', line, x)local len = utf8.len(line)local max_x = Text.x_after(line, len)if x > max_x thenreturn len+1endlocal left, right = 0, len+1while true dolocal curr = math.floor((left+right)/2)local currxmin = Text.x_after(line, curr+1)local currxmax = Text.x_after(line, curr+2)--? print('', x, left, right, curr, currxmin, currxmax)if currxmin <= x and x < currxmax thenreturn currendif left >= right-1 thenreturn leftendif currxmin > x thenright = currelseleft = currendendendfunction Text.screen_line_width(State, line_index, i)local line = State.lines[line_index]local line_cache = State.line_cache[line_index]local start_pos = line_cache.screen_line_starting_pos[i]local start_offset = Text.offset(line.data, start_pos)local screen_lineif i < #line_cache.screen_line_starting_pos thenlocal past_end_pos = line_cache.screen_line_starting_pos[i+1]local past_end_offset = Text.offset(line.data, past_end_pos)screen_line = string.sub(line.data, start_offset, past_end_offset-1)elsescreen_line = string.sub(line.data, start_pos)end-- duplicate some logic from Text.drawlocal y = line_cache.starty-- 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 thenbreakend--? print('skipping drawing of height', h)y = y - htable.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 thenendendif State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) thenState.cursor1.pos = State.cursor1.pos+1if State.cursor1.pos > 1 thenState.cursor1.pos = State.cursor1.pos-1local curr = s:sub(start_offset, end_offset-1)return curr:match(pat)endfunction Text.left(State)if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') thenif 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')--? print('down', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos, State.screen_bottom1.line, State.screen_bottom1.pos)local new_screen_line_starting_pos = State.line_cache[State.cursor1.line].screen_line_starting_pos[screen_line_index-1]local new_screen_line_starting_byte_offset = Text.offset(State.lines[State.cursor1.line].data, new_screen_line_starting_pos)local s = string.sub(State.lines[State.cursor1.line].data, new_screen_line_starting_byte_offset)State.cursor1.pos = new_screen_line_starting_pos + Text.nearest_cursor_pos(s, State.cursor_x, State.left) - 1--? print('cursor pos is now '..tostring(State.cursor1.pos))endif Text.lt1(State.cursor1, State.screen_top1) then--? print('up', State.cursor1.line, State.cursor1.pos, State.screen_top1.line, State.screen_top1.pos)local screen_line_starting_pos, screen_line_index = Text.pos_at_start_of_screen_line(State, State.cursor1)if screen_line_starting_pos == 1 then--? print('cursor is at first screen line of its line')-- line is done; skip to previous text lineschedule_save(State)record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})elseif chord == 'delete' thenlocal byte_offset = Text.offset(State.lines[State.cursor1.line].data, State.cursor1.pos)State.lines[State.cursor1.line].data = string.sub(State.lines[State.cursor1.line].data, 1, byte_offset-1)..t..string.sub(State.lines[State.cursor1.line].data, byte_offset)Text.clear_screen_line_cache(State, State.cursor1.line)State.cursor1.pos = State.cursor1.pos+1for i=1,#line_cache.screen_line_starting_pos dolocal pos = line_cache.screen_line_starting_pos[i]
assert(line, 'drawing in file is incomplete')assert(false, ('unknown drawing mode %s'):format(shape.mode))assert(false, ('unknown drawing mode %s'):format(shape.mode))assert(i, 'drawing in array is incomplete')assert(false, ('unknown drawing mode %s'):format(shape.mode))endtable.insert(drawing.shapes, shape)endreturn i, drawingend--? print(i)if line == '```' then break endlocal shape = json.decode(line)if shape.mode == 'freehand' then-- no changes neededelseif shape.mode == 'line' or shape.mode == 'manhattan' thenlocal name = shape.p1.nameshape.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 = namename = shape.p2.nameshape.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 = nameelseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' thenfor i,p in ipairs(shape.vertices) dolocal name = p.nameshape.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 = nameendelseif shape.mode == 'circle' or shape.mode == 'arc' thenlocal name = shape.center.nameshape.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 = nameelseif shape.mode == 'deleted' then-- ignoreelseendendoutfile:write('```\n')endtable.insert(drawing.shapes, shape)endreturn drawingendfunction store_drawing(outfile, drawing)outfile:write('```lines\n')for _,shape in ipairs(drawing.shapes) doif shape.mode == 'freehand' thenif line == '```' then break endlocal shape = json.decode(line)if shape.mode == 'freehand' then-- no changes neededelseif shape.mode == 'line' or shape.mode == 'manhattan' thenlocal name = shape.p1.nameshape.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 = namename = shape.p2.nameshape.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 = nameelseif shape.mode == 'polygon' or shape.mode == 'rectangle' or shape.mode == 'square' thenfor i,p in ipairs(shape.vertices) dolocal name = p.nameshape.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 = nameendelseif shape.mode == 'circle' or shape.mode == 'arc' thenlocal name = shape.center.nameshape.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 = nameelseif shape.mode == 'deleted' then-- ignoreelse
assert(#State.lines == #State.line_cache, ('line_cache is out of date; %d elements when it should be %d'):format(#State.line_cache, #State.lines))assert(Text.le1(State.screen_top1, State.cursor1), ('screen_top (line=%d,pos=%d) is below cursor (line=%d,pos=%d)'):format(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos))assert(false, ('unknown line mode %s'):format(line.mode))endState.cursor_x = nilState.cursor_y = nillocal y = State.top
assert(#State.lines == #State.line_cache, ('line_cache is out of date; %d elements when it should be %d'):format(#State.line_cache, #State.lines))else assert(line.section_end, "log line has a section name, but it's neither the start nor end of a section")local sectiony = y+State.line_height-Section_border_padding_verticallove.graphics.line(xleft,y, xleft,sectiony)love.graphics.line(xright,y, xright,sectiony)love.graphics.line(xleft,sectiony, xleft+50-2,sectiony)local mouse_line_index = log_browser.line_index(State, App.mouse_x(), App.mouse_y())local y = State.topfor line_index = State.screen_top1.line,#State.lines doApp.color(Text_color)local line = State.lines[line_index]if y + State.line_height > App.screen.height then break endlocal height = State.line_heightif should_show(line) thenlocal xleft = render_stack_left_margin(State, line_index, line, y)local xright = render_stack_right_margin(State, line_index, line, y)if line.section_name thenApp.color(Section_border_color)
assert(index, 'file missing from manifest')table.remove(File_navigation.all_candidates, index)table.insert(File_navigation.all_candidates, 1, s)endfunction reset_file_navigator()Show_file_navigator = falseFile_navigation.index = 1File_navigation.filter = ''File_navigation.candidates = File_navigation.all_candidatesend