new fork: tighter editor for just text

This commit is contained in:
Kartik K. Agaram 2024-07-28 21:54:04 -07:00
parent aad21bd9a1
commit 6fc74489c4
12 changed files with 210 additions and 1727 deletions

View File

@ -1,11 +1,8 @@
# Plain text with lines
# An editor for plain text.
An editor for plain text where you can also seamlessly insert line drawings.
Designed above all to be easy to modify and give you early warning if your
modifications break something.
http://akkartik.name/lines.html
## Getting started
Install [LÖVE](https://love2d.org). It's just a 5MB download, open-source and
@ -17,13 +14,13 @@ optionally with a file path to edit.
Alternatively, turn it into a .love file you can double-click on:
```
$ zip -r /tmp/lines2.love *.lua
$ zip -r /tmp/text2.love *.lua
```
By default, lines2.love reads/writes the file `lines.txt` in
By default, text2.love reads/writes the file `lines.txt` in
[a directory relative to this app](https://love2d.org/wiki/love.filesystem.getSourceBaseDirectory).
To open a different file, drop it on the lines2.love window.
To open a different file, drop it on the text2.love window.
## Keyboard shortcuts
@ -35,12 +32,7 @@ While editing text:
* `alt+right`/`alt+left` to jump to the next/previous word, respectively
* mouse drag or `shift` + movement to select text, `ctrl+a` to select all
For shortcuts while editing drawings, consult the online help. Either:
* hover on a drawing and hit `ctrl+h`, or
* click on a drawing to start a stroke and then press and hold `h` to see your
options at any point during a stroke.
lines2.love has been exclusively tested so far with a US keyboard layout. If
Exclusively tested so far with a US keyboard layout. If
you use a different layout, please let me know if things worked, or if you
found anything amiss: http://akkartik.name/contact
@ -51,38 +43,16 @@ found anything amiss: http://akkartik.name/contact
* No support yet for right-to-left languages.
* Undo/redo may be sluggish in large files. Large files may grow sluggish in
other ways. lines2.love works well in all circumstances with files under
50KB.
other ways. Works well in all circumstances with files under 50KB.
* If you kill the process, say by force-quitting because things things get
sluggish, you can lose data.
* lines2.love assumes a file always contains at least one line of text. You
can violate this invariant by editing the file outside lines2.love. Don't do
that.
* If you make the first line a drawing there's currently no way to insert
lines above it.
* Help screen may show up in multiple drawings at a time. That feels a bit
klunky.
* Clicking on a drawing to focus cursor on it adds a point. Orphaned points
disappears on reload, but still. Klunky. To avoid this you can move the
cursor using the keyboard, but who can remember that?
* No clipping yet for drawings. In particular, circles/squares/rectangles and
point labels can overflow a drawing.
* If you ever see a crash when clicking on the mouse, it might be because a
mouse press and release need to happen in separate frames. Try pressing and
releasing more slowly and let me know if that helps or not. This is klunky,
sorry.
* Touchpads can drag the mouse pointer using a light touch or a heavy click.
On Linux, drags using the light touch get interrupted when a key is pressed.
You'll have to press down to drag.
* Can't scroll while selecting text with mouse.
* No scrollbars yet. That stuff is hard.
@ -92,18 +62,8 @@ found anything amiss: http://akkartik.name/contact
This repo is a fork of [lines.love](http://akkartik.name/lines.html), aiming
to be more elegant and have fewer bugs. Updates to it can be downloaded from:
* https://git.merveilles.town/akkartik/lines2.love
* https://git.sr.ht/~akkartik/lines2.love
## Associated tools
* https://codeberg.org/akkartik/lines2md exports files to Markdown and
(non-editable) SVG.
* https://git.sr.ht/~akkartik/lines2html.love exports files to html and inline
SVG.
* https://codeberg.org/eril/lines2html.love provides the option to export
just the drawings to a directory. Also provides a CLI, which should be more
natural for many people.
* https://git.merveilles.town/akkartik/text2.love
* https://git.sr.ht/~akkartik/text2.love
## Feedback

View File

@ -1,724 +0,0 @@
-- primitives for editing drawings
Drawing = {}
-- All drawings span 100% of some conceptual 'page width' and divide it up
-- into 256 parts.
function Drawing.draw(Editor, line_index, starty)
local line = Editor.lines[line_index]
local pmx,pmy = love.mouse.getPosition()
local height = Drawing.pixels(line.h, Editor.width)
if line_index == Editor.cursor.line then
App.color(Cursor_color)
love.graphics.rectangle('line', Editor.left, starty, Editor.width, height)
elseif geom.in_rect(pmx,pmy, Editor.left, starty, Editor.width, height) then
App.color(Icon_color)
love.graphics.rectangle('line', Editor.left, starty, Editor.width, height)
end
if geom.in_rect(pmx,pmy, Editor.left, starty, Editor.width, height) then
App.color(Icon_color)
if icon[Editor.current_drawing_mode] then
icon[Editor.current_drawing_mode](Editor.right-22, starty+4)
else
icon[Editor.previous_drawing_mode](Editor.right-22, starty+4)
end
if love.mouse.isDown(1) and love.keyboard.isDown('h') then
draw_help_with_mouse_pressed(Editor, line_index)
return
end
end
if line.show_help then
draw_help_without_mouse_pressed(Editor, line_index)
return
end
local mx = Drawing.coord(pmx-Editor.left, Editor.width)
local my = Drawing.coord(pmy-starty, Editor.width)
for _,shape in ipairs(line.shapes) do
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, starty, Editor.left,Editor.right)
end
local function px(x) return Drawing.pixels(x, Editor.width)+Editor.left end
local function py(y) return Drawing.pixels(y, Editor.width)+starty end
for i,p in ipairs(line.points) do
if p.deleted == nil then
if Drawing.near(p, mx,my, Editor.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 Editor.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_width
if p.name == '' then
name_width = Editor.font:getWidth('m')
else
name_width = Editor.font:getWidth(p.name)
end
love.graphics.rectangle('fill', x,y, name_width, Editor.line_height)
end
end
end
end
App.color(Current_stroke_color)
Drawing.draw_pending_shape(line, starty, Editor.left,Editor.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
assert(false, ('unknown drawing mode %s'):format(shape.mode))
end
end
function Drawing.draw_pending_shape(drawing, top, left,right)
local width = right-left
local pmx,pmy = love.mouse.getPosition()
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_release
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 r = round(geom.dist(center.x, center.y, mx, my))
local cx,cy = px(center.x), py(center.y)
love.graphics.circle('line', cx,cy, Drawing.pixels(r, width))
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
assert(false, ('unknown drawing mode %s'):format(shape.mode))
end
end
function Drawing.in_current_drawing(Editor, x,y, left,right)
if Editor.cursor.mode ~= 'drawing' then return false end
assert(Editor.lines[Editor.cursor.line].mode == 'drawing')
return Drawing.in_drawing(Editor, Editor.cursor.line, x,y, left,right)
end
function Drawing.in_drawing(Editor, drawing_index, x,y, left,right)
assert(Editor.lines[drawing_index].mode == 'drawing')
local _, starty = edit.to_coord(Editor, {mode='drawing', line=drawing_index})
if starty == nil then return false end -- outside current page
local drawing = Editor.lines[drawing_index]
local width = right-left
return y >= starty and y < starty + Drawing.pixels(drawing.h, width) and x >= left and x < right
end
function Drawing.mouse_press(Editor, drawing_index, x,y, mouse_button)
local drawing = Editor.lines[drawing_index]
local _, starty = edit.to_coord(Editor, {mode='drawing', line=drawing_index})
local cx = Drawing.coord(x-Editor.left, Editor.width)
local cy = Drawing.coord(y-starty, Editor.width)
if Editor.current_drawing_mode == 'freehand' then
drawing.pending = {mode=Editor.current_drawing_mode, points={{x=cx, y=cy}}}
elseif Editor.current_drawing_mode == 'line' or Editor.current_drawing_mode == 'manhattan' then
local j = Drawing.find_or_insert_point(drawing.points, cx, cy, Editor.width)
drawing.pending = {mode=Editor.current_drawing_mode, p1=j}
elseif Editor.current_drawing_mode == 'polygon' or Editor.current_drawing_mode == 'rectangle' or Editor.current_drawing_mode == 'square' then
local j = Drawing.find_or_insert_point(drawing.points, cx, cy, Editor.width)
drawing.pending = {mode=Editor.current_drawing_mode, vertices={j}}
elseif Editor.current_drawing_mode == 'circle' then
local j = Drawing.find_or_insert_point(drawing.points, cx, cy, Editor.width)
drawing.pending = {mode=Editor.current_drawing_mode, center=j}
elseif Editor.current_drawing_mode == 'move' then
-- all the action is in mouse_release
elseif Editor.current_drawing_mode == 'name' then
-- nothing
else
assert(false, ('unknown drawing mode %s'):format(Editor.current_drawing_mode))
end
end
-- a couple of operations on drawings need to constantly check the state of the mouse
function Drawing.update(Editor)
if Editor.cursor.mode ~= 'drawing' then return end
local drawing = Editor.lines[Editor.cursor.line]
local _, starty = edit.to_coord(Editor, Editor.cursor)
if starty == nil then
-- some event cleared starty just this frame
-- draw in this frame will soon set starty
-- just skip this frame
return
end
assert(drawing.mode == 'drawing', 'Drawing.update: line is not a drawing')
local pmx,pmy = love.mouse.getPosition()
local mx = Drawing.coord(pmx-Editor.left, Editor.width)
local my = Drawing.coord(pmy-starty, Editor.width)
if love.mouse.isDown(1) then
if Drawing.in_current_drawing(Editor, pmx,pmy, Editor.left,Editor.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 Editor.current_drawing_mode == 'move' then
if Drawing.in_current_drawing(Editor, pmx, pmy, Editor.left,Editor.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_release(Editor, x,y, mouse_button)
if Editor.cursor.mode ~= 'drawing' then return end
if Editor.current_drawing_mode == 'move' then
Editor.current_drawing_mode = Editor.previous_drawing_mode
Editor.previous_drawing_mode = nil
else
local drawing = Editor.lines[Editor.cursor.line]
local _, starty = edit.to_coord(Editor, Editor.cursor)
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-Editor.left, Editor.width), Drawing.coord(y-starty, Editor.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, Editor.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-Editor.left, Editor.width), Drawing.coord(y-starty, Editor.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, Editor.width)
else
drawing.pending.p2 = Drawing.find_or_insert_point(drawing.points, p1.x, my, Editor.width)
end
local p2 = drawing.points[drawing.pending.p2]
love.mouse.setPosition(Editor.left+Drawing.pixels(p2.x, Editor.width), starty+Drawing.pixels(p2.y, Editor.width))
table.insert(drawing.shapes, drawing.pending)
end
elseif drawing.pending.mode == 'polygon' then
local mx,my = Drawing.coord(x-Editor.left, Editor.width), Drawing.coord(y-starty, Editor.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, Editor.width))
table.insert(drawing.shapes, drawing.pending)
end
elseif drawing.pending.mode == 'rectangle' then
assert(#drawing.pending.vertices <= 2, 'Drawing.mouse_release: rectangle has too many pending vertices')
if #drawing.pending.vertices == 2 then
local mx,my = Drawing.coord(x-Editor.left, Editor.width), Drawing.coord(y-starty, Editor.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, Editor.width))
table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, fourthx,fourthy, Editor.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, 'Drawing.mouse_release: square has too many pending vertices')
if #drawing.pending.vertices == 2 then
local mx,my = Drawing.coord(x-Editor.left, Editor.width), Drawing.coord(y-starty, Editor.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, Editor.width))
table.insert(drawing.pending.vertices, Drawing.find_or_insert_point(drawing.points, fourthx,fourthy, Editor.width))
table.insert(drawing.shapes, drawing.pending)
end
end
elseif drawing.pending.mode == 'circle' then
local mx,my = Drawing.coord(x-Editor.left, Editor.width), Drawing.coord(y-starty, Editor.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-Editor.left, Editor.width), Drawing.coord(y-starty, Editor.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
assert(false, ('unknown drawing mode %s'):format(drawing.pending.mode))
end
drawing.pending = {}
end
end
end
function Drawing.keychord_press(Editor, chord)
local pmx,pmy = love.mouse.getPosition()
if chord == 'C-p' and not love.mouse.isDown(1) then
Editor.current_drawing_mode = 'freehand'
elseif love.mouse.isDown(1) and chord == 'l' then
Editor.current_drawing_mode = 'line'
local drawing = Editor.lines[Editor.cursor.line]
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, Editor.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 love.mouse.isDown(1) then
Editor.current_drawing_mode = 'line'
elseif love.mouse.isDown(1) and chord == 'm' then
Editor.current_drawing_mode = 'manhattan'
local drawing = Drawing.select_drawing_at_mouse(Editor)
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, Editor.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 love.mouse.isDown(1) then
Editor.current_drawing_mode = 'manhattan'
elseif chord == 'C-g' and not love.mouse.isDown(1) then
Editor.current_drawing_mode = 'polygon'
elseif love.mouse.isDown(1) and chord == 'g' then
Editor.current_drawing_mode = 'polygon'
local drawing = Editor.lines[Editor.cursor.line]
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, Editor.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 love.mouse.isDown(1) then
Editor.current_drawing_mode = 'rectangle'
elseif love.mouse.isDown(1) and chord == 'r' then
Editor.current_drawing_mode = 'rectangle'
local drawing = Editor.lines[Editor.cursor.line]
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, Editor.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 love.mouse.isDown(1) then
Editor.current_drawing_mode = 'square'
elseif love.mouse.isDown(1) and chord == 's' then
Editor.current_drawing_mode = 'square'
local drawing = Editor.lines[Editor.cursor.line]
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, Editor.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 love.mouse.isDown(1) and chord == 'p' and Editor.current_drawing_mode == 'polygon' then
local drawing = Editor.lines[Editor.cursor.line]
local _, starty = edit.to_coord(Editor, Editor.cursor)
local mx,my = Drawing.coord(pmx-Editor.left, Editor.width), Drawing.coord(pmy-starty, Editor.width)
local j = Drawing.find_or_insert_point(drawing.points, mx,my, Editor.width)
table.insert(drawing.pending.vertices, j)
elseif love.mouse.isDown(1) and chord == 'p' and (Editor.current_drawing_mode == 'rectangle' or Editor.current_drawing_mode == 'square') then
local drawing = Editor.lines[Editor.cursor.line]
local _, starty = edit.to_coord(Editor, Editor.cursor)
local mx,my = Drawing.coord(pmx-Editor.left, Editor.width), Drawing.coord(pmy-starty, Editor.width)
local j = Drawing.find_or_insert_point(drawing.points, mx,my, Editor.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 love.mouse.isDown(1) then
Editor.current_drawing_mode = 'circle'
elseif love.mouse.isDown(1) and chord == 'a' and Editor.current_drawing_mode == 'circle' then
local drawing = Editor.lines[Editor.cursor.line]
local _, starty = edit.to_coord(Editor, Editor.cursor)
drawing.pending.mode = 'arc'
local mx,my = Drawing.coord(pmx-Editor.left, Editor.width), Drawing.coord(pmy-starty, Editor.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 love.mouse.isDown(1) and chord == 'o' then
Editor.current_drawing_mode = 'circle'
local drawing = Editor.lines[Editor.cursor.line]
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, Editor.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 love.mouse.isDown(1) then
if Drawing.in_current_drawing(Editor, pmx, pmy, Editor.left,Editor.right) then
local drawing = Editor.lines[Editor.cursor.line]
local point_index,p = Drawing.select_point_at_mouse(Editor)
if Editor.previous_drawing_mode == nil then
Editor.previous_drawing_mode = Editor.current_drawing_mode
end
Editor.current_drawing_mode = 'move'
drawing.pending = {mode=Editor.current_drawing_mode, target_point=p, target_point_index=point_index}
end
elseif chord == 'C-n' and not love.mouse.isDown(1) then
if Drawing.in_current_drawing(Editor, pmx, pmy, Editor.left,Editor.right) then
local drawing = Editor.lines[Editor.cursor.line]
local point_index,p = Drawing.select_point_at_mouse(Editor)
if Editor.previous_drawing_mode == nil then
Editor.previous_drawing_mode = Editor.current_drawing_mode
end
Editor.current_drawing_mode = 'name'
p.name = ''
drawing.pending = {mode=Editor.current_drawing_mode, target_point=point_index}
end
elseif chord == 'C-d' and not love.mouse.isDown(1) then
if Drawing.in_current_drawing(Editor, pmx, pmy, Editor.left,Editor.right) then
local _, starty = edit.to_coord(Editor, Editor.cursor)
local mx, my = Drawing.coord(pmx-Editor.left, Editor.width), Drawing.coord(pmy-starty, Editor.width)
local drawing = Editor.lines[Editor.cursor.line]
local point_index,p = Drawing.select_point_at_mouse(Editor)
for _,shape in ipairs(drawing.shapes) do
if point_index and Drawing.contains_point(shape, point_index) then
if shape.mode == 'polygon' then
local idx = table.find(shape.vertices, point_index)
assert(idx, 'point to delete is not in vertices')
table.remove(shape.vertices, idx)
if #shape.vertices < 3 then
shape.mode = 'deleted'
end
else
shape.mode = 'deleted'
end
end
if geom.on_shape(mx,my, drawing, shape) then
shape.mode = 'deleted'
end
end
if point_index then
drawing.points[point_index].deleted = true
end
end
elseif chord == 'C-h' and not love.mouse.isDown(1) then
local drawing = Drawing.select_drawing_at_mouse(Editor)
if drawing then
drawing.show_help = true
end
elseif chord == 'escape' and love.mouse.isDown(1) then
local _,drawing = Drawing.current_drawing(Editor)
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.select_point_at_mouse(Editor)
if Editor.cursor.mode ~= 'drawing' then return end
local x,y = love.mouse.getPosition()
if not Drawing.in_current_drawing(Editor, x,y, Editor.left,Editor.right) then return end
local _, starty = edit.to_coord(Editor, Editor.cursor)
local mx,my = Drawing.coord(x-Editor.left, Editor.width), Drawing.coord(y-starty, Editor.width)
local drawing = Editor.lines[Editor.cursor.line]
assert(drawing.mode == 'drawing')
for i,point in ipairs(drawing.points) do
if Drawing.near(point, mx,my, Editor.width) then
return i,point
end
end
end
function Drawing.select_drawing_at_mouse(Editor)
for drawing_index,drawing in ipairs(Editor.lines) do
if drawing.mode == 'drawing' then
local x,y = love.mouse.getPosition()
if Drawing.in_drawing(Editor, drawing_index, x,y, Editor.left,Editor.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
assert(false, ('unknown drawing mode %s'):format(shape.mode))
end
end
function Drawing.smoothen(shape)
assert(shape.mode == 'freehand', 'can only smoothen freehand shapes')
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

238
edit.lua
View File

@ -1,25 +1,12 @@
-- 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}
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 = {}
function new_editor(top, left, right, bottom, font, font_height, line_height) -- currently always draws to bottom of screen
@ -34,27 +21,12 @@ function new_editor(top, left, right, bottom, font, font_height, line_height) -
font_height = font_height,
line_height = line_height,
-- The editor is for editing an array of lines. A line is either text or a drawing.
-- The editor is for editing an array of lines.
-- The array of lines can never be empty; there must be at least one line for positioning a cursor at.
--
-- A text line is a table with:
-- A line is a table with:
-- mode = 'text',
-- string data,
-- A drawing line is a table with:
-- mode = 'drawing'
-- 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=''}}, -- array of lines
-- We need to track a couple of _locations_:
@ -62,7 +34,6 @@ function new_editor(top, left, right, bottom, font, font_height, line_height) -
cursor = nil, -- location where editing will occur
-- Valid location values:
-- {mode='text', line=, pos=}
-- {mode='drawing', line=}
selection1 = {},
-- some extra state to compute selection between mouse press and release
@ -70,16 +41,12 @@ function new_editor(top, left, right, bottom, font, font_height, line_height) -
old_selection1 = nil,
mousepress_shift = nil,
current_drawing_mode = 'line', -- one of the available shape modes
previous_drawing_mode = nil, -- extra state for some ephemeral modes like moving/deleting/naming points
filename = love.filesystem.getSourceBaseDirectory()..'/lines.txt', -- '/' should work even on Windows
next_save = nil,
-- undo
history = {},
next_history = 1,
drawing_before = nil, -- extra state for drawing operations
-- search
search_term = nil,
@ -90,13 +57,8 @@ end -- new_editor
function edit.scroll_to_top(Editor)
assert(#Editor.lines > 0)
if Editor.lines[1].mode == 'text' then
Editor.screen_top = {mode='text', line=1, pos=1}
Editor.cursor = {mode='text', line=1, pos=1}
else
Editor.screen_top = {mode='drawing', line=1}
Editor.cursor = {mode='drawing', line=1}
end
Editor.screen_top = {mode='text', line=1, pos=1}
Editor.cursor = {mode='text', line=1, pos=1}
end
function edit.valid_loc(Editor, loc)
@ -142,69 +104,47 @@ function edit.draw(Editor)
local y = Editor.top
for line_index, line in array.each(Editor.lines, Editor.screen_top.line) do
if line.mode == 'text' then
local x = Editor.left
local initpos = 1
if line_index == Editor.screen_top.line then
initpos = Editor.screen_top.pos
end
--? print('screen line', line_index, initpos, y)
if line.data == '' then
-- button to insert new drawing
button(Editor, 'draw', {x=Editor.left-Margin_left+4, y=y+4, w=12,h=12, bg={r=1,g=1,b=0},
icon = icon.insert_drawing,
onpress1 = function()
Editor.drawing_before = snapshot(Editor, line_index-1, line_index)
table.insert(Editor.lines, line_index, {mode='drawing', y=y, h=256/2, points={}, shapes={}, pending={}})
Editor.cursor = {mode='drawing', line=line_index}
schedule_save(Editor)
record_undo_event(Editor, {before=Editor.drawing_before, after=snapshot(Editor, line_index-1, line_index+1)})
end,
})
else
for pos,char in utf8chars(line.data, initpos) do
local w = Editor.font:getWidth(char)
if char:match('%s') then
if Text.line_wrap_at_word_boundary(Editor, x, line.data, pos) then
do_it(x,y, w, line_index, pos, char)
x = Editor.left
y = y + Editor.line_height
if y + Editor.line_height > Editor.bottom then
break
end
--? print('screen line', line_index, pos+1, y)
draw_just_cursor(x,y, line_index, pos)
else
do_it(x,y, w, line_index, pos, char)
x = x + w
end
else
if x+w > Editor.right then
draw_just_cursor(x,y, line_index, pos)
x = Editor.left
y = y + Editor.line_height
if y + Editor.line_height > Editor.bottom then
break
end
--? print('screen line', line_index, pos, y)
do_it(x,y, w, line_index, pos, char)
else
do_it(x,y, w, line_index, pos, char)
end
x = x + w
end
end
end
-- draw cursor if it's at end of line
do_it(x,y, 0, line_index, utf8.len(line.data)+1, '')
y = y + Editor.line_height
elseif line.mode == 'drawing' then
local h = Drawing_padding_height + Drawing.pixels(line.h, Editor.width)
Drawing.draw(Editor, line_index, y+Drawing_padding_top)
y = y + h
else
assert(false, ('unknown line mode %s'):format(line.mode))
local x = Editor.left
local initpos = 1
if line_index == Editor.screen_top.line then
initpos = Editor.screen_top.pos
end
--? print('screen line', line_index, initpos, y)
for pos,char in utf8chars(line.data, initpos) do
local w = Editor.font:getWidth(char)
if char:match('%s') then
if Text.line_wrap_at_word_boundary(Editor, x, line.data, pos) then
do_it(x,y, w, line_index, pos, char)
x = Editor.left
y = y + Editor.line_height
if y + Editor.line_height > Editor.bottom then
break
end
--? print('screen line', line_index, pos+1, y)
draw_just_cursor(x,y, line_index, pos)
else
do_it(x,y, w, line_index, pos, char)
x = x + w
end
else
if x+w > Editor.right then
draw_just_cursor(x,y, line_index, pos)
x = Editor.left
y = y + Editor.line_height
if y + Editor.line_height > Editor.bottom then
break
end
--? print('screen line', line_index, pos, y)
do_it(x,y, w, line_index, pos, char)
else
do_it(x,y, w, line_index, pos, char)
end
x = x + w
end
end
-- draw cursor if it's at end of line
do_it(x,y, 0, line_index, utf8.len(line.data)+1, '')
y = y + Editor.line_height
if y + Editor.line_height > Editor.bottom then
break
end
@ -215,7 +155,6 @@ function edit.draw(Editor)
end
function edit.update(Editor, dt)
Drawing.update(Editor, dt)
if Editor.next_save and Editor.next_save < Current_time then
save_to_disk(Editor)
Editor.next_save = nil
@ -254,25 +193,20 @@ function edit.mouse_press(Editor, mx,my, mouse_button)
end
Editor.cursor = loc
if loc.mode == 'text' then
-- prepare for a drag selecting text
-- delicate dance between cursor/selection and old cursor/selection
-- scenarios:
-- regular press+release: sets cursor, clears selection
-- shift press+release:
-- sets selection to old cursor if not set otherwise leaves it untouched
-- sets cursor
-- press and hold to start a selection: sets selection on press, cursor on release
-- press and hold, then press shift: ignore shift
-- i.e. mouse_release should never look at shift state
Editor.old_cursor1 = Editor.cursor
Editor.old_selection1 = Editor.selection1
Editor.mousepress_shift = shift_down()
Editor.selection1 = deepcopy(loc)
else
Editor.drawing_before = snapshot(Editor, loc.line)
Drawing.mouse_press(Editor, loc.line, mx,my, mouse_button)
end
-- prepare for a drag selecting text
-- delicate dance between cursor/selection and old cursor/selection
-- scenarios:
-- regular press+release: sets cursor, clears selection
-- shift press+release:
-- sets selection to old cursor if not set otherwise leaves it untouched
-- sets cursor
-- press and hold to start a selection: sets selection on press, cursor on release
-- press and hold, then press shift: ignore shift
-- i.e. mouse_release should never look at shift state
Editor.old_cursor1 = Editor.cursor
Editor.old_selection1 = Editor.selection1
Editor.mousepress_shift = shift_down()
Editor.selection1 = deepcopy(loc)
end
function edit.mouse_release(Editor, mx,my, mouse_button)
@ -280,18 +214,8 @@ function edit.mouse_release(Editor, mx,my, mouse_button)
Editor.mouse_down = nil
local loc = edit.to_loc(Editor, mx,my)
if loc.mode == 'text' then
Editor.cursor = loc
edit.clean_up_mouse_press(Editor)
else
Editor.selection1 = {}
Drawing.mouse_release(Editor, mx,my, mouse_button)
schedule_save(Editor)
if Editor.drawing_before then
record_undo_event(Editor, {before=Editor.drawing_before, after=snapshot(Editor, Editor.cursor.line)})
Editor.drawing_before = nil
end
end
Editor.cursor = loc
edit.clean_up_mouse_press(Editor)
end
function edit.clean_up_mouse_press(Editor)
@ -327,12 +251,6 @@ function edit.text_input(Editor, t)
if Editor.search_term then
Editor.search_term = Editor.search_term..t
Text.search_next(Editor)
elseif Editor.cursor.mode == 'drawing' and Editor.current_drawing_mode == 'name' then
local before = snapshot(Editor, Editor.cursor.line)
local drawing = Editor.lines[Editor.cursor.line]
local p = drawing.points[drawing.pending.target_point]
p.name = p.name..t
record_undo_event(Editor, {before=before, after=snapshot(Editor, Editor.cursor.line)})
else
-- why is this here?
Text.text_input(Editor, t)
@ -439,42 +357,6 @@ function edit.keychord_press(Editor, chord, key)
maybe_snap_cursor_to_top_of_screen(Editor)
record_undo_event(Editor, {before=before, after=snapshot(Editor, before_line, Editor.cursor.line)})
schedule_save(Editor)
-- dispatch to drawing or text
elseif love.mouse.isDown(1) or chord:sub(1,2) == 'C-' then
if Editor.cursor.mode == 'drawing' then
local before = snapshot(Editor, Editor.cursor.line)
Drawing.keychord_press(Editor, chord)
record_undo_event(Editor, {before=before, after=snapshot(Editor, Editor.cursor.line)})
schedule_save(Editor)
end
elseif chord == 'escape' and not love.mouse.isDown(1) then
for _,line in ipairs(Editor.lines) do
if line.mode == 'drawing' then
line.show_help = false
end
end
elseif Editor.cursor.mode == 'drawing' and Editor.current_drawing_mode == 'name' then
if chord == 'return' then
Editor.current_drawing_mode = Editor.previous_drawing_mode
Editor.previous_drawing_mode = nil
else
local before = snapshot(Editor, Editor.cursor.line)
local drawing = Editor.lines[Editor.cursor.line]
local p = drawing.points[drawing.pending.target_point]
if chord == 'escape' then
p.name = nil
record_undo_event(Editor, {before=before, after=snapshot(Editor, Editor.cursor.line)})
elseif chord == 'backspace' then
local len = utf8.len(p.name)
if len > 0 then
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(Editor, {before=before, after=snapshot(Editor, Editor.cursor.line)})
end
end
end
schedule_save(Editor)
else
Text.keychord_press(Editor, chord)
end

133
file.lua
View File

@ -24,11 +24,7 @@ function load_from_file(infile)
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
table.insert(result, {mode='text', data=line})
end
end
if #result == 0 then
@ -44,99 +40,17 @@ function save_to_disk(Editor)
error('failed to write to "'..Editor.filename..'"')
end
for _,line in ipairs(Editor.lines) do
if line.mode == 'drawing' then
store_drawing(outfile, line)
else
outfile:write(line.data)
outfile:write('\n')
end
outfile:write(line.data)
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, '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
if line == '```lines' then -- inflexible with whitespace since these files are always autogenerated
i, drawing = load_drawing_from_array(next_line, a, i)
table.insert(result, drawing)
else
table.insert(result, {mode='text', data=line})
end
for _,line in ipairs(a) do
table.insert(result, {mode='text', data=line})
end
if #result == 0 then
table.insert(result, {mode='text', data=''})
@ -144,43 +58,6 @@ function load_array(a)
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')
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 open_for_reading(filename)
local result = nativefs.newFile(filename)
local ok, err = result:open('r')

171
geom.lua
View File

@ -1,171 +0,0 @@
geom = {}
function geom.in_rect(x,y, top,left,width,height)
return x >= left and x <= left+width and y >= top and y <= top+height
end
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