Skip to content

Commit

Permalink
Dynamic add/remove of nodes and outline selected node
Browse files Browse the repository at this point in the history
  • Loading branch information
ultraq committed Jun 20, 2024
1 parent f61f99a commit fe5fcbe
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 85 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@ import org.joml.Vector2f
import org.joml.Vector3f
import org.joml.primitives.Rectanglef

import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList

/**
Expand Down Expand Up @@ -55,26 +58,71 @@ class Node<T extends Node> implements SceneEvents, Scriptable<T>, Visitable {
}

/**
* Adds a child node to this node.
* Adds a child node to this node. If an index is specified, then this will
* shift any existing nodes at the given position to the right to make room.
*/
T addChild(Node child) {

children << child
T addChild(Node child, int index = -1) {

if (index != -1) {
children.add(index, child)
}
else {
children.add(child)
}
child.parent = this

var scene = getScene()
if (scene) {
addNodeAndChildren(scene, child).join()
}
bounds.expand(child.bounds)
return this
}

/**
* Adds a child node to this node, shifting any existing nodes at the given
* position to the right to make room.
* Trigger the {@code onSceneAdded} event for this node and all its children.
* Each node triggers a {@link NodeAddedEvent} event.
*/
T addChild(int index, Node child) {
protected CompletableFuture<Void> addNodeAndChildren(Scene scene, Node node) {

children.add(index, child)
child.parent = this
return this
return CompletableFuture.allOf(
node.onSceneAddedAsync(scene),
node.script?.onSceneAddedAsync(scene) ?: CompletableFuture.completedFuture(null)
)
.thenRun { ->
scene.trigger(new NodeAddedEvent(node))
}
.thenCompose { _ ->
var futures = node.children.collect { childNode -> addNodeAndChildren(scene, childNode) }

// Originally used the Groovy spread operator `*` but this would throw
// an exception about "array length is not legal" 🤷
return CompletableFuture.allOf(futures.toArray(new CompletableFuture<Void>[0]))
}
}

/**
* Remove all child nodes from this node.
*/
void clear() {

children.each { child ->
removeChild(child)
}
}

/**
* Locate the first node in the scene that satisfies the given predicate.
* Shorthand for {@code scene.root.findChild(predicate)}.
*
* @param predicate
* @return
* The matching node, or {@code null} if no match is found.
*/
Node findNode(@ClosureParams(value = SimpleType, options = "Node") Closure<Boolean> predicate) {

return children.find { node ->
return predicate(node) ? node : node.findNode(predicate)
}
}

/**
Expand Down Expand Up @@ -159,6 +207,15 @@ class Node<T extends Node> implements SceneEvents, Scriptable<T>, Visitable {
return transform.getScale(scale)
}

/**
* Walk up the scene graph to locate and return the scene to which this node
* belongs.
*/
protected Scene getScene() {

return parent?.getScene()
}

/**
* Return the width of the node. This is a shortcut for calling
* {@code bounds.lengthX()}.
Expand Down Expand Up @@ -186,6 +243,54 @@ class Node<T extends Node> implements SceneEvents, Scriptable<T>, Visitable {
addChild(child)
}

/**
* Remove the child node from this one.
*/
T removeChild(Node node) {

if (children.remove(node)) {
removeNodeAndChildren(scene, node).join()
node.parent = null
// TODO: Recalculate bounds
}
return this
}

/**
* Remove any child node that satisfies the closure condition.
*/
T removeChild(Closure predicate) {

children.each { node ->
if (predicate(node)) {
removeChild(node)
}
}
return this
}

/**
* Trigger the {@code onSceneRemoved} handler for this node and all its
* children. Each node triggers a {@link NodeRemovedEvent} event.
*/
protected CompletableFuture<Void> removeNodeAndChildren(Scene scene, Node node) {

return CompletableFuture.allOf(
node.onSceneRemovedAsync(scene),
node.script?.onSceneRemovedAsync(scene) ?: CompletableFuture.completedFuture(null)
)
.thenRun { ->
scene.trigger(new NodeRemovedEvent(node))
}
.thenCompose { _ ->
var futures = node.children.collect { childNode -> removeNodeAndChildren(scene, childNode) }

// Originally used the Groovy spread operator `*` but this would throw
// an exception about "array length is not legal" 🤷
return CompletableFuture.allOf(futures.toArray(new CompletableFuture<Void>[0]))
}
}

/**
* Set the local position of this node.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,9 @@ import nz.net.ultraq.redhorizon.events.EventTarget
import org.slf4j.Logger
import org.slf4j.LoggerFactory

import groovy.transform.TupleConstructor
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import java.util.concurrent.CompletableFuture
import java.util.concurrent.CopyOnWriteArrayList

/**
* Entry point for the Red Horizon scene graph, holds all of the objects that
Expand All @@ -44,7 +43,7 @@ class Scene implements EventTarget, Visitable {

private static final Logger logger = LoggerFactory.getLogger(Scene)

final List<Node> nodes = new CopyOnWriteArrayList<>()
final Node root = new RootNode(this)

// TODO: This stuff is really all 'scene/application context' objects, so
// should be moved to something as such 🤔
Expand All @@ -67,67 +66,43 @@ class Scene implements EventTarget, Visitable {
@Override
void accept(SceneVisitor visitor) {

nodes.each { node ->
node.accept(visitor)
}
root.accept(visitor)
}

/**
* Add a top-level node to this scene.
* Add a top-level node to this scene. Shorthand for
* {@code scene.root.addNode(node)}.
*/
Scene addNode(Node node) {

return time('Adding node', logger) { ->
nodes << node
addNodeAndChildren(node).join()
return this
time('Adding node', logger) { ->
root.addChild(node)
}
return this
}

/**
* Trigger the {@code onSceneAdded} event for this node and all its children.
* Each node triggers a {@link NodeAddedEvent} event.
*/
private CompletableFuture<Void> addNodeAndChildren(Node node) {

return CompletableFuture.allOf(
node.onSceneAddedAsync(this),
node.script?.onSceneAddedAsync(this) ?: CompletableFuture.completedFuture(null)
)
.thenRun { ->
trigger(new NodeAddedEvent(node))
}
.thenCompose { _ ->
var futures = node.children.collect { childNode -> addNodeAndChildren(childNode) }

// Originally used the Groovy spread operator `*` but this would throw
// an exception about "array length is not legal" 🤷
return CompletableFuture.allOf(futures.toArray(new CompletableFuture<Void>[0]))
}
}

/**
* Clear this scene's existing elements.
* Clear this scene's existing elements. Shorthand for
* {@code scene.root.clear()}.
*/
void clear() {

time('Clearing scene', logger) { ->
nodes.each { node ->
removeNode(node)
}
root.clear()
}
}

/**
* Locate the first node in the scene that satisfies the given predicate.
* Shorthand for {@code scene.root.findChild(predicate)}.
*
* @param predicate
* @return
* The matching node, or {@code null} if no match is found.
*/
<T extends Node> T findNode(@ClosureParams(value = SimpleType, options = "Node") Closure<Boolean> predicate) {
Node findNode(@ClosureParams(value = SimpleType, options = "Node") Closure<Boolean> predicate) {

return (T)nodes.find(predicate)
return root.findNode(predicate)
}

/**
Expand Down Expand Up @@ -173,7 +148,8 @@ class Scene implements EventTarget, Visitable {
// }

/**
* Removes a top-level node from the scene.
* Removes a top-level node from the scene. Shorthand for
* {@code scene.root.removeChild(node)}.
*
* @param node
* The node to remove. If {@code null}, then this method does nothing.
Expand All @@ -182,32 +158,30 @@ class Scene implements EventTarget, Visitable {
*/
Scene removeNode(Node node) {

if (node) {
nodes.remove(node)
removeNodeAndChildren(node).join()
time('Removing node', logger) { ->
root.removeChild(node)
}
return this
}

/**
* Trigger the {@code onSceneRemoved} event for this node and all its
* children. Each node triggers a {@link NodeRemovedEvent} event.
* A special instance of {@link Node} that is always present in the scene.
*/
private CompletableFuture<Void> removeNodeAndChildren(Node node) {

return CompletableFuture.allOf(
node.onSceneRemovedAsync(this),
node.script?.onSceneRemovedAsync(this) ?: CompletableFuture.completedFuture(null)
)
.thenRun { ->
trigger(new NodeRemovedEvent(node))
}
.thenCompose { _ ->
var futures = node.children.collect { childNode -> removeNodeAndChildren(childNode) }

// Originally used the Groovy spread operator `*` but this would throw
// an exception about "array length is not legal" 🤷
return CompletableFuture.allOf(futures.toArray(new CompletableFuture<Void>[0]))
}
@TupleConstructor(defaults = false)
private static class RootNode extends Node<RootNode> {

final Scene scene

@Override
String getName() {

return 'Root node'
}

@Override
protected Scene getScene() {

return scene
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ class GridLines extends Node<GridLines> {

// TODO: Add support for vertices with different colours
var cellLines = new Primitive(MeshType.LINES, new Colour('GridLines-Grey', 0.6, 0.6, 0.6), lines as Vector2f[])
cellLines.name = "Step lines"
cellLines.name = 'Step lines'
addChild(cellLines)

var originLines = new Primitive(MeshType.LINES, new Colour('GridLines-DarkGrey', 0.2, 0.2, 0.2),
new Vector2f(coordMin, 0), new Vector2f(coordMax, 0),
new Vector2f(0, coordMin), new Vector2f(0, coordMax)
)
originLines.name = "Origin lines"
originLines.name = 'Origin lines'
addChild(originLines)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2024, Emanuel Rabina (http://www.ultraq.net.nz/)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package nz.net.ultraq.redhorizon.engine.scenegraph.nodes

import nz.net.ultraq.redhorizon.engine.graphics.Colour
import nz.net.ultraq.redhorizon.engine.graphics.MeshType

import org.joml.Vector2f
import org.joml.primitives.Rectanglef

/**
* Draws a line at the bounds of its parent node. Mainly used for debugging.
*
* @author Emanuel Rabina
*/
class Outline extends Primitive {

Outline(Rectanglef bounds) {

super(MeshType.LINE_LOOP, Colour.RED, bounds as Vector2f[])
this.bounds.set(bounds)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,8 @@ class Primitive extends Node<Primitive> implements GraphicsElement {
final Colour colour
final Vector2f[] points

private Mesh mesh
private Shader shader
protected Mesh mesh
protected Shader shader

/**
* Constructor, create a set of lines for every 2 vectors passed in to this
Expand Down
Loading

0 comments on commit fe5fcbe

Please sign in to comment.