KKQKPGCIHAG2JESQAWEMCBTAKBDC5AVIQ6LCZ2ORQM2AUCFQYLSQC
44BTGR7UG3PYPZQ5EHUOOSMJ4P5NV2FO23EFHMKVOQLFRTT6TJTQC
WB6SIB7HPVIOOFHO4BKYZMD23M4GFN2RMK47CZBFBALSVOKTUFUAC
7YGYHOEOWGHRJ54YP6EEHVAGLUWMDATSRUC4IH4XRRHUBBRZQSVAC
TBTRYEBPU7OOMGLGMFM6KF7CXXKH5R6YGAOOPGRLCJGRO7NN44VAC
G3DLS5OUO77V4MC6754KTETRCTVUBYBHMGR7MTV52IYYM7QA3ROQC
R5QXEHUIZLELJGGCZAE7ATNS3CLRJ7JFRENMGH4XXH24C5WABZDQC
3PSFWAILGRA4OYXWS2DX7VF332AIBPYBXHEA4GIQY2XEJVD65UMAC
KKMFQDR43ZWVCDRHQLWWX3FCWCFA3ZSXYOBRJNPHUQZR2XPKWULAC
ZLJYLPOTXIVBVWJ4NTRM2YCQPT2FCSN7446P56MJFEFY45QTB7IAC
OI4FPFINEROK6GNDEMOBTGSPYIULCLRGGT5W3H7VLM7VFH22GMWQC
5SM6DRHKPLFWQPCZDTVM4ENVENWBYBQ3Q2KYKSWLWYUTOSEPCRLAC
G54H3YG2NEZPW2F6OYT5JPV7KSKVMNW5D3QT3FBCXTJHAQYTV5UAC
GZ5WULJVEZJJQPQPSQZE7CEPIYPJ2BJDYUJBMZRA5HLOO7TE3DOQC
AIBA4RWQ7W4YNWHFR3LDJUWYSF3P3Z2676YNC5X5KVRNE6FHI2BQC
R3KXFRZNL4CAT5OSKIIGWR3CHL2YJ5S4TKQDIPEHIJ2HW2WS46BQC
W6XUYQKPHYZLEMBPY5L2WCGXIDBKAHVAWAH5V7BKYQXZO33DTNIQC
73OCE2MCBJJZZMN2KYPJTBOUCKBZAOQ2QIAMTGCNOOJ2AJAXFT2AC
MD3W5IRAC6UQALQE4LJC52VQNDO3I3HXF3XE2XHDABXBYJBUVAXQC
MXA3RZYKUI4UF2ISY7JEF6VKX6NOPZMZH5SLLCZHRJKFIXXXDPSAC
BULPIBEGL7TMK6CVIE7IS7WGAHGOSUJBGJSFQK542MOWGHP2ADQQC
ZPUQSPQPQFVRUIHGLAWW3IDBYODIWDHO62HAC3WWF5TM3CIJGHNQC
MUJTM6REGQAK3LZTIFWGJRXE2UPCM4HSLXQYSF5ITLXLS6JCVPMQC
Z5HLXU4PJWWJJDBCK52NBD6PIRIA3TAN2BKZB5HBYFGIDBX4F5HAC
V3EABA35RWCOOU5OMIYRWXAKZOLHO4XPGTPOKY24RR2LOAD7ZQAQC
F65ADDGLR2PNXVSM2XBHM3OSLQC2OTRR3GQBI7DJWIKPJCJ5CSOAC
MXSAHZN4AGTGEXFTMREVVYJLR2KY7X2Z4SUYKEPO2TGSVYR6N5MQC
QCPXQ2E3USF3Z6R6WJ2JKHTRMPKA6QWXFKKRMLXA3MXABJEL543AC
MDXGMZU2MBEDMTB755D3RRYEFKF54GTTYTI5XJYKKKN5ZFQWZXTAC
TRNWIQN6RPLDLYWULLKG5L255E7E3DPNGLCSLAF6IJWYQRCCLARQC
242L3OQXTU2TCAINRJXQEEDSXQXM7Y7USUPBK37ZNM3A7V5TUDSAC
PLKNHYZ4KXWWKC2DHXCI4WVO23I7VMEVYT5H2J6JDE4S3D3CHDJQC
B4FAIVRAXKCS2ZZXYUHL7HHWVMDMG2MQ3BY6IOJARK7B5XTBAQNAC
JOPVPUSAMMU6RFVDQR4NJC4GNNUFB7GPKVH7OS5FKCYS5QZ53VLQC
KWIVKQQ7AANRG6R4ZRB5TDBZ2TZTXAXIR2P6JNT362KIAJ7JQ4VQC
REAIVN7WJ3JMUKLX4BIACO2VMNHVTWLP3DMXPYD7PHVSYFFSV64QC
2TQUKHBC2EB3WDBD5UL62DQYV7CV6B7OJYK7CHOEDNOZENSOG42AC
2344TV56YERMZVRV4NPBVJSJWDPNUF43FFQAZ22EYUAZDUQU3JAQC
ONHKBLLCD5NDO3HSSUMAMGJ7HDT53JYVV56DI42AEYI3W63GKACQC
AYX33NBCPCBFWEZI5KZXHP3Y2Q62GYVT4WHYJUYRIPYV67KPSYZAC
LXTTOB33N2HCUZFIUDRQGGBVHK2HODRG4NBLH6RXRQZDCHF27BSAC
VHQCNMARPMNBSIUFLJG7HVK4QGDNPCGNVFLHS3I4IGNVSV5MRLYQC
2XLZCWZCOFZGNAPSD7XNCYITKVY5WZW3OD7IXMC7WJQ5SBI7YHNQC
QYIFOHW3WDDQMK4ATY6IOSQRFHJOQ5QCPDKRC4GVGWLQEH4HGWVQC
YTSPVDZHEN5LLNMGIBUBLPWFWSFM3SOHBRGWYSDEVFKRTH24ARRQC
JDZVBFEIBB5JVB7PJJMKFO4Z52NCEKFFFQ4B4QECCBRPEJ4EPGDQC
NFI42KGXEHTNACH2CATBLJIAXJYNV2C3QBPR2ORPJZKOVEREVM7QC
2L5MEZV344TOZLVY3432RHJFIRVXFD6O3GWLL5O4CV66BGAFTURQC
R2ASHK5CEE3PTRLS37GP4PXJ7HIGJ6UD72KKBI57UDJI7VRROQGQC
4KC7I3E2DIKLIP7LQRKB5WFA2Z5XZXAU46RFHNFQU5BVEJPDX6UQC
ORKN6EOBUFVAD2TXYW5OIKSL55RU24LOFDTTTXHDZUZ57QRDCY7QC
BLWAYPKV3MLDZ4ALXLUJ25AIR6PCIL4RFYNRYLB26GFVC2KQBYBAC
NYQ7HD4D5L44UORK52TH7CAEXYN5CE4ZUVLCWMY6XXPYHXVBTGHAC
2CK5QI7WA7M4IVSACFGOJYAIDKRUTZVMMPSFWEJTUNMWTN7AX4NAC
34BZ5ZKNAB4XQGXOPVBZHBDYD5D3X4V6T72XZSR5LJXF4UIVSWQAC
WTDKUACNTWB4KD34TZZNPILNX4FQ6MR64XYBAA5GOMICF73WLIAAC
KYNGDE2CKNOKUC2XMAS5MEU6YT2C3IW5SIZLOJE64G3ERT7BSWFAC
4PHGNJN64ELUEU4SJH6KG36WNTATT3QSZYTY4I5R3XGRZHZPAQ4QC
SRVDX4I5QKWAH3Y5DX25PG34U7NY55H46ZYG2APH47BUZT3EJ2HAC
XNFTJHC4QSHNSIWNN7K6QZEZ37GTQYKHS4EPNSVPQCUSWREROGIQC
65HNIAOSEEJPYMO5XJBDMLU27X5XD3UJIDGSOS2IZ7CIYQRBOZ2QC
ATQO62TFDZ7J4RCOSB3K2QCCB5R6PNYQIIGNXTLZMEFG5UG5PUJQC
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
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 do
table.insert(line_cache, to.start_line+i-1, {})
end
end
-- https://stackoverflow.com/questions/640642/how-do-you-copy-a-lua-table-by-value/26367080#26367080
function deepcopy(obj, seen)
if type(obj) ~= 'table' then return obj end
if seen and seen[obj] then return seen[obj] end
local s = seen or {}
local result = setmetatable({}, getmetatable(obj))
s[obj] = result
for k,v in pairs(obj) do
result[deepcopy(k, s)] = deepcopy(v, s)
end
return result
end
function minmax(a, b)
return math.min(a,b), math.max(a,b)
end
for i=from.end_line,from.start_line,-1 do
table.remove(line_cache, i)
end
for i=1,#to.lines do
table.insert(lines, to.start_line+i-1, to.lines[i])
end
end
function patch_placeholders(line_cache, from, to)
for i=from.end_line,from.start_line,-1 do
table.remove(lines, i)
end
end
if s < 1 then s = 1 end
if s > #State.lines then s = #State.lines end
if e < 1 then e = 1 end
if e > #State.lines then e = #State.lines end
-- compare with App.initialize_globals
local event = {
screen_top=deepcopy(State.screen_top1),
selection=deepcopy(State.selection1),
cursor=deepcopy(State.cursor1),
if e == nil then
e = s
end
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 result
end
function Text.previous_screen_line(State, loc2)
return result
end
function Text.to1(State, loc2)
end
function 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+1
function 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 then
return len+1
end
local left, right = 0, len+1
while true do
local 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 then
return curr
end
if left >= right-1 then
return left
end
if currxmin > x then
right = curr
else
left = curr
end
end
end
function 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_line
if i < #line_cache.screen_line_starting_pos then
local 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)
else
screen_line = string.sub(line.data, start_pos)
end
-- duplicate some logic from Text.draw
local 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 then
break
end
--? print('skipping drawing of height', h)
y = y - h
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
end
end
if State.cursor1.pos <= utf8.len(State.lines[State.cursor1.line].data) then
State.cursor1.pos = State.cursor1.pos+1
if State.cursor1.pos > 1 then
State.cursor1.pos = State.cursor1.pos-1
local curr = s:sub(start_offset, end_offset-1)
return curr:match(pat)
end
function Text.left(State)
if Text.match(State.lines[State.cursor1.line].data, State.cursor1.pos-1, '%s') then
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')
--? 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))
end
if 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 line
schedule_save(State)
record_undo_event(State, {before=before, after=snapshot(State, State.cursor1.line)})
elseif chord == 'delete' then
local 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+1
for i=1,#line_cache.screen_line_starting_pos do
local 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))
end
table.insert(drawing.shapes, shape)
end
return i, drawing
end
--? 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
end
end
outfile:write('```\n')
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
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
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))
end
State.cursor_x = nil
State.cursor_y = nil
local 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_vertical
love.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.top
for line_index = State.screen_top1.line,#State.lines do
App.color(Text_color)
local line = State.lines[line_index]
if y + State.line_height > App.screen.height then break end
local height = State.line_height
if should_show(line) then
local 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 then
App.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)
assert(Text.le1(State.screen_top1, State.cursor1))
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))
if #State.lines ~= #State.line_cache then
print(('line_cache is out of date; %d when it should be %d'):format(#State.line_cache, #State.lines))
assert(false)
end
if not Text.le1(State.screen_top1, State.cursor1) then
print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
assert(false)
end
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))
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
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
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))
-- for tests
function load_array(a)
local result = {}
local next_line = ipairs(a)
local i,line,drawing = 0, ''
while true do
i,line = next_line(a, i)
if i == nil then break end
table.insert(result, {data=line})
end
if #result == 0 then
table.insert(result, {data=''})
end
return result
-- for tests
function load_array(a)
local result = {}
local next_line = ipairs(a)
local i,line,drawing = 0, ''
while true do
i,line = next_line(a, i)
if i == nil then break end
table.insert(result, {data=line})
end
if #result == 0 then
table.insert(result, {data=''})
end
return result
assert(i, 'drawing in array is incomplete')
assert(false, ('unknown drawing mode %s'):format(shape.mode))
if #State.lines ~= #State.line_cache then
print(('line_cache is out of date; %d when it should be %d'):format(#State.line_cache, #State.lines))
assert(false)
end
if not Text.le1(State.screen_top1, State.cursor1) then
print(State.screen_top1.line, State.screen_top1.pos, State.cursor1.line, State.cursor1.pos)
assert(false)
end
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))
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(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')