-- primitives for saving to file and loading from file function file_exists(filename) local infile = App.open_for_reading(App.save_dir..filename) if not infile then infile = App.open_for_reading(App.source_dir..filename) end if infile then infile:close() return true else return false end end -- the source editor supports only files in the save dir backed by the source dir function load_from_disk(State) local infile = App.open_for_reading(App.save_dir..State.filename) if not infile then infile = App.open_for_reading(App.source_dir..State.filename) end 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) local outfile = App.open_for_writing(App.save_dir..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() 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 -- 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') local line_info = {mode='text'} line_info.data = line table.insert(result, line_info) 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 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