local class = require 'class' local hsluv = require 'hsluv' local catenary = require 'catenary' local util = require 'util' local texts = require 'texts' local tau = 2*math.pi local ZOOM = 200 local G = love.graphics local function hovered(set) -- returns which of the things in set is hovered over -- or nil if no such thing local mx,my = util.mouse_pos() for s in pairs(set) do if s:contains(mx,my) then return s end end return nil end local Port = class() Port.R = 0.1 function Port.make(cls, x,y, num) return setmetatable({ x=x, y=y, n=num, -- map of conns={}, },cls) end function Port.draw(self, istate) local c = {0,0,0} if istate == 'hover' then c[1] = 1 elseif istate == 'selected' then c[2] = 1 elseif istate == 'editing' then c[3] = 1 end love.graphics.setColor(c) G.circle('line',self.x,self.y,self.R) util.write_at(self.n, self.x, self.y) end 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 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) if istate == 'normal' then G.setLineWidth(0.01) else G.setLineWidth(0.02) end G.setColor(self.color) G.line(self.curve) end local EditHandle = class() EditHandle.R = 0.04 EditHandle.D = 0.2 function EditHandle.make(cls, theta,p,q,w,pg) local self = setmetatable({ x = cls.D * math.cos(theta) + p.x, y = cls.D * math.sin(theta) + p.y, color = w.color, here = p, there = q, pg = pg, },cls) self.curve = catenary.catenary( self.x,self.y, self.there.x,self.there.y) return self end function EditHandle.draw(self) G.setColor(self.color) G.circle('fill',self.x,self.y,self.R) G.line(self.curve) 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,emph) if self.state == 'joining' and self.selected == port then return 'selected' elseif self.state == 'editing' and self.selected == port then return 'editing' elseif emph[port] then return 'hover' else return 'normal' end end function PortGraph.istate_of_wire(self, wire, emph) if emph[wire] then return 'hover' else return 'normal' end end function PortGraph.draw(self) local hp = hovered(self.ports) local emph_ports = {} local emph_wires = {} if hp then emph_ports, emph_wires = self:trans_conns(hp) end for p in pairs(self.ports) do local istate = self:istate_of_port(p, emph_ports) p:draw(istate) end for w in pairs(self.wires) do if self.state == 'editing' then if self.selected == w.p1 or self.selected == w.p2 then goto next end end local istate = self:istate_of_wire(w, emph_wires) w:draw(istate) ::next:: end if self.state == 'joining' then local p = self.selected G.setColor(util.phi_color(self.wire_n)) local mx,my = util.mouse_pos() G.line(catenary.catenary(p.x,p.y,mx,my)) end if self.state == 'editing' then for h in pairs(self.handles) do h:draw() end end end function PortGraph.trans_conns(self, port) local seen = {} local queue = {[port]=true} local wires = {} while next(queue) do local p = next(queue) queue[p] = nil seen[p] = true for q,w in pairs(p.conns) do if not seen[q] then queue[q] = true wires[w] = true end end end return seen, wires end 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 = EditHandle:make(theta,p,q,w,self) hs[h] = true end 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.selected = p self.handles = self:make_edit_handles() self.state = 'editing' 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, util.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 i = 1 -- local S = math.sqrt(3) -- for q = -3,2 do -- for r = -3,3 do -- local x = q*S + r*S/2 -- local y = r*3/2 -- pg:add(x/4,y/4,i) -- i=i+1 -- end -- end local n = 10 for i = 1,n do local theta = tau * i/n pg:add(math.cos(theta), math.sin(theta), i) end function love.mousepressed(x,y,b) local ok, err = pcall(pg.click, pg, b) if not ok then texts.add(err,util.mouse_pos()) end end function love.draw() local W,H = G.getDimensions() G.clear(1,1,1) G.setColor(0,0,0) G.origin() G.setLineWidth(0.01) G.translate(W/2,H/2) G.scale(ZOOM) pg:draw() texts.draw() end function love.update(dt) texts.update(dt) end