diff --git a/lib/ocelot-brain b/lib/ocelot-brain index 69c4076..2ea8158 160000 --- a/lib/ocelot-brain +++ b/lib/ocelot-brain @@ -1 +1 @@ -Subproject commit 69c4076ae6dd199c403bbfb564c5e54d5065fbcc +Subproject commit 2ea81581884e6f766dc8477af54eb645ab63a5a7 diff --git a/sprites/nodes/Relay.png b/sprites/nodes/Relay.png new file mode 100644 index 0000000..0afdbe3 Binary files /dev/null and b/sprites/nodes/Relay.png differ diff --git a/src/main/resources/ocelot/desktop/spritesheet.png b/src/main/resources/ocelot/desktop/spritesheet.png index f023214..4628902 100644 Binary files a/src/main/resources/ocelot/desktop/spritesheet.png and b/src/main/resources/ocelot/desktop/spritesheet.png differ diff --git a/src/main/resources/ocelot/desktop/spritesheet.txt b/src/main/resources/ocelot/desktop/spritesheet.txt index db3ea9d..b1577b2 100644 --- a/src/main/resources/ocelot/desktop/spritesheet.txt +++ b/src/main/resources/ocelot/desktop/spritesheet.txt @@ -1,6 +1,6 @@ BackgroundPattern 0 0 304 304 -Empty 337 197 1 1 -ShadowBorder 305 197 1 24 +Empty 337 204 1 1 +ShadowBorder 305 204 1 24 ShadowCorner 424 0 24 24 buttons/PowerOff 441 25 18 18 buttons/PowerOn 460 25 18 18 @@ -78,18 +78,19 @@ nodes/ComputerActivityOverlay 356 180 16 16 nodes/ComputerErrorOverlay 373 180 16 16 nodes/ComputerOnOverlay 390 180 16 16 nodes/NewNode 407 180 16 16 -nodes/Screen 424 180 16 16 -nodes/ScreenOnOverlay 441 180 16 16 -screen/BorderB 310 197 2 8 -screen/BorderT 307 197 2 10 -screen/CornerBL 476 180 8 8 -screen/CornerBR 485 180 8 8 -screen/CornerTL 458 180 8 10 -screen/CornerTR 467 180 8 10 -window/BorderDark 333 197 1 4 -window/BorderLight 335 197 1 4 -window/CloseButton 494 180 7 6 -window/CornerBL 313 197 4 4 -window/CornerBR 318 197 4 4 -window/CornerTL 323 197 4 4 -window/CornerTR 328 197 4 4 +nodes/Relay 424 180 16 16 +nodes/Screen 441 180 16 16 +nodes/ScreenOnOverlay 458 180 16 16 +screen/BorderB 310 204 2 8 +screen/BorderT 307 204 2 10 +screen/CornerBL 493 180 8 8 +screen/CornerBR 502 180 8 8 +screen/CornerTL 475 180 8 10 +screen/CornerTR 484 180 8 10 +window/BorderDark 333 204 1 4 +window/BorderLight 335 204 1 4 +window/CloseButton 305 197 7 6 +window/CornerBL 313 204 4 4 +window/CornerBR 318 204 4 4 +window/CornerTL 323 204 4 4 +window/CornerTR 328 204 4 4 diff --git a/src/main/scala/ocelot/desktop/geometry/Rect2D.scala b/src/main/scala/ocelot/desktop/geometry/Rect2D.scala index 499f08d..6798496 100644 --- a/src/main/scala/ocelot/desktop/geometry/Rect2D.scala +++ b/src/main/scala/ocelot/desktop/geometry/Rect2D.scala @@ -88,4 +88,8 @@ case class Rect2D(x: Float, y: Float, w: Float, h: Float) { def manhattanDistanceTo(that: Rect2D): Float = { ((center - that.center).abs - (extent + that.extent)).max(Vector2D(0, 0)).manhattanLength } + + def mapX(f: Float => Float): Rect2D = copy(x = f(x)) + + def mapY(f: Float => Float): Rect2D = copy(y = f(y)) } diff --git a/src/main/scala/ocelot/desktop/graphics/Graphics.scala b/src/main/scala/ocelot/desktop/graphics/Graphics.scala index 8e433a6..dbb80d3 100644 --- a/src/main/scala/ocelot/desktop/graphics/Graphics.scala +++ b/src/main/scala/ocelot/desktop/graphics/Graphics.scala @@ -1,7 +1,7 @@ package ocelot.desktop.graphics import ocelot.desktop.color.{Color, RGBAColorNorm} -import ocelot.desktop.geometry.{Size2D, Transform2D, Vector2D} +import ocelot.desktop.geometry.{Rect2D, Size2D, Transform2D, Vector2D} import ocelot.desktop.graphics.mesh.{Mesh, MeshInstance} import ocelot.desktop.graphics.render.InstanceRenderer import ocelot.desktop.util.{Font, Logging, Spritesheet} @@ -227,6 +227,10 @@ class Graphics extends Logging { _rect(x, y, width, height) } + def rect(r: Rect2D, color: Color): Unit = { + rect(r.x, r.y, r.w, r.h, color) + } + def rect(x: Float, y: Float, width: Float, height: Float, color: Color = RGBAColorNorm(1f, 1f, 1f)): Unit = { sprite("Empty", x, y, width, height, color) } diff --git a/src/main/scala/ocelot/desktop/node/Node.scala b/src/main/scala/ocelot/desktop/node/Node.scala index 96adcb2..4a5d781 100644 --- a/src/main/scala/ocelot/desktop/node/Node.scala +++ b/src/main/scala/ocelot/desktop/node/Node.scala @@ -1,18 +1,21 @@ package ocelot.desktop.node import ocelot.desktop.color.{Color, RGBAColor} -import ocelot.desktop.geometry.{Size2D, Vector2D} +import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} import ocelot.desktop.graphics.Graphics -import ocelot.desktop.ui.event.{ClickEvent, DragEvent, HoverEvent, MouseEvent} import ocelot.desktop.ui.event.handlers.{ClickHandler, DragHandler, HoverHandler} import ocelot.desktop.ui.event.sources.KeyEvents -import ocelot.desktop.ui.widget.{Widget, WorkspaceView} +import ocelot.desktop.ui.event.{ClickEvent, DragEvent, HoverEvent, MouseEvent} import ocelot.desktop.ui.widget.window.Window +import ocelot.desktop.ui.widget.{Widget, WorkspaceView} import ocelot.desktop.util.DrawUtils import ocelot.desktop.util.animation.ColorAnimation import org.apache.commons.lang3.StringUtils import org.lwjgl.input.Keyboard import totoro.ocelot.brain.entity.traits.Environment +import totoro.ocelot.brain.network + +import scala.collection.mutable.ArrayBuffer trait Node extends Widget with DragHandler with ClickHandler with HoverHandler { var workspaceView: WorkspaceView = _ @@ -25,6 +28,8 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler { private var isMoving = false private var grabPoint: Vector2D = Vector2D(0, 0) + protected val _connections: ArrayBuffer[(NodePort, Node, NodePort)] = ArrayBuffer[(NodePort, Node, NodePort)]() + size = minimumSize override def receiveMouseEvents = true @@ -37,6 +42,9 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler { if (!KeyEvents.isDown(Keyboard.KEY_LSHIFT)) { grabPoint = pos - position startMoving() + } else { + val port = portsBounds.flatMap(p => p._2.map(a => (p._1, a))).minBy(p => (p._2.center - pos).lengthSquared)._1 + workspaceView.newConnection = Some((this, port, pos)) } case DragEvent(DragEvent.State.Drag, MouseEvent.Button.Left, pos) => @@ -50,9 +58,8 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler { workspaceView.resolveCollision(this) if (workspaceView.collides(this) || (position - desiredPos).lengthSquared > 50 * 50) position = oldPos - // UiHandler.cursor = Cursor.Hand } else { - workspaceView.newConnection = Some((this, pos)) + workspaceView.newConnection = Some((this, workspaceView.newConnection.get._2, pos)) } case DragEvent(DragEvent.State.Stop, MouseEvent.Button.Left, _) => @@ -78,6 +85,69 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler { def iconColor: Color = RGBAColor(255, 255, 255) + def ports: Array[NodePort] = Array(NodePort()) + + def getNodeByPort(port: NodePort): network.Node = environment.node + + def connections: Iterator[(NodePort, Node, NodePort)] = _connections.iterator + + def connect(portA: NodePort, node: Node, portB: NodePort): Unit = { + this._connections.append((portA, node, portB)) + node._connections.append((portB, this, portA)) + this.onConnectionAdded(portA, node, portB) + node.onConnectionAdded(portB, this, portA) + } + + def disconnect(portA: NodePort, node: Node, portB: NodePort): Unit = { + this._connections -= ((portA, node, portB)) + node._connections -= ((portB, this, portA)) + this.onConnectionRemoved(portA, node, portB) + node.onConnectionRemoved(portB, this, portA) + } + + def isConnected(portA: NodePort, node: Node, portB: NodePort): Boolean = { + _connections.contains((portA, node, portB)) + } + + def onConnectionAdded(portA: NodePort, node: Node, portB: NodePort): Unit = { + getNodeByPort(portA).connect(node.getNodeByPort(portB)) + } + + def onConnectionRemoved(portA: NodePort, node: Node, portB: NodePort): Unit = { + getNodeByPort(portA).disconnect(node.getNodeByPort(portB)) + } + + def portsBounds: Iterator[(NodePort, Array[Rect2D])] = { + val length = -4 + val thickness = 4 + val stride = thickness + 4 + val hsize = Size2D(length, thickness) + val vsize = Size2D(thickness, length) + + val centers = bounds.edgeCenters + val ports = this.ports + val numPorts = ports.length + + ports.sorted.iterator.zipWithIndex.map { case (port, portIdx) => + val top = Rect2D(centers(0) + Vector2D(-thickness / 2, -length), vsize) + val right = Rect2D(centers(1) + Vector2D(0, -thickness / 2), hsize) + val bottom = Rect2D(centers(2) + Vector2D(-thickness / 2, 0), vsize) + val left = Rect2D(centers(3) + Vector2D(-length, -thickness / 2), hsize) + val centersBounds = Array[Rect2D](top, right, bottom, left) + + val portBounds = (0 until 4).map(side => { + val offset = thickness - numPorts * stride / 2 + portIdx * stride + val rect = centersBounds(side) + side match { + case 0 | 2 => rect.mapX(_ + offset) + case 1 | 3 => rect.mapY(_ + offset) + } + }) + + (port, portBounds.toArray) + } + } + // noinspection VarCouldBeVal var label: Option[String] = None @@ -98,7 +168,7 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler { highlight.goto(NoHighlight) } - protected def getShortLabel: String = + def getShortLabel: String = StringUtils.substring(label.orElse(Option(environment.node.address)).getOrElse("unknown"), 0, 8) override def draw(g: Graphics): Unit = { @@ -115,5 +185,13 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler { g.setNormalFont() } + def drawPorts(g: Graphics): Unit = { + for ((port, rects) <- portsBounds) { + val color = port.getColor + for (rect <- rects) + g.rect(rect, color) + } + } + lazy val window: Option[Window] = None } diff --git a/src/main/scala/ocelot/desktop/node/NodePort.scala b/src/main/scala/ocelot/desktop/node/NodePort.scala new file mode 100644 index 0000000..5e49671 --- /dev/null +++ b/src/main/scala/ocelot/desktop/node/NodePort.scala @@ -0,0 +1,18 @@ +package ocelot.desktop.node + +import ocelot.desktop.color.{Color, IntColor, RGBAColor} +import totoro.ocelot.brain.util.Direction + +case class NodePort(direction: Option[Direction.Value] = None) extends Ordered[NodePort] { + def getColor: Color = direction match { + case Some(Direction.Down) => IntColor(0x8382d8) + case Some(Direction.Up) => IntColor(0x75bdc1) + case Some(Direction.North) => IntColor(0xc8ca5f) + case Some(Direction.East) => IntColor(0xdb7d75) + case Some(Direction.West) => IntColor(0x7ec95f) + case Some(Direction.South) => IntColor(0x990da3) + case None => IntColor(0x9b9b9b) + } + + override def compare(that: NodePort): Int = this.direction.compare(that.direction) +} diff --git a/src/main/scala/ocelot/desktop/node/NodeRegistry.scala b/src/main/scala/ocelot/desktop/node/NodeRegistry.scala index f64f010..a82e557 100644 --- a/src/main/scala/ocelot/desktop/node/NodeRegistry.scala +++ b/src/main/scala/ocelot/desktop/node/NodeRegistry.scala @@ -1,6 +1,7 @@ package ocelot.desktop.node -import totoro.ocelot.brain.entity.{Case, Screen} +import ocelot.desktop.node.nodes.{ComputerNode, RelayNode, ScreenNode} +import totoro.ocelot.brain.entity.{Case, Relay, Screen} import scala.collection.mutable @@ -11,6 +12,10 @@ object NodeRegistry { types += t } + register(NodeType("Relay", "nodes/Relay", -1, () => { + new RelayNode(new Relay) + })) + for (tier <- 0 to 2) { register(NodeType("Screen" + tier, "nodes/Screen", tier, () => { new ScreenNode(new Screen(tier)) diff --git a/src/main/scala/ocelot/desktop/node/ComputerNode.scala b/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala similarity index 92% rename from src/main/scala/ocelot/desktop/node/ComputerNode.scala rename to src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala index 949c8cb..9e8e541 100644 --- a/src/main/scala/ocelot/desktop/node/ComputerNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/ComputerNode.scala @@ -1,11 +1,12 @@ -package ocelot.desktop.node +package ocelot.desktop.node.nodes import ocelot.desktop.OcelotDesktop import ocelot.desktop.color.Color import ocelot.desktop.graphics.Graphics +import ocelot.desktop.node.Node import ocelot.desktop.util.TierColor import totoro.ocelot.brain.entity.traits.Computer -import totoro.ocelot.brain.entity.{CPU, Case, EEPROM, GraphicsCard, HDDManaged, InternetCard, Memory} +import totoro.ocelot.brain.entity.{CPU, Case, EEPROM, GraphicsCard, HDDManaged, InternetCard, Memory, NetworkCard} import totoro.ocelot.brain.loot.Loot import totoro.ocelot.brain.util.Tier @@ -17,6 +18,7 @@ class ComputerNode(val computer: Case) extends Node { computer.add(new InternetCard) computer.add(new Memory(Tier.Six)) computer.add(new Memory(Tier.Six)) + computer.add(new NetworkCard) computer.add(new HDDManaged(java.util.UUID.randomUUID().toString, Tier.Three, "hello")) private val eeprom = Loot.OpenOsEEPROM.create() eeprom.asInstanceOf[EEPROM].readonly = false diff --git a/src/main/scala/ocelot/desktop/node/ComputerWindow.scala b/src/main/scala/ocelot/desktop/node/nodes/ComputerWindow.scala similarity index 97% rename from src/main/scala/ocelot/desktop/node/ComputerWindow.scala rename to src/main/scala/ocelot/desktop/node/nodes/ComputerWindow.scala index f7313ee..7e211f7 100644 --- a/src/main/scala/ocelot/desktop/node/ComputerWindow.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/ComputerWindow.scala @@ -1,4 +1,4 @@ -package ocelot.desktop.node +package ocelot.desktop.node.nodes import ocelot.desktop.geometry.Size2D import ocelot.desktop.graphics.Graphics diff --git a/src/main/scala/ocelot/desktop/node/nodes/RelayNode.scala b/src/main/scala/ocelot/desktop/node/nodes/RelayNode.scala new file mode 100644 index 0000000..80246db --- /dev/null +++ b/src/main/scala/ocelot/desktop/node/nodes/RelayNode.scala @@ -0,0 +1,27 @@ +package ocelot.desktop.node.nodes + +import ocelot.desktop.OcelotDesktop +import ocelot.desktop.node.{Node, NodePort} +import totoro.ocelot.brain.entity.Relay +import totoro.ocelot.brain.network +import totoro.ocelot.brain.util.Direction + +class RelayNode(relay: Relay) extends Node { + OcelotDesktop.workspace.add(relay) + + override def environment: Relay = relay + + override val icon: String = "nodes/Relay" + + override def ports: Array[NodePort] = Array( + NodePort(Some(Direction.North)), + NodePort(Some(Direction.South)), + NodePort(Some(Direction.East)), + NodePort(Some(Direction.West)), + NodePort(Some(Direction.Up)), + NodePort(Some(Direction.Down))) + + override def getShortLabel: String = "" + + override def getNodeByPort(port: NodePort): network.Node = relay.sidedNode(port.direction.get) +} diff --git a/src/main/scala/ocelot/desktop/node/ScreenNode.scala b/src/main/scala/ocelot/desktop/node/nodes/ScreenNode.scala similarity index 93% rename from src/main/scala/ocelot/desktop/node/ScreenNode.scala rename to src/main/scala/ocelot/desktop/node/nodes/ScreenNode.scala index 96dd489..1f6f00f 100644 --- a/src/main/scala/ocelot/desktop/node/ScreenNode.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/ScreenNode.scala @@ -1,8 +1,9 @@ -package ocelot.desktop.node +package ocelot.desktop.node.nodes import ocelot.desktop.OcelotDesktop import ocelot.desktop.color.Color import ocelot.desktop.graphics.Graphics +import ocelot.desktop.node.Node import ocelot.desktop.util.TierColor import totoro.ocelot.brain.entity.traits.Environment import totoro.ocelot.brain.entity.{Keyboard, Screen} diff --git a/src/main/scala/ocelot/desktop/node/ScreenWindow.scala b/src/main/scala/ocelot/desktop/node/nodes/ScreenWindow.scala similarity index 99% rename from src/main/scala/ocelot/desktop/node/ScreenWindow.scala rename to src/main/scala/ocelot/desktop/node/nodes/ScreenWindow.scala index 6379deb..81f822f 100644 --- a/src/main/scala/ocelot/desktop/node/ScreenWindow.scala +++ b/src/main/scala/ocelot/desktop/node/nodes/ScreenWindow.scala @@ -1,4 +1,4 @@ -package ocelot.desktop.node +package ocelot.desktop.node.nodes import ocelot.desktop.color.{IntColor, RGBAColor, RGBAColorNorm} import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} diff --git a/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala b/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala index 9ae8848..9d262b0 100644 --- a/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala +++ b/src/main/scala/ocelot/desktop/ui/widget/WorkspaceView.scala @@ -3,7 +3,8 @@ package ocelot.desktop.ui.widget import ocelot.desktop.color.{Color, RGBAColor, RGBAColorNorm} import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} import ocelot.desktop.graphics.Graphics -import ocelot.desktop.node.{ComputerNode, Node, ScreenNode} +import ocelot.desktop.node.nodes.{ComputerNode, ScreenNode} +import ocelot.desktop.node.{Node, NodePort} import ocelot.desktop.ui.event._ import ocelot.desktop.ui.event.handlers.{ClickHandler, DragHandler, HoverHandler} import ocelot.desktop.ui.layout.{CopyLayout, Layout} @@ -23,10 +24,11 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover val nodeSelector = new NodeSelector var cameraOffset: Vector2D = Vector2D(0, 0) - var newConnection: Option[(Node, Vector2D)] = None + var newConnection: Option[(Node, NodePort, Vector2D)] = None private var newNodePos = Vector2D(0, 0) private val gridAlpha = new ValueAnimation(0, 1f) + private val portsAlpha = new ValueAnimation(0, 7f) override protected val layout: Layout = new CopyLayout(this) @@ -45,21 +47,22 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover } def buildNewConnection(): Unit = { - val (start, endPoint) = newConnection.get - val end = nodes.find(_.bounds.contains(endPoint)) - if (end.isDefined && end.get != start) { - if (end.get.environment.node.neighbors.exists(_ == start.environment.node)) - end.get.environment.disconnect(start.environment) - else - end.get.environment.connect(start.environment) + val (node, port, _) = newConnection.get + newConnectionTarget match { + case Some((target, targetPort)) => + if (node.isConnected(port, target, targetPort)) + node.disconnect(port, target, targetPort) + else + node.connect(port, target, targetPort) + case None => } newConnection = None } def createDefaultWorkspace(): Unit = { - addNode(new ComputerNode(new Case(Tier.Six))) + addNode(new ComputerNode(new Case(Tier.Four))) addNode(new ScreenNode(new Screen(Tier.Two)), Vector2D(200, 100)) - nodes(0).environment.connect(nodes(1).environment) + nodes(0).connect(NodePort(), nodes(1), NodePort()) } override def receiveMouseEvents = true @@ -144,40 +147,39 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover !nodes.exists(_.bounds.collides(clearance)) } - private def findSuitableConnections(a: Rect2D, b: Rect2D, checkCollision: Boolean, - forceParallel: Boolean, clampMin: Boolean): Iterator[Array[Vector2D]] = { - var pairs = - for (aSide <- a.edgeCenters.iterator; bSide <- b.edgeCenters.iterator) - yield (aSide, bSide) + private def findSuitableConnections(a: Array[(Vector2D, Vector2D)], + b: Array[(Vector2D, Vector2D)], + checkCollision: Boolean, + forceParallel: Boolean, + clampMin: Boolean): Array[Array[Vector2D]] = { + val product = for (x <- a; y <- b) yield (x, y) + var iter = product.map { case ((aSide, aCenter), (bSide, bCenter)) => + val (aLen, bLen) = connectorLen(aSide, aCenter, bSide, bCenter, clampMin) + ((aSide, aCenter, aLen), (bSide, bCenter, bLen)) + } if (checkCollision) - pairs = pairs.filter(pair => { - val (aSide, bSide) = pair - val (aLen, bLen) = connectorLen(a.center, aSide, b.center, bSide, clampMin) - checkConnectorCollision(aSide, a.center, aLen) && - checkConnectorCollision(bSide, b.center, bLen) - }) + iter = iter.filter { case ((aSide, aCenter, aLen), (bSide, bCenter, bLen)) => + checkConnectorCollision(aSide, aCenter, aLen) && + checkConnectorCollision(bSide, bCenter, bLen) + } - var paths = pairs.map(p => { - val (aLen, bLen) = connectorLen(a.center, p._1, b.center, p._2, clampMin) - val aEnd = p._1 + (p._1 - a.center).normalizeAxisAligned * aLen - val bEnd = p._2 + (p._2 - b.center).normalizeAxisAligned * bLen - Array(p._1, aEnd, bEnd, p._2) - }).filter(DrawUtils.isValidPolyline) + var paths = iter.map { case ((aSide, aCenter, aLen), (bSide, bCenter, bLen)) => + val aEnd = aSide + (aSide - aCenter).normalizeAxisAligned * aLen + val bEnd = bSide + (bSide - bCenter).normalizeAxisAligned * bLen + Array(aSide, aEnd, bEnd, bSide) + }.filter(DrawUtils.isValidPolyline) if (forceParallel) - paths = paths.filter(p => { - val Array(aStart, aEnd, bEnd, bStart) = p - val aDir = aEnd - aStart - val bDir = bEnd - bStart - aDir.dot(bDir) != 0f - }) + paths = paths.filter { case Array(aStart, aEnd, bEnd, bStart) => + (aEnd - aStart).dot(bEnd - bStart) != 0f + } paths } - private def connectorLen(aCenter: Vector2D, aSide: Vector2D, - bCenter: Vector2D, bSide: Vector2D, + private def connectorLen(aSide: Vector2D, aCenter: Vector2D, + bSide: Vector2D, bCenter: Vector2D, clampMin: Boolean): (Float, Float) = { val aDir = aSide - aCenter val bDir = bSide - bCenter @@ -191,32 +193,111 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover (aLen.min(40f), bLen.min(40f)) } - def drawConnection(g: Graphics, a: Rect2D, b: Rect2D, - clampMin: Boolean = true, - checkCollision: Boolean = true, - forceParallel: Boolean = false, - drawBorder: Boolean = true, - thickness: Float = 4, col: Color = RGBAColor(150, 150, 150)): Unit = { - if (a.collides(b)) return + private def portDirections(node: Node, port: NodePort): Array[(Vector2D, Vector2D)] = { + val rect = node.bounds + val sides = node.portsBounds.find(_._1 == port).get._2 + .map(rects => rects.edgeCenters.maxBy(c => (c - rect.center).lengthSquared)) - val paths = findSuitableConnections(a, b, checkCollision, forceParallel, clampMin) + sides.zipWithIndex.map { case (side, i) => + val center = if (i % 2 == 0) rect.center.copy(x = side.x) else rect.center.copy(y = side.y) + (side, center) + } + } + + private def drawConnection(g: Graphics, aNode: Node, aPort: NodePort, bNode: Node, bPort: NodePort, + color: Color = RGBAColor(150, 150, 150)): Unit = { + val (aRect, bRect) = (aNode.bounds, bNode.bounds) + if (aRect.collides(bRect)) return + if (aRect.x < bRect.x) { + drawConnection(g, bNode, bPort, aNode, aPort, color) + return + } + + val paths = findSuitableConnections(portDirections(aNode, aPort), + portDirections(bNode, bPort), checkCollision = true, forceParallel = false, clampMin = true) if (paths.isEmpty) { - val canGiveUp = checkCollision || clampMin - if (canGiveUp) { - if (drawBorder) - g.line(a.center, b.center, thickness + 4, RGBAColorNorm(0.1f, 0.1f, 0.1f)) - g.line(a.center, b.center, thickness, col) - } + // give up + g.line(aRect.center, bRect.center, 8, RGBAColorNorm(0.1f, 0.1f, 0.1f)) + g.line(aRect.center, bRect.center, 4, color) } else { - val path = paths.minBy(p => { - val Array(_, aEnd, bEnd, _) = p - (aEnd - bEnd).lengthSquared - }) + val path = paths.minBy { case Array(_, aEnd, bEnd, _) => (aEnd - bEnd).lengthSquared } + DrawUtils.polyline(g, path, 8, RGBAColorNorm(0.1f, 0.1f, 0.1f)) + DrawUtils.polyline(g, path, 4, color) - if (drawBorder) - DrawUtils.polyline(g, path, thickness + 4, RGBAColorNorm(0.1f, 0.1f, 0.1f)) - DrawUtils.polyline(g, path, thickness, col) + val aDir = (path(1) - path(0)).normalizeAxisAligned * 8 + g.line(path(0), path(0) + aDir, 4, aPort.getColor) + val bDir = (path(2) - path(3)).normalizeAxisAligned * 8 + g.line(path(3), path(3) + bDir, 4, bPort.getColor) + } + } + + private def drawNewConnectionNoTarget(g: Graphics, node: Node, port: NodePort, endpoint: Vector2D): Unit = { + val color = RGBAColorNorm(0.3f, 0.5f, 0.4f) + val rect = node.bounds + + val filtered = portDirections(node, port) + .map { case (side, center) => + val (len, _) = connectorLen(side, side, endpoint, endpoint, clampMin = true) + (center, side, len) + } + .filter { case (center, side, len) => checkConnectorCollision(side, center, len) } + .map { case (center, side, len) => + val end = side + (side - center).normalizeAxisAligned * len + (side, end, (end - endpoint).lengthSquared) + } + .filter { case (side, end, _) => DrawUtils.isValidPolyline(Array(side, end, endpoint)) } + + if (filtered.isEmpty) + g.line(rect.center, endpoint, 4, color) + else { + val (side, end, _) = filtered.minBy(_._3) + DrawUtils.polyline(g, Array(side, end, endpoint), 4, color) + val dir = (end - side).normalizeAxisAligned * 8 + g.line(side, side + dir, 4, port.getColor) + } + } + + private def newConnectionTarget: Option[(Node, NodePort)] = { + val (node, _, endpoint) = newConnection.get + val validTargets = nodes.iterator.filter(n => n != node && n.bounds.inflate(20).contains(endpoint)) + + if (validTargets.nonEmpty) { + val target = validTargets.minBy(n => (n.bounds.center - endpoint).lengthSquared) + val targetPort = target.portsBounds.flatMap(p => p._2.map(a => (p._1, a))) + .minBy(p => (p._2.center - endpoint).lengthSquared)._1 + Some((target, targetPort)) + } else + None + } + + private def drawNewConnection(g: Graphics): Unit = { + val (node, port, endpoint) = newConnection.get + if (node.bounds.contains(endpoint)) return + + val color = RGBAColorNorm(0.3f, 0.5f, 0.4f) + + newConnectionTarget match { + case Some((target, targetPort)) => + drawConnection(g, node, port, target, targetPort, color = color) + case None => + drawNewConnectionNoTarget(g, node, port, endpoint) + } + } + + private def drawSelectorConnection(g: Graphics, aRect: Rect2D, bRect: Rect2D, + thickness: Float = 4, + color: Color = RGBAColor(150, 150, 150)): Unit = { + if (aRect.collides(bRect)) return + val (a, b) = if (aRect.x > bRect.x) (aRect, bRect) else (bRect, aRect) + + val paths = findSuitableConnections(a.edgeCenters.map((_, a.center)), + b.edgeCenters.map((_, b.center)), + checkCollision = false, forceParallel = true, clampMin = false) + + if (paths.nonEmpty) { + val path = paths.minBy { case Array(_, aEnd, bEnd, _) => (aEnd - bEnd).lengthSquared } + DrawUtils.polyline(g, path, thickness, color) } } @@ -263,37 +344,43 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover gridAlpha.goto(0f) } - val drawn = mutable.HashSet[(Node, Node)]() + val drawn = mutable.HashSet[(Node, NodePort, Node, NodePort)]() - for (node <- nodes) { - for (neighbour <- node.environment.node.neighbors) - for (other <- nodes.find(_.environment.node == neighbour)) { - if (!drawn.contains((node, other)) && !drawn.contains((other, node))) { - val gray = ((node.environment.node.address + other.environment.node.address).hashCode % 25 + 133).toShort - val color = RGBAColor(gray, gray, gray) - drawn.add((node, other)) - drawConnection(g, node.bounds, other.bounds, col = color) - } + for (nodeA <- nodes) { + nodeA.connections.foreach { case (portA, nodeB, portB) => + if (!drawn.contains((nodeA, portA, nodeB, portB)) && !drawn.contains((nodeB, portB, nodeA, portA))) { + val gray = ((nodeA.getShortLabel + nodeB.getShortLabel).hashCode % 25 + 133).toShort + val color = RGBAColor(gray, gray, gray) + drawn += ((nodeA, portA, nodeB, portB)) + drawConnection(g, nodeA, portA, nodeB, portB, color = color) } + } } - for ((start, endpoint) <- newConnection) { - drawConnection(g, start.bounds, Rect2D(endpoint - Vector2D(32, 32), Size2D(64, 64)), - col = RGBAColorNorm(0.3f, 0.5f, 0.4f)) + if (newConnection.isDefined) { + drawNewConnection(g) } nodes.foreach(_.draw(g)) nodes.foreach(_.drawLabel(g)) - val col = nodeSelector.ringColor + portsAlpha.update() + portsAlpha.goto(if (newConnection.isDefined) 1 else 0) + + g.save() + g.alphaMultiplier *= portsAlpha.value + nodes.foreach(_.drawPorts(g)) + g.restore() + + val color = nodeSelector.ringColor if (nodeSelector.isShown) { g.sprite("nodes/NewNode", newNodePos.x + cameraOffset.x, - newNodePos.y + cameraOffset.y, 64, 64, col) + newNodePos.y + cameraOffset.y, 64, 64, color) } if (nodeSelector.isShown) - drawConnection(g, Rect2D(newNodePos.x + cameraOffset.x, newNodePos.y + cameraOffset.y, 64, 64), - nodeSelector.bounds, col = col, checkCollision = false, drawBorder = false, forceParallel = true, clampMin = false) + drawSelectorConnection(g, Rect2D(newNodePos.x + cameraOffset.x, newNodePos.y + cameraOffset.y, 64, 64), + nodeSelector.bounds, color = color) drawChildren(g) } diff --git a/src/main/scala/ocelot/desktop/util/DrawUtils.scala b/src/main/scala/ocelot/desktop/util/DrawUtils.scala index e1d7d61..b3c3faf 100644 --- a/src/main/scala/ocelot/desktop/util/DrawUtils.scala +++ b/src/main/scala/ocelot/desktop/util/DrawUtils.scala @@ -43,7 +43,7 @@ object DrawUtils { def polyline(g: Graphics, points: Array[Vector2D], thickness: Float = 4, color: Color = RGBAColor(255, 255, 255)): Unit = { if (points.length < 2) return - if (!isValidPolyline(points)) return +// if (!isValidPolyline(points)) return var start = points(0) for ((end, i) <- points.iterator.zipWithIndex.drop(1)) { if (end != points.last) { diff --git a/src/main/scala/ocelot/desktop/util/TierColor.scala b/src/main/scala/ocelot/desktop/util/TierColor.scala index fe06316..da26eb4 100644 --- a/src/main/scala/ocelot/desktop/util/TierColor.scala +++ b/src/main/scala/ocelot/desktop/util/TierColor.scala @@ -10,5 +10,5 @@ object TierColor { val Tiers: Array[Color] = Array(Tier0, Tier1, Tier2, Tier3) - def get(tier: Int): Color = Tiers(tier.min(3)) + def get(tier: Int): Color = if (tier >= 0 && tier <= 3) Tiers(tier.min(3)) else IntColor(0xFFFFFF) }