Code/UI/Spatial/NodeManipulator.cs
namespace Nodebox.UI.Spatial;
[Title("Nodebox Spatial Node Manipulator")]
[Category("Nodebox")]
public class NodeManipulator : Component, Node.IEvent, IGraphStyle {
[RequireComponent] public ScreenPanel ScreenPanel { get; set; }
[RequireComponent] public NodePickRoot NodePickRoot { get; set; }
[RequireComponent] public Sandbox.WorldInput WorldInput { get; set; }
[RequireComponent] public LineRenderer LineRenderer { get; set; }
[Property] public Graph Graph { get; set; }
[Property] public GraphStyle GraphStyle { get; set; }
[Property] public float SpinSpeed { get; set; } = 5f;
[Property] public float ScrollSpeed { get; set; } = 5f;
[Property]
[ReadOnly]
[JsonIgnore]
public Node Hovered => WorldInput.Hovered?.FindRootPanel()?.GameObject?.GetComponent<Node>();
public bool IsHoveringPin => WorldInput.Hovered is PinButton;
public (bool Down, bool Pressed, bool Released) LeftAction => (
Input.Down(WorldInput.LeftMouseAction),
Input.Pressed(WorldInput.LeftMouseAction),
Input.Released(WorldInput.LeftMouseAction)
);
public (bool Down, bool Pressed, bool Released) RightAction => (
Input.Down(WorldInput.RightMouseAction),
Input.Pressed(WorldInput.RightMouseAction),
Input.Released(WorldInput.RightMouseAction)
);
protected override void OnAwake() {
base.OnAwake();
GraphStyle = GraphStyle.DefaultForExecutionContext();
GraphStyle.ConfigureLineRenderer(LineRenderer);
// ScreenPanel.Enabled = false;
ScreenPanel.AutoScreenScale = false;
NodePickRoot.Enabled = false;
NodePickRoot.GraphStyle = GraphStyle;
NodePickRoot.Collection = Nodebox.Node.Collection.FromAllWithIntrinsics();
NodePickRoot.NodeSelected += OnNodeSelected;
LineRenderer.Enabled = false;
LineRenderer.VectorPoints = [
new(), new(), new(),
];
Graph ??= GlobalGraph.Instance?.Graph;
}
[Property]
[ReadOnly]
[JsonIgnore]
public Node Grabbed { get; protected set; }
public Vector3 GrabbedLocalPosition { get; protected set; } = Vector3.Zero;
protected void Grab() {
if (!Hovered.IsValid()) { return; }
if (Hovered.MousePosition is not Vector3 worldMousePosition) { return; }
Grabbed = Hovered;
GrabbedLocalPosition = Grabbed.WorldTransform.PointToLocal(worldMousePosition);
}
public void UnGrab() {
Grabbed = null;
}
[Property]
[ReadOnly]
[JsonIgnore]
public (Node Node, PinRef PinRef)? WirePoint { get; protected set; } = null;
public (Node Node, PinRef PinRef)? WireSource => (WirePoint.HasValue && WirePoint.Value.PinRef.Flow == Flow.Output) ? WirePoint.Value : null;
public (Node Node, PinRef PinRef)? WireDestination => (WirePoint.HasValue && WirePoint.Value.PinRef.Flow == Flow.Input) ? WirePoint.Value : null;
protected float WireDistance { get; set; } = 0f;
protected void BeginWire(Node node, PinRef pinRef) {
if (node.PanelToWorldRelativeToPin(
pinRef.Flow,
pinRef.Index,
new Vector2(0.5f, 0.5f)
) is not Vector3 worldPosition) {
return;
}
WireDistance = worldPosition.Distance(WorldPosition);
if (pinRef.Flow == Flow.Input && pinRef.Wires.Any()) {
foreach (var oldWire in pinRef.Wires) {
WirePoint = (oldWire.Source.Spatial, new PinRef(oldWire.Source, Flow.Output, oldWire.SourceIndex));
oldWire.Spatial.DestroyGameObject();
}
return;
}
WirePoint = (node, pinRef);
}
public void DropWire() {
WirePoint = null;
}
public void EndWire(Node node2, PinRef pinRef2) {
if (!WirePoint.HasValue) { return; }
(Node node1, PinRef pinRef1) = WirePoint.Value;
if (pinRef1.Flow == pinRef2.Flow) { DropWire(); return; }
var source = WireSource?.Node ?? node2;
var sourcePinRef = WireSource?.PinRef ?? pinRef2;
var destination = WireDestination?.Node ?? node2;
var destinationPinRef = WireDestination?.PinRef ?? pinRef2;
if (sourcePinRef.Type != typeof(object) && destinationPinRef.Type != typeof(object)) {
if (sourcePinRef.Type != destinationPinRef.Type) { DropWire(); return; } // TODO: Create a Cast<TSource, TDestination> somehow
}
foreach (var oldWire in destinationPinRef.Wires) {
oldWire.Spatial.DestroyGameObject();
if (oldWire.Source == source.Inner) {
DropWire();
return;
}
}
var wireGo = new GameObject(source.GameObject, name: $"{source.GameObject.Name} -> {destination.GameObject.Name}");
var wire = wireGo.AddComponent<Wire>(false);
wire.GraphStyle = GraphStyle;
wire.SourceIndex = sourcePinRef.Index;
wire.Destination = destination;
wire.DestinationIndex = destinationPinRef.Index;
wire.Enabled = true;
DropWire();
}
void Node.IEvent.OnPinClicked(Node node, PinRef pinRef) {
if (WirePoint == null) {
BeginWire(node, pinRef);
return;
}
EndWire(node, pinRef);
}
protected Transform? PreviousTransform { get; set; }
protected void MoveGrabbed() {
if (!Grabbed.IsValid()) { return; }
if (!PreviousTransform.HasValue) { return; }
var previous = PreviousTransform.Value;
var current = GameObject.WorldTransform;
var worldTransform = Grabbed.WorldTransform.ToWorld(new Transform(GrabbedLocalPosition));
var localTransform = previous.ToLocal(worldTransform);
localTransform.Position = localTransform.Position.Normal * (localTransform.Position.Length + Input.MouseWheel.y * 5f);
worldTransform = current.ToWorld(localTransform);
worldTransform.Rotation = Rotation.LookAt(worldTransform.Forward.SubtractDirection(Vector3.Up).Normal, Vector3.Up);
Grabbed.WorldTransform = worldTransform with { Position = worldTransform.Position - worldTransform.Rotation * GrabbedLocalPosition };
}
protected void UpdateLineRenderer() {
if (!WirePoint.HasValue) {
LineRenderer.Enabled = false;
return;
}
var wirePointInFront = WorldPosition + WorldTransform.Forward * WireDistance;
if (WireSource?.Node?.PanelToWorldRelativeToPin(
Flow.Output,
WireSource.Value.PinRef.Index,
new Vector2(0.5f, 0.5f)
) is not Vector3 start) {
start = wirePointInFront;
}
if (WireDestination?.Node?.PanelToWorldRelativeToPin(
Flow.Input,
WireDestination.Value.PinRef.Index,
new Vector2(0f, 0.5f)
) is not Vector3 end) {
end = wirePointInFront;
}
var color = GraphStyle?.TypePalette?.Get(WirePoint.Value.PinRef.Type).Color ?? Color.White;
var startNormal = WireSource?.Node.WorldTransform.NormalToWorld(new Vector3(0f, 1f, 0f)) ?? Vector3.Zero;
var endNormal = WireDestination?.Node.WorldTransform.NormalToWorld(new Vector3(0f, -1f, 0f)) ?? Vector3.Zero;
LineRenderer.Enabled = true;
LineRenderer.Color = color;
LineRenderer.VectorPoints[0] = start; //- startNormal * 0f;
if (WireSource.HasValue) {
LineRenderer.VectorPoints[1] = start + startNormal * 3f;
} else {
LineRenderer.VectorPoints[1] = end + endNormal * 3f;
}
LineRenderer.VectorPoints[2] = end + endNormal * 1f;
}
protected override void OnUpdate() {
WorldInput.Enabled = Enabled;
if (RightAction.Released) {
if (WorldInput.Hovered == null) {
NodePickRoot.Enabled = true;
} else {
var go = WorldInput.Hovered.GameObject;
if (go.IsValid() && go.GetComponent<Node>() is Node node && node.IsValid()) {
go.Destroy();
}
}
}
if (WirePoint.HasValue && !WirePoint.Value.Node.IsValid()) {
DropWire();
}
NodePickRoot.NodePick?.PinRef = WirePoint?.PinRef;
if (LeftAction.Pressed && !IsHoveringPin) {
Grab();
if (!Grabbed.IsValid()) {
DropWire();
}
}
if (!LeftAction.Down && Grabbed != null) {
UnGrab();
}
MoveGrabbed();
PreviousTransform = GameObject.WorldTransform;
}
protected void OnNodeSelected(SerializableType type) {
NodePickRoot.Enabled = false;
var nodeGo = new GameObject(Graph.GameObject, name: $"{DisplayInfo.ForGenericType(type).Name}");
var distance = 50f;
if (WirePoint.HasValue) {
distance = WireDistance;
}
nodeGo.WorldPosition = WorldTransform.PointToWorld(Vector3.Forward * distance);
nodeGo.WorldRotation = Rotation.LookAt(-WorldTransform.Forward.SubtractDirection(Vector3.Up).Normal, Vector3.Up);
var node = nodeGo.AddComponent<Node>(false);
node.Type = type;
node.Enabled = true;
if (WirePoint.HasValue) {
EndWire(node, new PinRef(node.Inner, WirePoint.Value.PinRef.Flow.Opposite, 0));
}
}
protected override void OnPreRender() {
base.OnPreRender();
UpdateLineRenderer();
}
protected override void OnDisabled() {
base.OnDisabled();
UnGrab();
}
}