new fork: tighter editor for just text
This commit is contained in:
parent
aad21bd9a1
commit
6fc74489c4
56
README.md
56
README.md
|
@ -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
|
||||
|
||||
|
|
724
drawing.lua
724
drawing.lua
|
@ -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
238
edit.lua
|
@ -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
133
file.lua
|
@ -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
171
geom.lua
|
@ -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
|
||||