package net.shadowfacts.asmr.ui import net.shadowfacts.asmr.program.* import net.shadowfacts.asmr.program.execution.ExecutableBlock import net.shadowfacts.asmr.program.execution.IncomingExecutionFlow import net.shadowfacts.asmr.program.execution.OutgoingExecutionFlow import net.shadowfacts.asmr.ui.block.ProgramBlockConnectionIndicatorView import net.shadowfacts.asmr.ui.block.ProgramBlockExecutionView import net.shadowfacts.asmr.ui.block.ProgramBlockParamView import net.shadowfacts.asmr.ui.block.ProgramBlockView import net.shadowfacts.cacao.geometry.Point import net.shadowfacts.cacao.util.Color import net.shadowfacts.cacao.util.MouseButton import net.shadowfacts.cacao.util.RenderHelper import net.shadowfacts.cacao.view.View /** * @author shadowfacts */ class ProgramCanvasView(val program: Program): View() { private lateinit var blocks: Map // map of starting block to (start, end) points private val executionFlowStartLinks = mutableMapOf>() private val parameterLinks = mutableMapOf>() private var currentPartialLink: PartialLink? = null override fun wasAdded() { super.wasAdded() zIndex = 5.0 blocks = program.blocks.associateWith { ProgramBlockView(it) } blocks.values.forEach { addSubview(it) } } private fun updateBlockLinks() { for ((startBlock, startView) in blocks) { (startBlock as? ExecutableBlock)?.let { startBlock -> for (outgoing in startBlock.outgoing) { val outgoingView = startView.outgoingViews[outgoing] ?: continue val start = outgoingView.convert(outgoingView.bounds.center, to = this) val incoming = outgoing.destination if (incoming != null) { val incomingBlockView = blocks[incoming.block] ?: continue val incomingView = incomingBlockView.incomingView!! val end = incomingView.convert(incomingView.bounds.center, to = this) executionFlowStartLinks[startBlock] = start to end } else { executionFlowStartLinks.remove(startBlock) } } } for (output in startBlock.outputs) { val outputView = startView.outputViews[output] ?: continue val start = outputView.convert(outputView.bounds.center, to = this) for (destination in output.destinations) { val endView = blocks[destination.block] ?: continue val inputView = endView.inputViews[destination] ?: continue val end = inputView.convert(inputView.bounds.center, to = this) parameterLinks[startBlock] = start to end } } } } override fun didLayout() { super.didLayout() // we can't calculate the link points until layout is completed and all of the views have concrete positions/sizes updateBlockLinks() } override fun drawContent(mouse: Point, delta: Float) { super.drawContent(mouse, delta) for ((start, end) in executionFlowStartLinks.values) { RenderHelper.drawLine(start, end, zIndex, 5f, Color.GREEN) } for ((start, end) in parameterLinks.values) { RenderHelper.drawLine(start, end, zIndex, 5f, Color.RED) } val currentPartialLink = currentPartialLink if (currentPartialLink != null) { val color = if (currentPartialLink is ParamInputPartialLink<*> || currentPartialLink is ParamOutputPartialLink<*>) Color.RED else Color.GREEN RenderHelper.drawLine(currentPartialLink.startPoint, mouse, zIndex, 5f, color) } } override fun mouseClicked(point: Point, mouseButton: MouseButton): Boolean { if (mouseButton == MouseButton.RIGHT && currentPartialLink != null) { this.currentPartialLink = null return true } for ((block, blockView) in blocks) { if (convert(point, to = blockView) !in blockView.bounds) continue for ((input, inputView) in blockView.inputViews) { if (convert(point, to = inputView) in inputView.bounds) { return inputViewClicked(block, input, inputView, mouseButton) } } for ((output, outputView) in blockView.outputViews) { if (convert(point, to = outputView) in outputView.bounds) { return outputViewClicked(block, output, outputView, mouseButton) } } if (block is ExecutableBlock) { if (convert(point, to = blockView.incomingView!!) in blockView.incomingView!!.bounds) { return incomingExecutionViewClicked(block, block.incoming, blockView.incomingView!!, mouseButton) } for ((outgoing, outgoingView) in blockView.outgoingViews) { if (convert(point, to = outgoingView) in outgoingView.bounds) { return outgoingExecutionViewClicked(block, outgoing, outgoingView, mouseButton) } } } } return super.mouseClicked(point, mouseButton) } private fun inputViewClicked(block: ProgramBlock, input: ProgramBlockInput, inputView: ProgramBlockParamView, mouseButton: MouseButton): Boolean { val currentPartialLink = currentPartialLink return if (currentPartialLink == null) { // no partial link in progress when (mouseButton) { MouseButton.LEFT -> { // begin a new one this.currentPartialLink = ParamInputPartialLink(input, inputView, inputView.convert(inputView.bounds.center, to = this)) true } MouseButton.RIGHT -> { // clear clicked connection val source = input.source ?: return false val outputView = blocks[source.block]?.outputViews?.get(source) ?: return false input.unlink() inputView.updateTexture() outputView.updateTexture() updateBlockLinks() true } else -> false } } else if (currentPartialLink is ParamOutputPartialLink<*> && currentPartialLink.output.type == input.type) { // current link in progress originated with an output param of same type as clicked input, so complete it // we can ignore the unchecked cast because we checked it ourselves above by comparing ProgramTypes, but the compiler doesn't know that @Suppress("NAME_SHADOWING", "UNCHECKED_CAST") val currentPartialLink = currentPartialLink as ParamOutputPartialLink currentPartialLink.output.link(to = input) currentPartialLink.view.updateTexture() inputView.updateTexture() updateBlockLinks() this.currentPartialLink = null true } else { // otherwise, click always fails false } } private fun outputViewClicked(block: ProgramBlock, output: ProgramBlockOutput, outputView: ProgramBlockParamView, mouseButton: MouseButton): Boolean { val currentPartialLink = currentPartialLink return if (currentPartialLink == null) { // no partial link in progress when (mouseButton) { MouseButton.LEFT -> { // begin a new one this.currentPartialLink = ParamOutputPartialLink(output, outputView, outputView.convert(outputView.bounds.center, to = this)) true } MouseButton.RIGHT -> { // clear clicked connection val inputViews = output.destinations.mapNotNull { blocks[it.block]?.inputViews?.get(it) } output.unlinkAll() outputView.updateTexture() inputViews.forEach(ProgramBlockParamView::updateTexture) updateBlockLinks() true } else -> false } } else if (currentPartialLink is ParamInputPartialLink<*> && currentPartialLink.input.type == output.type) { // current link in progress originated with an input param of same type as clicked output, so complete it // we can ignore the unchecked cast because we checked it ourselves above by comparing ProgramTypes, but the compiler doesn't know that @Suppress("NAME_SHADOWING", "UNCHECKED_CAST") val currentPartialLink = currentPartialLink as ParamInputPartialLink currentPartialLink.input.link(from = output) currentPartialLink.view.updateTexture() outputView.updateTexture() updateBlockLinks() this.currentPartialLink = null true } else { // otherwise, click always fails false } } private fun incomingExecutionViewClicked(block: ProgramBlock, incoming: IncomingExecutionFlow, incomingView: ProgramBlockExecutionView, mouseButton: MouseButton): Boolean { val currentPartialLink = currentPartialLink return if (currentPartialLink == null) { // no partial link in progress when (mouseButton) { MouseButton.LEFT -> { // being a new one this.currentPartialLink = IncomingExecutionPartialLink(incoming, incomingView, incomingView.convert(incomingView.bounds.center, to = this)) true } MouseButton.RIGHT -> { // clear clicked connection val outgoing = incoming.source ?: return false val outgoingView = blocks[outgoing.block]?.outgoingViews?.get(outgoing) ?: return false incoming.unlink() incomingView.updateTexture() outgoingView.updateTexture() updateBlockLinks() true } else -> false } } else if (currentPartialLink is OutgoingExecutionPartialLink) { // current link in progress originated with an outgoing execution connection, so complete it currentPartialLink.outgoing.link(incoming) currentPartialLink.view.updateTexture() incomingView.updateTexture() updateBlockLinks() this.currentPartialLink = null true } else { // otherwise, click always fails false } } private fun outgoingExecutionViewClicked(block: ProgramBlock, outgoing: OutgoingExecutionFlow, outgoingView: ProgramBlockExecutionView, mouseButton: MouseButton): Boolean { val currentPartialLink = currentPartialLink return if (currentPartialLink == null) { // no partial link in progress when (mouseButton) { MouseButton.LEFT -> { // begin a new one this.currentPartialLink = OutgoingExecutionPartialLink(outgoing, outgoingView, outgoingView.convert(outgoingView.bounds.center, to = this)) true } MouseButton.RIGHT -> { // clear clicked connection val incoming = outgoing.destination ?: return false val incomingView = blocks[incoming.block]?.incomingView ?: return false outgoing.unlink() outgoingView.updateTexture() incomingView.updateTexture() updateBlockLinks() true } else -> false } } else if (currentPartialLink is IncomingExecutionPartialLink) { // current link in progress originated with an incoming execution connection, so complete it currentPartialLink.incoming.link(outgoing) currentPartialLink.view.updateTexture() outgoingView.updateTexture() updateBlockLinks() this.currentPartialLink = null true } else { // otherwise, click always fails false } } open class PartialLink(val view: ProgramBlockConnectionIndicatorView, val startPoint: Point) class ParamInputPartialLink(val input: ProgramBlockInput, view: ProgramBlockParamView, startPoint: Point): PartialLink(view, startPoint) class ParamOutputPartialLink(val output: ProgramBlockOutput, view: ProgramBlockParamView, startPoint: Point): PartialLink(view, startPoint) class IncomingExecutionPartialLink(val incoming: IncomingExecutionFlow, view: ProgramBlockExecutionView, startPoint: Point): PartialLink(view, startPoint) class OutgoingExecutionPartialLink(val outgoing: OutgoingExecutionFlow, view: ProgramBlockExecutionView, startPoint: Point): PartialLink(view, startPoint) }