diff options
-rwxr-xr-x | check.sh | 7 | ||||
-rw-r--r-- | hsluv.lua | 8 | ||||
-rw-r--r-- | main.lua | 273 |
3 files changed, 225 insertions, 63 deletions
diff --git a/check.sh b/check.sh new file mode 100755 index 0000000..0e28904 --- /dev/null +++ b/check.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +luacheck . \ + --std=love+luajit --no-unused --ignore 611 \ + --max-line-length 70 \ + -q --codes \ + --exclude-files '**/dkjson.lua' --exclude-files '*/common/*' @@ -20,16 +20,16 @@ WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ]] -hsluv = {} +local hsluv = {} -hexChars = "0123456789abcdef" +local hexChars = "0123456789abcdef" -distance_line_from_origin = function(line) +local distance_line_from_origin = function(line) return math.abs(line.intercept) / math.sqrt((line.slope ^ 2) + 1) end -length_of_ray_until_intersect = function(theta, line) +local length_of_ray_until_intersect = function(theta, line) return line.intercept / (math.sin(theta) - line.slope * math.cos(theta)) end @@ -6,18 +6,45 @@ local tau = 2*math.pi local ZOOM = 200 local G = love.graphics +local function mouse_pos() + -- in world space + return G.inverseTransformPoint(love.mouse.getPosition()) +end + +local function hovered(set) + -- returns which of the things in set is hovered over + -- or nil if no such thing + local mx,my = mouse_pos() + for s in pairs(set) do + if s:contains(mx,my) then + return s + end + end + return nil +end + local function write_at(text,x,y) + local tx,ty = G.transformPoint(x,y) G.push() - G.translate(x,y) - G.scale(1/ZOOM) - G.print(text,0,0) + G.origin() + G.print(text,tx,ty) G.pop() end +local function phi_color(n) + local phi = (1+math.sqrt(5))/2 + local h = (360*phi*n)%360 + return hsluv.hsluv_to_rgb({h, 80, 60}) +end + local Port = class() Port.R = 0.1 function Port.make(cls, x,y, num) - return setmetatable({x=x,y=y,n=num,wires={}},cls) + return setmetatable({ + x=x, y=y, n=num, + -- map of + conns={}, + },cls) end function Port.draw(self, istate) local c = {0,0,0} @@ -34,59 +61,205 @@ function Port.contains(self, px,py) local d = math.sqrt((self.x - px)^2 + (self.y - py)^2) return d <= self.R end +function Port.__tostring(self) + return 'p#'..self.n +end -local ports = {} -local n = 10 -for i = 1,n do - local theta = tau * i/n - table.insert(ports, Port:make(math.cos(theta), math.sin(theta), i)) +local Wire = class() +function Wire.make(cls, p1,p2, color) + return setmetatable({ + color = color, + p1 = p1, p2 = p2, + curve = catenary.catenary(p1.x,p1.y,p2.x,p2.y), + }, cls) +end +function Wire.draw(self, istate) + G.setColor(self.color) + G.line(self.curve) end -ports[3].wires = {ports[6]} -ports[5].wires = {ports[7],ports[2]} -local function wire_color(n) - local phi = (1+math.sqrt(5))/2 - local h = (360*phi*n)%360 - return hsluv.hsluv_to_rgb({h, 80, 60}) +local EditHandle = class() +EditHandle.R = 0.04 +function EditHandle.draw(self) + G.setColor(self.color) + G.circle('fill',self.x,self.y,self.R) +end +function EditHandle.contains(self, px,py) + local d = math.sqrt((self.x - px)^2 + (self.y - py)^2) + return d <= self.R +end +function EditHandle.remove_wire(self) + self.pg:remove_conn(self.here,self.there) +end + + +local PortGraph = class() +function PortGraph.make(cls) + return setmetatable({ + -- set of ports, set of wires + ports={}, wires={}, + state='normal', selected=nil, + wire_n = 1, + },cls) +end +function PortGraph.add(self,...) + local p = Port:make(...) + self.ports[p] = true +end +function PortGraph.istate_of_port(self,port) + if self.state == 'joining' and self.selected == port then + return 'selected' + elseif port:contains(mouse_pos()) then + return 'hover' + else + return 'normal' + end +end +function PortGraph.istate_of_wire(self, wire) + return 'normal' end +function PortGraph.draw(self) + for p in pairs(self.ports) do + local istate = self:istate_of_port(p) + p:draw(istate) + end + for w in pairs(self.wires) do + local istate = self:istate_of_wire(w) + w:draw(istate) + end -local state = 'normal' -local selected = nil + if self.state == 'joining' then + local p = self.selected + G.setColor(phi_color(self.wire_n)) + local mx,my = mouse_pos() + G.line(catenary.catenary(p.x,p.y,mx,my)) + end -local function hovering_port(mx,my) - for i,p in ipairs(ports) do - if p:contains(mx,my) then - return p + if self.state == 'editing' then + for h in pairs(self.handles) do + h:draw() end end - return nil end -function love.mousepressed(x,y,b) - if b == 2 then - state = 'normal' - selected = nil - return +function PortGraph.make_edit_handles(self) + local p = self.selected + local n = 0 + for _ in pairs(p.conns) do n = n + 1 end + local hs = {} + local D = 0.2 + + local i = 0 + for q,w in pairs(p.conns) do + local theta = tau * i/n + i = i + 1 + local h = setmetatable({ + x = D * math.cos(theta) + p.x, + y = D * math.sin(theta) + p.y, + color = w.color, + here = p, + there = q, + w = w, + pg = self, + },EditHandle) + hs[h] = true end - local mx,my = G.inverseTransformPoint(x,y) - local p = hovering_port(mx,my) - if p then - if state == 'normal' then - state = 'joining' - selected = p.n - elseif state == 'joining' then - table.insert(ports[selected].wires, p) - state = 'normal' - selected = nil + + return hs +end + +function PortGraph.click(self,button) + if self.state == 'normal' then + local p = hovered(self.ports) + if p then + if button == 1 then + self.state = 'joining' + self.selected = p + elseif button == 2 then + self.state = 'editing' + self.selected = p + self.handles = self:make_edit_handles() + end + end + elseif self.state == 'joining' then + if button == 2 then + self.state = 'normal' + self.selected = nil + elseif button == 1 then + local p = hovered(self.ports) + if p then + self:add_conn(self.selected,p) + self.state = 'normal' + self.selected = nil + end + end + elseif self.state == 'editing' then + -- rmb on other port -> edit on that port instead + -- rmb on wire blob -> delete that wire, stay in edit mode + -- lmb on wire blob -> delete that wire, go to joining mode on connected port + -- anything anywhere else -> back to normal mode + local p = hovered(self.ports) + local h = hovered(self.handles) + if button == 2 and p then + self.selected = p + self.handles = self:make_edit_handles() + elseif button == 2 and h then + h:remove_wire() + self.handles = self:make_edit_handles() + elseif button == 1 and h then + self.selected = h.there + h:remove_wire() + self.handles = nil + self.state = 'joining' + else + self.state = 'normal' + self.selected = nil + self.handles = nil end + + end +end +function PortGraph.connection(self, p1,p2) + -- returns the wire connecting p1 and p2, if it exists (truthy) + -- or false otherwise + local w1 = p1.conns[p2] + local w2 = p2.conns[p1] + -- better safe than sorry + assert(w1==w2, "wire bidirectionality inconsistency") + if w1 then + assert((w1.p1 == p1 and w1.p2 == p2) + or (w1.p1 == p2 and w1.p2 == p1), + "wire backlink inconsistency") end + return w1 end +function PortGraph.add_conn(self, p1,p2) + assert(not self:connection(p1,p2), + string.format("%s and %s already connected!", p1,p2)) + local wire = Wire:make(p1, p2, phi_color(self.wire_n)) + self.wires[wire] = true + p1.conns[p2] = wire + p2.conns[p1] = wire + self.wire_n = self.wire_n + 1 +end +function PortGraph.remove_conn(self,p1,p2) + local wire = self:connection(p1,p2) + assert(wire,string.format("%s and %s not connected!", p1,p2)) + self.wires[wire] = nil + p1.conns[p2] = nil + p2.conns[p1] = nil +end +local pg = PortGraph:make() +local n = 10 +for i = 1,n do + local theta = tau * i/n + pg:add(math.cos(theta), math.sin(theta), i) +end -local L = 2 -function love.wheelmoved(x,y) - L = L + (y/15) +function love.mousepressed(x,y,b) + pg:click(b) end function love.draw() @@ -94,29 +267,11 @@ function love.draw() G.clear(1,1,1) G.setColor(0,0,0) G.origin() - G.print(L,10,10) G.setLineWidth(0.01) G.translate(W/2,H/2) G.scale(ZOOM) - local mx,my = G.inverseTransformPoint(love.mouse.getPosition()) - local wn = 0 - for i,p in ipairs(ports) do - local istate = 'normal' - if p:contains(mx,my) then istate = 'hover' end - if state ~= 'normal' and selected == i then istate = 'selected' end - p:draw(istate) - for _,q in ipairs(p.wires) do - G.setColor(wire_color(wn)) - wn = wn + 1 - G.line(catenary.catenary(p.x,p.y,q.x,q.y)) - end - end + pg:draw() - if state == 'joining' then - G.setColor(wire_color(wn)) - local p = ports[selected] - G.line(catenary.catenary(p.x,p.y,mx,my)) - end end |