Add relays

This commit is contained in:
LeshaInc 2020-06-04 18:55:10 +03:00
parent d3642d0800
commit c8decda297
No known key found for this signature in database
GPG Key ID: B4855290FC36DE72
17 changed files with 334 additions and 107 deletions

@ -1 +1 @@
Subproject commit 69c4076ae6dd199c403bbfb564c5e54d5065fbcc Subproject commit 2ea81581884e6f766dc8477af54eb645ab63a5a7

BIN
sprites/nodes/Relay.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 595 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -1,6 +1,6 @@
BackgroundPattern 0 0 304 304 BackgroundPattern 0 0 304 304
Empty 337 197 1 1 Empty 337 204 1 1
ShadowBorder 305 197 1 24 ShadowBorder 305 204 1 24
ShadowCorner 424 0 24 24 ShadowCorner 424 0 24 24
buttons/PowerOff 441 25 18 18 buttons/PowerOff 441 25 18 18
buttons/PowerOn 460 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/ComputerErrorOverlay 373 180 16 16
nodes/ComputerOnOverlay 390 180 16 16 nodes/ComputerOnOverlay 390 180 16 16
nodes/NewNode 407 180 16 16 nodes/NewNode 407 180 16 16
nodes/Screen 424 180 16 16 nodes/Relay 424 180 16 16
nodes/ScreenOnOverlay 441 180 16 16 nodes/Screen 441 180 16 16
screen/BorderB 310 197 2 8 nodes/ScreenOnOverlay 458 180 16 16
screen/BorderT 307 197 2 10 screen/BorderB 310 204 2 8
screen/CornerBL 476 180 8 8 screen/BorderT 307 204 2 10
screen/CornerBR 485 180 8 8 screen/CornerBL 493 180 8 8
screen/CornerTL 458 180 8 10 screen/CornerBR 502 180 8 8
screen/CornerTR 467 180 8 10 screen/CornerTL 475 180 8 10
window/BorderDark 333 197 1 4 screen/CornerTR 484 180 8 10
window/BorderLight 335 197 1 4 window/BorderDark 333 204 1 4
window/CloseButton 494 180 7 6 window/BorderLight 335 204 1 4
window/CornerBL 313 197 4 4 window/CloseButton 305 197 7 6
window/CornerBR 318 197 4 4 window/CornerBL 313 204 4 4
window/CornerTL 323 197 4 4 window/CornerBR 318 204 4 4
window/CornerTR 328 197 4 4 window/CornerTL 323 204 4 4
window/CornerTR 328 204 4 4

View File

