-- primitives for saving to file and loading from file function file_exists(filename) local infile = App.open_for_reading(filename) if infile then infile:close() return true else return false end end function load_from_disk(State) local infile = App.open_for_reading(State.filename) State.lines = load_from_file(infile) if infile then infile:close() end end function load_from_file(infile) local result = {} if infile then local infile_next_line = infile:lines() -- works with both Lua files and LÖVE Files (https://www.love2d.org/wiki/File) while true do local line = infile_next_line() if line == nil then break end if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated table.insert(result, load_drawing(infile_next_line)) else table.insert(result, {mode='text', data=line}) end end end if #result == 0 then table.insert(result, {mode='text', data=''}) end return result end function save_to_disk(State) -- save the payload log(2, 'save_to_disk: '..State.id) local outfile = App.open_for_writing(State.filename) if not outfile then error('failed to write to "'..State.filename..'"') end for _,line in ipairs(State.lines) do if line.mode == 'drawing' then store_drawing(outfile, line) else outfile:write(line.data) outfile:write('\n') end end outfile:close() -- skip other writes in tests if App.run_tests then return end -- log(2, 'save_to_disk: about to save links for '..State.id) save_links(State.id) log(2, 'save_to_disk: done saving links for '..State.id) -- update recent if not State.recent_updated then if State.id then local f, err = io.open(Directory..'recent', 'a') if not f then error(err) end assert(not err, 'failed to save recent') f:write(State.id, '\n') f:close() end State.recent_updated = true end log(2, 'save_to_disk: done updating recent with '..State.id) 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, 'drawing in file is incomplete') 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(false, ('unknown drawing mode %s'):format(shape.mode)) 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)) outfile:write('\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) outfile:write('\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) outfile:write('\n') elseif shape.mode == 'circle' then outfile:write(json.encode({mode=shape.mode, center=drawing.points[shape.center], radius=shape.radius})) outfile:write('\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})) outfile:write('\n') elseif shape.mode == 'deleted' then -- ignore else assert(false, ('unknown drawing mode %s'):format(shape.mode)) end end outfile:write('```\n') end function load_links(id) print(id) local infile = App.open_for_reading(Directory..id..'.json') if not infile then return {} end return json.decode(infile:read()) end function save_links(id) local links_filename = Directory..id..'.json' local status = App.mkdir(dirname(links_filename)) assert(status, 'failed to create directory for links') log(2, 'save_links: '..id) if empty(Links[id]) then print_and_log('save_links: no links; getting rid of .json if it exists') App.remove(links_filename) return end local outfile = App.open_for_writing(links_filename) if not outfile then log(2, 'save_links; error') error('failed to write to "'..links_filename..'"') end emit_links_in_json_in_consistent_order(outfile, Links[id]) outfile:write('\n') outfile:close() log(2, 'save_links done: '..id) end -- 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 --? print(line) if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated --? print('inserting drawing') i, drawing = load_drawing_from_array(next_line, a, i) --? print('i now', i) table.insert(result, drawing) else --? print('inserting text') table.insert(result, {mode='text', data=line}) end end if #result == 0 then 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, 'drawing in array is incomplete') --? 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 assert(false, ('unknown drawing mode %s'):format(shape.mode)) end table.insert(drawing.shapes, shape) end return i, drawing end -- append to a potentially-nonexistent file function append_to_file(filename, contents) local old_contents = '' local f, err = App.open_for_reading(filename) if f then old_contents = f:read() f:close() end f, err = App.open_for_writing(filename) if not f then return nil, err end f:write(old_contents..contents) f:close() return --[[success]] true, --[[err]] nil end function is_absolute_path(path) local os_path_separator = package.config:sub(1,1) if os_path_separator == '/' then -- POSIX systems permit backslashes in filenames return path:sub(1,1) == '/' elseif os_path_separator == '\\' then if path:sub(2,2) == ':' then return true end -- DOS drive letter followed by volume separator local f = path:sub(1,1) return f == '/' or f == '\\' else error('What OS is this? LÖVE reports that the path separator is "'..os_path_separator..'"') end end function is_relative_path(path) return not is_absolute_path(path) end function dirname(path) local os_path_separator = package.config:sub(1,1) if os_path_separator == '/' then -- POSIX systems permit backslashes in filenames return path:match('.*/') or './' elseif os_path_separator == '\\' then return path:match('.*[/\\]') or './' else error('What OS is this? LÖVE reports that the path separator is "'..os_path_separator..'"') end end function test_dirname() check_eq(dirname('a/b'), 'a/', 'F - test_dirname') check_eq(dirname('x'), './', 'F - test_dirname/current') end function basename(path) local os_path_separator = package.config:sub(1,1) if os_path_separator == '/' then -- POSIX systems permit backslashes in filenames return string.gsub(path, ".*/(.*)", "%1") elseif os_path_separator == '\\' then return string.gsub(path, ".*[/\\](.*)", "%1") else error('What OS is this? LÖVE reports that the path separator is "'..os_path_separator..'"') end end function empty(h) for _,_ in pairs(h) do return false end return true end