@ -88,4 +88,8 @@ case class Rect2D(x: Float, y: Float, w: Float, h: Float) {
def manhattanDistanceTo(that: Rect2D): Float = { def manhattanDistanceTo(that: Rect2D): Float = {
((center - that.center).abs - (extent + that.extent)).max(Vector2D(0, 0)).manhattanLength ((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))
} }

View File

@ -1,7 +1,7 @@
package ocelot.desktop.graphics package ocelot.desktop.graphics
import ocelot.desktop.color.{Color, RGBAColorNorm} 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.mesh.{Mesh, MeshInstance}
import ocelot.desktop.graphics.render.InstanceRenderer import ocelot.desktop.graphics.render.InstanceRenderer
import ocelot.desktop.util.{Font, Logging, Spritesheet} import ocelot.desktop.util.{Font, Logging, Spritesheet}
@ -227,6 +227,10 @@ class Graphics extends Logging {
_rect(x, y, width, height) _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 = { def rect(x: Float, y: Float, width: Float, height: Float, color: Color = RGBAColorNorm(1f, 1f, 1f)): Unit = {
sprite("Empty", x, y, width, height, color) sprite("Empty", x, y, width, height, color)
} }

View File

@ -1,18 +1,21 @@
package ocelot.desktop.node package ocelot.desktop.node
import ocelot.desktop.color.{Color, RGBAColor} 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.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.handlers.{ClickHandler, DragHandler, HoverHandler}
import ocelot.desktop.ui.event.sources.KeyEvents 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.window.Window
import ocelot.desktop.ui.widget.{Widget, WorkspaceView}
import ocelot.desktop.util.DrawUtils import ocelot.desktop.util.DrawUtils
import ocelot.desktop.util.animation.ColorAnimation import ocelot.desktop.util.animation.ColorAnimation
import org.apache.commons.lang3.StringUtils import org.apache.commons.lang3.StringUtils
import org.lwjgl.input.Keyboard import org.lwjgl.input.Keyboard
import totoro.ocelot.brain.entity.traits.Environment 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 { trait Node extends Widget with DragHandler with ClickHandler with HoverHandler {
var workspaceView: WorkspaceView = _ var workspaceView: WorkspaceView = _
@ -25,6 +28,8 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler {
private var isMoving = false private var isMoving = false
private var grabPoint: Vector2D = Vector2D(0, 0) private var grabPoint: Vector2D = Vector2D(0, 0)
protected val _connections: ArrayBuffer[(NodePort, Node, NodePort)] = ArrayBuffer[(NodePort, Node, NodePort)]()
size = minimumSize size = minimumSize
override def receiveMouseEvents = true override def receiveMouseEvents = true
@ -37,6 +42,9 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler {
if (!KeyEvents.isDown(Keyboard.KEY_LSHIFT)) { if (!KeyEvents.isDown(Keyboard.KEY_LSHIFT)) {
grabPoint = pos - position grabPoint = pos - position
startMoving() 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) => 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) workspaceView.resolveCollision(this)
if (workspaceView.collides(this) || (position - desiredPos).lengthSquared > 50 * 50) if (workspaceView.collides(this) || (position - desiredPos).lengthSquared > 50 * 50)
position = oldPos position = oldPos
// UiHandler.cursor = Cursor.Hand
} else { } else {
workspaceView.newConnection = Some((this, pos)) workspaceView.newConnection = Some((this, workspaceView.newConnection.get._2, pos))
} }
case DragEvent(DragEvent.State.Stop, MouseEvent.Button.Left, _) => 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 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 // noinspection VarCouldBeVal
var label: Option[String] = None var label: Option[String] = None
@ -98,7 +168,7 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler {
highlight.goto(NoHighlight) highlight.goto(NoHighlight)
} }
protected def getShortLabel: String = def getShortLabel: String =
StringUtils.substring(label.orElse(Option(environment.node.address)).getOrElse("unknown"), 0, 8) StringUtils.substring(label.orElse(Option(environment.node.address)).getOrElse("unknown"), 0, 8)
override def draw(g: Graphics): Unit = { override def draw(g: Graphics): Unit = {
@ -115,5 +185,13 @@ trait Node extends Widget with DragHandler with ClickHandler with HoverHandler {
g.setNormalFont() 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 lazy val window: Option[Window] = None
} }

View File

@ -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)
}

View File

@ -1,6 +1,7 @@
package ocelot.desktop.node 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 import scala.collection.mutable
@ -11,6 +12,10 @@ object NodeRegistry {
types += t types += t
} }
register(NodeType("Relay", "nodes/Relay", -1, () => {
new RelayNode(new Relay)
}))
for (tier <- 0 to 2) { for (tier <- 0 to 2) {
register(NodeType("Screen" + tier, "nodes/Screen", tier, () => { register(NodeType("Screen" + tier, "nodes/Screen", tier, () => {
new ScreenNode(new Screen(tier)) new ScreenNode(new Screen(tier))

View File

@ -1,11 +1,12 @@
package ocelot.desktop.node package ocelot.desktop.node.nodes
import ocelot.desktop.OcelotDesktop import ocelot.desktop.OcelotDesktop
import ocelot.desktop.color.Color import ocelot.desktop.color.Color
import ocelot.desktop.graphics.Graphics import ocelot.desktop.graphics.Graphics
import ocelot.desktop.node.Node
import ocelot.desktop.util.TierColor import ocelot.desktop.util.TierColor
import totoro.ocelot.brain.entity.traits.Computer 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.loot.Loot
import totoro.ocelot.brain.util.Tier import totoro.ocelot.brain.util.Tier
@ -17,6 +18,7 @@ class ComputerNode(val computer: Case) extends Node {
computer.add(new InternetCard) computer.add(new InternetCard)
computer.add(new Memory(Tier.Six)) computer.add(new Memory(Tier.Six))
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")) computer.add(new HDDManaged(java.util.UUID.randomUUID().toString, Tier.Three, "hello"))
private val eeprom = Loot.OpenOsEEPROM.create() private val eeprom = Loot.OpenOsEEPROM.create()
eeprom.asInstanceOf[EEPROM].readonly = false eeprom.asInstanceOf[EEPROM].readonly = false

View File

@ -1,4 +1,4 @@
package ocelot.desktop.node package ocelot.desktop.node.nodes
import ocelot.desktop.geometry.Size2D import ocelot.desktop.geometry.Size2D
import ocelot.desktop.graphics.Graphics import ocelot.desktop.graphics.Graphics

View File

@ -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)
}

View File

@ -1,8 +1,9 @@
package ocelot.desktop.node package ocelot.desktop.node.nodes
import ocelot.desktop.OcelotDesktop import ocelot.desktop.OcelotDesktop
import ocelot.desktop.color.Color import ocelot.desktop.color.Color
import ocelot.desktop.graphics.Graphics import ocelot.desktop.graphics.Graphics
import ocelot.desktop.node.Node
import ocelot.desktop.util.TierColor import ocelot.desktop.util.TierColor
import totoro.ocelot.brain.entity.traits.Environment import totoro.ocelot.brain.entity.traits.Environment
import totoro.ocelot.brain.entity.{Keyboard, Screen} import totoro.ocelot.brain.entity.{Keyboard, Screen}

View File

@ -1,4 +1,4 @@
package ocelot.desktop.node package ocelot.desktop.node.nodes
import ocelot.desktop.color.{IntColor, RGBAColor, RGBAColorNorm} import ocelot.desktop.color.{IntColor, RGBAColor, RGBAColorNorm}
import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}

View File

@ -3,7 +3,8 @@ package ocelot.desktop.ui.widget
import ocelot.desktop.color.{Color, RGBAColor, RGBAColorNorm} import ocelot.desktop.color.{Color, RGBAColor, RGBAColorNorm}
import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D} import ocelot.desktop.geometry.{Rect2D, Size2D, Vector2D}
import ocelot.desktop.graphics.Graphics 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._
import ocelot.desktop.ui.event.handlers.{ClickHandler, DragHandler, HoverHandler} import ocelot.desktop.ui.event.handlers.{ClickHandler, DragHandler, HoverHandler}
import ocelot.desktop.ui.layout.{CopyLayout, Layout} 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 val nodeSelector = new NodeSelector
var cameraOffset: Vector2D = Vector2D(0, 0) 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 var newNodePos = Vector2D(0, 0)
private val gridAlpha = new ValueAnimation(0, 1f) private val gridAlpha = new ValueAnimation(0, 1f)
private val portsAlpha = new ValueAnimation(0, 7f)
override protected val layout: Layout = new CopyLayout(this) 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 = { def buildNewConnection(): Unit = {
val (start, endPoint) = newConnection.get val (node, port, _) = newConnection.get
val end = nodes.find(_.bounds.contains(endPoint)) newConnectionTarget match {
if (end.isDefined && end.get != start) { case Some((target, targetPort)) =>
if (end.get.environment.node.neighbors.exists(_ == start.environment.node)) if (node.isConnected(port, target, targetPort))
end.get.environment.disconnect(start.environment) node.disconnect(port, target, targetPort)
else else
end.get.environment.connect(start.environment) node.connect(port, target, targetPort)
case None =>
} }
newConnection = None newConnection = None
} }
def createDefaultWorkspace(): Unit = { 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)) 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 override def receiveMouseEvents = true
@ -144,40 +147,39 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover
!nodes.exists(_.bounds.collides(clearance)) !nodes.exists(_.bounds.collides(clearance))
} }
private def findSuitableConnections(a: Rect2D, b: Rect2D, checkCollision: Boolean, private def findSuitableConnections(a: Array[(Vector2D, Vector2D)],
forceParallel: Boolean, clampMin: Boolean): Iterator[Array[Vector2D]] = { b: Array[(Vector2D, Vector2D)],
var pairs = checkCollision: Boolean,
for (aSide <- a.edgeCenters.iterator; bSide <- b.edgeCenters.iterator) forceParallel: Boolean,
yield (aSide, bSide) 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) if (checkCollision)
pairs = pairs.filter(pair => { iter = iter.filter { case ((aSide, aCenter, aLen), (bSide, bCenter, bLen)) =>
val (aSide, bSide) = pair checkConnectorCollision(aSide, aCenter, aLen) &&
val (aLen, bLen) = connectorLen(a.center, aSide, b.center, bSide, clampMin) checkConnectorCollision(bSide, bCenter, bLen)
checkConnectorCollision(aSide, a.center, aLen) && }
checkConnectorCollision(bSide, b.center, bLen)
})
var paths = pairs.map(p => { var paths = iter.map { case ((aSide, aCenter, aLen), (bSide, bCenter, bLen)) =>
val (aLen, bLen) = connectorLen(a.center, p._1, b.center, p._2, clampMin) val aEnd = aSide + (aSide - aCenter).normalizeAxisAligned * aLen
val aEnd = p._1 + (p._1 - a.center).normalizeAxisAligned * aLen val bEnd = bSide + (bSide - bCenter).normalizeAxisAligned * bLen
val bEnd = p._2 + (p._2 - b.center).normalizeAxisAligned * bLen Array(aSide, aEnd, bEnd, bSide)
Array(p._1, aEnd, bEnd, p._2) }.filter(DrawUtils.isValidPolyline)
}).filter(DrawUtils.isValidPolyline)
if (forceParallel) if (forceParallel)
paths = paths.filter(p => { paths = paths.filter { case Array(aStart, aEnd, bEnd, bStart) =>
val Array(aStart, aEnd, bEnd, bStart) = p (aEnd - aStart).dot(bEnd - bStart) != 0f
val aDir = aEnd - aStart }
val bDir = bEnd - bStart
aDir.dot(bDir) != 0f
})
paths paths
} }
private def connectorLen(aCenter: Vector2D, aSide: Vector2D, private def connectorLen(aSide: Vector2D, aCenter: Vector2D,
bCenter: Vector2D, bSide: Vector2D, bSide: Vector2D, bCenter: Vector2D,
clampMin: Boolean): (Float, Float) = { clampMin: Boolean): (Float, Float) = {
val aDir = aSide - aCenter val aDir = aSide - aCenter
val bDir = bSide - bCenter val bDir = bSide - bCenter
@ -191,32 +193,111 @@ class WorkspaceView extends Widget with DragHandler with ClickHandler with Hover
(aLen.min(40f), bLen.min(40f)) (aLen.min(40f), bLen.min(40f))
} }
def drawConnection(g: Graphics, a: Rect2D, b: Rect2D, private def portDirections(node: Node, port: NodePort): Array[(Vector2D, Vector2D)] = {
clampMin: Boolean = true, val rect = node.bounds
checkCollision: Boolean = true, val sides = node.portsBounds.find(_._1 == port).get._2
forceParallel: Boolean = false, .map(rects => rects.edgeCenters.maxBy(c => (c - rect.center).lengthSquared))
drawBorder: Boolean = true,
thickness: Float = 4, col: Color = RGBAColor(150, 150, 150)): Unit = {
if (a.collides(b)) return
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) { if (paths.isEmpty) {
val canGiveUp = checkCollision || clampMin // give up
if (canGiveUp) { g.line(aRect.center, bRect.center, 8, RGBAColorNorm(0.1f, 0.1f, 0.1f))
if (drawBorder) g.line(aRect.center, bRect.center, 4, color)
g.line(a.center, b.center, thickness + 4, RGBAColorNorm(0.1f, 0.1f, 0.1f))
g.line(a.center, b.center, thickness, col)
}
} else { } else {
val path = paths.minBy(p => { val path = paths.minBy { case Array(_, aEnd, bEnd, _) => (aEnd - bEnd).lengthSquared }
val Array(_, aEnd, bEnd, _) = p DrawUtils.polyline(g, path, 8, RGBAColorNorm(0.1f, 0.1f, 0.1f))
(aEnd - bEnd).lengthSquared DrawUtils.polyline(g, path, 4, color)
})
if (drawBorder) val aDir = (path(1) - path(0)).normalizeAxisAligned * 8
DrawUtils.polyline(g, path, thickness + 4, RGBAColorNorm(0.1f, 0.1f, 0.1f)) g.line(path(0), path(0) + aDir, 4, aPort.getColor)
DrawUtils.polyline(g, path, thickness, col) 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) gridAlpha.goto(0f)
} }
val drawn = mutable.HashSet[(Node, Node)]() val drawn = mutable.HashSet[(Node, NodePort, Node, NodePort)]()
for (node <- nodes) { for (nodeA <- nodes) {
for (neighbour <- node.environment.node.neighbors) nodeA.connections.foreach { case (portA, nodeB, portB) =>
for (other <- nodes.find(_.environment.node == neighbour)) { if (!drawn.contains((nodeA, portA, nodeB, portB)) && !drawn.contains((nodeB, portB, nodeA, portA))) {
if (!drawn.contains((node, other)) && !drawn.contains((other, node))) { val gray = ((nodeA.getShortLabel + nodeB.getShortLabel).hashCode % 25 + 133).toShort
val gray = ((node.environment.node.address + other.environment.node.address).hashCode % 25 + 133).toShort val color = RGBAColor(gray, gray, gray)
val color = RGBAColor(gray, gray, gray) drawn += ((nodeA, portA, nodeB, portB))
drawn.add((node, other)) drawConnection(g, nodeA, portA, nodeB, portB, color = color)
drawConnection(g, node.bounds, other.bounds, col = color)
}
} }
}
} }
for ((start, endpoint) <- newConnection) { if (newConnection.isDefined) {
drawConnection(g, start.bounds, Rect2D(endpoint - Vector2D(32, 32), Size2D(64, 64)), drawNewConnection(g)
col = RGBAColorNorm(0.3f, 0.5f, 0.4f))
} }
nodes.foreach(_.draw(g)) nodes.foreach(_.draw(g))
nodes.foreach(_.drawLabel(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) { if (nodeSelector.isShown) {
g.sprite("nodes/NewNode", newNodePos.x + cameraOffset.x, 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) if (nodeSelector.isShown)
drawConnection(g, Rect2D(newNodePos.x + cameraOffset.x, newNodePos.y + cameraOffset.y, 64, 64), drawSelectorConnection(g, Rect2D(newNodePos.x + cameraOffset.x, newNodePos.y + cameraOffset.y, 64, 64),
nodeSelector.bounds, col = col, checkCollision = false, drawBorder = false, forceParallel = true, clampMin = false) nodeSelector.bounds, color = color)
drawChildren(g) drawChildren(g)
} }

View File

@ -43,7 +43,7 @@ object DrawUtils {
def polyline(g: Graphics, points: Array[Vector2D], thickness: Float = 4, color: Color = RGBAColor(255, 255, 255)): Unit = { def polyline(g: Graphics, points: Array[Vector2D], thickness: Float = 4, color: Color = RGBAColor(255, 255, 255)): Unit = {
if (points.length < 2) return if (points.length < 2) return
if (!isValidPolyline(points)) return // if (!isValidPolyline(points)) return
var start = points(0) var start = points(0)
for ((end, i) <- points.iterator.zipWithIndex.drop(1)) { for ((end, i) <- points.iterator.zipWithIndex.drop(1)) {
if (end != points.last) { if (end != points.last) {

View File

@ -10,5 +10,5 @@ object TierColor {
val Tiers: Array[Color] = Array(Tier0, Tier1, Tier2, Tier3) 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)
} }