summaryrefslogtreecommitdiffstats
path: root/Scripts/Fold/Editor
diff options
context:
space:
mode:
authoryum <yum.food.vr@gmail.com>2026-01-06 18:18:05 -0800
committeryum <yum.food.vr@gmail.com>2026-01-06 18:18:05 -0800
commit317f8527bc1c0aedb234ecae331aeec18aeaf9bd (patch)
treebc44dc70287d856b7f4c5a732db3e8ebcb123f8e /Scripts/Fold/Editor
parentb1bfb0400c65b2602670bb22eea50a8b9db285a8 (diff)
Fold: big rewrite
Diffstat (limited to 'Scripts/Fold/Editor')
-rw-r--r--Scripts/Fold/Editor/FoldGraph.cs112
-rw-r--r--Scripts/Fold/Editor/FoldGraphView.cs785
-rw-r--r--Scripts/Fold/Editor/FoldWindow.cs7
3 files changed, 193 insertions, 711 deletions
diff --git a/Scripts/Fold/Editor/FoldGraph.cs b/Scripts/Fold/Editor/FoldGraph.cs
index ca985f4..5782dc7 100644
--- a/Scripts/Fold/Editor/FoldGraph.cs
+++ b/Scripts/Fold/Editor/FoldGraph.cs
@@ -5,10 +5,7 @@ using UnityEngine;
[Serializable]
public class FoldEdge
{
- public string outputNodeGuid;
- public string outputPortName;
- public string inputNodeGuid;
- public string inputPortName;
+ public string outputNodeGuid, outputPortName, inputNodeGuid, inputPortName;
}
[Serializable]
@@ -22,126 +19,45 @@ public abstract class FoldNodeData
[Serializable]
public abstract class FoldOperationData : FoldNodeData
{
+ public abstract IEnumerable<(string name, Type type)> GetInputPorts();
public abstract FoldNodeSerialized Serialize(FoldGraphView graphView);
}
-[Serializable]
-public class FloatValueNodeData : FoldNodeData
-{
- public float value;
- public override string Title => "Float";
-}
-
-[Serializable]
-public class VectorValueNodeData : FoldNodeData
-{
- public Vector4 value;
- public override string Title => "Vector";
-}
-
-[Serializable]
-public class GameObjectNodeData : FoldNodeData
-{
- public GameObject output;
- public override string Title => "GameObject";
-}
+[Serializable] public class FloatValueNodeData : FoldNodeData { public float value; public override string Title => "Float"; }
+[Serializable] public class VectorValueNodeData : FoldNodeData { public Vector4 value; public override string Title => "Vector"; }
+[Serializable] public class GameObjectNodeData : FoldNodeData { public GameObject output; public override string Title => "GameObject"; }
+[Serializable] public class KeyframeNodeData : FoldNodeData { public override string Title => "Keyframe"; }
[Serializable]
public class AxisAlignNodeData : FoldOperationData
{
- public Vector4 po;
- public Vector4 pp;
- public Vector4 r;
- public float t;
-
public override string Title => "Axis Align";
-
- public override FoldNodeSerialized Serialize(FoldGraphView graphView)
- {
- return new FoldNodeSerialized
- {
- opcode = 4,
- float0 = graphView.GetInputValue(guid, "t", t),
- vec0 = graphView.GetInputValue(guid, "po", po),
- vec1 = graphView.GetInputValue(guid, "pp", pp),
- vec2 = graphView.GetInputValue(guid, "r", r),
- };
- }
+ public override IEnumerable<(string, Type)> GetInputPorts() { yield return ("po", typeof(Vector4)); yield return ("pp", typeof(Vector4)); yield return ("r", typeof(Vector4)); yield return ("t", typeof(float)); }
+ public override FoldNodeSerialized Serialize(FoldGraphView gv) => new() { opcode = 4, float0 = gv.GetInputValue(guid, "t", 0f), vec0 = gv.GetInputValue(guid, "po", Vector4.zero), vec1 = gv.GetInputValue(guid, "pp", Vector4.zero), vec2 = gv.GetInputValue(guid, "r", Vector4.zero) };
}
[Serializable]
public class PlaneToTubeNodeData : FoldOperationData
{
- public Vector4 p;
- public Vector4 r;
- public Vector4 s;
- public float t;
-
public override string Title => "Plane to Tube";
-
- public override FoldNodeSerialized Serialize(FoldGraphView graphView)
- {
- return new FoldNodeSerialized
- {
- opcode = 2,
- float0 = graphView.GetInputValue(guid, "t", t),
- vec0 = graphView.GetInputValue(guid, "p", p),
- vec1 = graphView.GetInputValue(guid, "r", r),
- vec2 = graphView.GetInputValue(guid, "s", s),
- };
- }
+ public override IEnumerable<(string, Type)> GetInputPorts() { yield return ("p", typeof(Vector4)); yield return ("r", typeof(Vector4)); yield return ("s", typeof(Vector4)); yield return ("t", typeof(float)); }
+ public override FoldNodeSerialized Serialize(FoldGraphView gv) => new() { opcode = 2, float0 = gv.GetInputValue(guid, "t", 0f), vec0 = gv.GetInputValue(guid, "p", Vector4.zero), vec1 = gv.GetInputValue(guid, "r", Vector4.zero), vec2 = gv.GetInputValue(guid, "s", Vector4.zero) };
}
[Serializable]
public class PointAlignNodeData : FoldOperationData
{
- public Vector4 po;
- public Vector4 pp;
- public Vector4 r;
- public float t;
-
public override string Title => "Point Align";
-
- public override FoldNodeSerialized Serialize(FoldGraphView graphView)
- {
- return new FoldNodeSerialized
- {
- opcode = 3,
- float0 = graphView.GetInputValue(guid, "t", t),
- vec0 = graphView.GetInputValue(guid, "po", po),
- vec1 = graphView.GetInputValue(guid, "pp", pp),
- vec2 = graphView.GetInputValue(guid, "r", r),
- };
- }
+ public override IEnumerable<(string, Type)> GetInputPorts() { yield return ("po", typeof(Vector4)); yield return ("pp", typeof(Vector4)); yield return ("r", typeof(Vector4)); yield return ("t", typeof(float)); }
+ public override FoldNodeSerialized Serialize(FoldGraphView gv) => new() { opcode = 3, float0 = gv.GetInputValue(guid, "t", 0f), vec0 = gv.GetInputValue(guid, "po", Vector4.zero), vec1 = gv.GetInputValue(guid, "pp", Vector4.zero), vec2 = gv.GetInputValue(guid, "r", Vector4.zero) };
}
[Serializable]
public class TubeToPlaneNodeData : FoldOperationData
{
- public Vector4 p;
- public Vector4 r;
- public Vector4 s;
- public float t;
-
public override string Title => "Tube to Plane";
-
- public override FoldNodeSerialized Serialize(FoldGraphView graphView)
- {
- return new FoldNodeSerialized
- {
- opcode = 1,
- float0 = graphView.GetInputValue(guid, "t", t),
- vec0 = graphView.GetInputValue(guid, "p", p),
- vec1 = graphView.GetInputValue(guid, "r", r),
- vec2 = graphView.GetInputValue(guid, "s", s),
- };
- }
-}
-
-[Serializable]
-public class KeyframeNodeData : FoldNodeData
-{
- public override string Title => "Keyframe";
+ public override IEnumerable<(string, Type)> GetInputPorts() { yield return ("p", typeof(Vector4)); yield return ("r", typeof(Vector4)); yield return ("s", typeof(Vector4)); yield return ("t", typeof(float)); }
+ public override FoldNodeSerialized Serialize(FoldGraphView gv) => new() { opcode = 1, float0 = gv.GetInputValue(guid, "t", 0f), vec0 = gv.GetInputValue(guid, "p", Vector4.zero), vec1 = gv.GetInputValue(guid, "r", Vector4.zero), vec2 = gv.GetInputValue(guid, "s", Vector4.zero) };
}
public class FoldGraph : ScriptableObject
diff --git a/Scripts/Fold/Editor/FoldGraphView.cs b/Scripts/Fold/Editor/FoldGraphView.cs
index 6602af3..2c9e933 100644
--- a/Scripts/Fold/Editor/FoldGraphView.cs
+++ b/Scripts/Fold/Editor/FoldGraphView.cs
@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
-using System.Reflection;
using UnityEditor;
using UnityEditor.Experimental.GraphView;
using UnityEditor.UIElements;
@@ -15,16 +14,10 @@ public interface IFoldNode
IFoldNode GetPreviousFold();
}
-public interface IValueNode<T>
-{
- T GetValue();
-}
-
public abstract class FoldNodeView : Node
{
public FoldNodeData Data { get; private set; }
protected FoldGraphView graphView;
-
readonly Dictionary<string, Port> ports = new();
public virtual void Initialize(FoldGraphView view, FoldNodeData data)
@@ -35,12 +28,9 @@ public abstract class FoldNodeView : Node
viewDataKey = data.guid;
SetPosition(new Rect(data.position, Vector2.zero));
- // Allow shift+click to add to the current selection without clearing it.
RegisterCallback<MouseDownEvent>(evt =>
{
- if (!evt.shiftKey)
- return;
-
+ if (!evt.shiftKey) return;
if (!graphView.selection.Contains(this))
graphView.AddToSelection(this);
evt.StopImmediatePropagation();
@@ -65,290 +55,161 @@ public abstract class FoldNodeView : Node
port.portName = name;
ports[name] = port;
graphView.BindEdgeConnector(port);
- if (direction == Direction.Input) inputContainer.Add(port);
- else outputContainer.Add(port);
+ (direction == Direction.Input ? inputContainer : outputContainer).Add(port);
return port;
}
- public Port GetPort(string name) => ports.TryGetValue(name, out var port) ? port : null;
+ public Port GetPort(string name) => ports.TryGetValue(name, out var p) ? p : null;
}
-public abstract class FoldOperationNodeView : FoldNodeView, IFoldNode
+public class ValueNodeView : FoldNodeView
{
- protected Port foldInputPort;
- protected Port foldOutputPort;
-
- public override void Initialize(FoldGraphView view, FoldNodeData data)
- {
- base.Initialize(view, data);
-
- foldInputPort = AddPort(Direction.Input, Port.Capacity.Single, "Input", typeof(IFoldNode));
- foldOutputPort = AddPort(Direction.Output, Port.Capacity.Single, "Out", typeof(IFoldNode));
- }
-
- public IFoldNode GetPreviousFold()
+ object GetValue() => Data switch
{
- var edge = foldInputPort.connections.FirstOrDefault();
- return edge?.output?.node as IFoldNode;
- }
-
- public abstract FoldNodeSerialized Serialize();
-}
+ FloatValueNodeData f => f.value,
+ VectorValueNodeData v => v.value,
+ GameObjectNodeData g => g.output,
+ _ => null
+ };
-public class FloatValueNodeView : FoldNodeView, IValueNode<float>
-{
- FloatValueNodeData DataTyped => (FloatValueNodeData)Data;
+ public T GetTypedValue<T>() => GetValue() is T v ? v : default;
public override void Initialize(FoldGraphView view, FoldNodeData data)
{
base.Initialize(view, data);
-
- AddPort(Direction.Output, Port.Capacity.Multi, "Out", typeof(float));
-
- var field = new FloatField("Value") { value = DataTyped.value };
- field.RegisterValueChangedCallback(evt =>
+ var portType = data switch
{
- graphView.RecordUndo("Edit Float");
- DataTyped.value = evt.newValue;
- graphView.MarkDirty();
- });
- mainContainer.Add(field);
- }
-
- public float GetValue() => DataTyped.value;
-}
-
-public class VectorValueNodeView : FoldNodeView, IValueNode<Vector4>
-{
- VectorValueNodeData DataTyped => (VectorValueNodeData)Data;
-
- public override void Initialize(FoldGraphView view, FoldNodeData data)
- {
- base.Initialize(view, data);
-
- AddPort(Direction.Output, Port.Capacity.Multi, "Out", typeof(Vector4));
-
- var field = new Vector4Field("Value") { value = DataTyped.value };
- field.RegisterValueChangedCallback(evt =>
- {
- graphView.RecordUndo("Edit Vector");
- DataTyped.value = evt.newValue;
- graphView.MarkDirty();
- });
- mainContainer.Add(field);
- }
-
- public Vector4 GetValue() => DataTyped.value;
-}
-
-public class GameObjectNodeView : FoldNodeView, IValueNode<GameObject>
-{
- GameObjectNodeData DataTyped => (GameObjectNodeData)Data;
-
- public override void Initialize(FoldGraphView view, FoldNodeData data)
- {
- base.Initialize(view, data);
-
- AddPort(Direction.Output, Port.Capacity.Multi, "Out", typeof(GameObject));
+ FloatValueNodeData => typeof(float),
+ VectorValueNodeData => typeof(Vector4),
+ GameObjectNodeData => typeof(GameObject),
+ _ => typeof(object)
+ };
+ AddPort(Direction.Output, Port.Capacity.Multi, "Out", portType);
- var field = new ObjectField("Object")
+ VisualElement field = data switch
{
- allowSceneObjects = true,
- objectType = typeof(GameObject),
- value = DataTyped.output
+ FloatValueNodeData f => CreateFloatField(f),
+ VectorValueNodeData v => CreateVectorField(v),
+ GameObjectNodeData g => CreateObjectField(g),
+ _ => null
};
- field.RegisterValueChangedCallback(evt =>
- {
- graphView.RecordUndo("Edit GameObject");
- DataTyped.output = evt.newValue as GameObject;
- graphView.MarkDirty();
- });
- mainContainer.Add(field);
- }
-
- public GameObject GetValue() => DataTyped.output;
-}
-
-public class AxisAlignNodeView : FoldOperationNodeView
-{
- AxisAlignNodeData DataTyped => (AxisAlignNodeData)Data;
-
- Port poPort, ppPort, rPort, tPort;
-
- public override void Initialize(FoldGraphView view, FoldNodeData data)
- {
- base.Initialize(view, data);
- poPort = AddPort(Direction.Input, Port.Capacity.Single, "po", typeof(Vector4));
- ppPort = AddPort(Direction.Input, Port.Capacity.Single, "pp", typeof(Vector4));
- rPort = AddPort(Direction.Input, Port.Capacity.Single, "r", typeof(Vector4));
- tPort = AddPort(Direction.Input, Port.Capacity.Single, "t", typeof(float));
+ if (field != null) extensionContainer.Add(field);
+ RefreshExpandedState();
}
- public override FoldNodeSerialized Serialize()
+ FloatField CreateFloatField(FloatValueNodeData d)
{
- return DataTyped.Serialize(graphView);
+ var f = new FloatField("Value") { value = d.value };
+ f.RegisterValueChangedCallback(e => { graphView.RecordUndo("Edit Float"); d.value = e.newValue; graphView.MarkDirty(); });
+ return f;
}
-}
-public class PlaneToTubeNodeView : FoldOperationNodeView
-{
- PlaneToTubeNodeData DataTyped => (PlaneToTubeNodeData)Data;
-
- public override void Initialize(FoldGraphView view, FoldNodeData data)
+ Vector4Field CreateVectorField(VectorValueNodeData d)
{
- base.Initialize(view, data);
-
- AddPort(Direction.Input, Port.Capacity.Single, "p", typeof(Vector4));
- AddPort(Direction.Input, Port.Capacity.Single, "r", typeof(Vector4));
- AddPort(Direction.Input, Port.Capacity.Single, "s", typeof(Vector4));
- AddPort(Direction.Input, Port.Capacity.Single, "t", typeof(float));
+ var f = new Vector4Field("Value") { value = d.value };
+ f.RegisterValueChangedCallback(e => { graphView.RecordUndo("Edit Vector"); d.value = e.newValue; graphView.MarkDirty(); });
+ return f;
}
- public override FoldNodeSerialized Serialize() => DataTyped.Serialize(graphView);
-}
-
-public class PointAlignNodeView : FoldOperationNodeView
-{
- PointAlignNodeData DataTyped => (PointAlignNodeData)Data;
-
- public override void Initialize(FoldGraphView view, FoldNodeData data)
+ ObjectField CreateObjectField(GameObjectNodeData d)
{
- base.Initialize(view, data);
-
- AddPort(Direction.Input, Port.Capacity.Single, "po", typeof(Vector4));
- AddPort(Direction.Input, Port.Capacity.Single, "pp", typeof(Vector4));
- AddPort(Direction.Input, Port.Capacity.Single, "r", typeof(Vector4));
- AddPort(Direction.Input, Port.Capacity.Single, "t", typeof(float));
+ var f = new ObjectField("Object") { allowSceneObjects = true, objectType = typeof(GameObject), value = d.output };
+ f.RegisterValueChangedCallback(e => { graphView.RecordUndo("Edit GameObject"); d.output = e.newValue as GameObject; graphView.MarkDirty(); });
+ return f;
}
-
- public override FoldNodeSerialized Serialize() => DataTyped.Serialize(graphView);
}
-public class TubeToPlaneNodeView : FoldOperationNodeView
+public class OperationNodeView : FoldNodeView, IFoldNode
{
- TubeToPlaneNodeData DataTyped => (TubeToPlaneNodeData)Data;
+ Port foldInputPort;
public override void Initialize(FoldGraphView view, FoldNodeData data)
{
base.Initialize(view, data);
+ foldInputPort = AddPort(Direction.Input, Port.Capacity.Single, "Input", typeof(IFoldNode));
+ AddPort(Direction.Output, Port.Capacity.Single, "Out", typeof(IFoldNode));
- AddPort(Direction.Input, Port.Capacity.Single, "p", typeof(Vector4));
- AddPort(Direction.Input, Port.Capacity.Single, "r", typeof(Vector4));
- AddPort(Direction.Input, Port.Capacity.Single, "s", typeof(Vector4));
- AddPort(Direction.Input, Port.Capacity.Single, "t", typeof(float));
+ foreach (var (name, type) in ((FoldOperationData)data).GetInputPorts())
+ AddPort(Direction.Input, Port.Capacity.Single, name, type);
}
- public override FoldNodeSerialized Serialize() => DataTyped.Serialize(graphView);
+ public IFoldNode GetPreviousFold() => foldInputPort.connections.FirstOrDefault()?.output?.node as IFoldNode;
+ public FoldNodeSerialized Serialize() => ((FoldOperationData)Data).Serialize(graphView);
}
public class KeyframeNodeView : FoldNodeView
{
- Port gameObjectPort;
- Port foldPort;
- readonly List<string> validationMessages = new();
+ Port gameObjectPort, foldPort;
public override void Initialize(FoldGraphView view, FoldNodeData data)
{
base.Initialize(view, data);
-
gameObjectPort = AddPort(Direction.Input, Port.Capacity.Single, "GameObject", typeof(GameObject));
foldPort = AddPort(Direction.Input, Port.Capacity.Single, "Fold Data", typeof(IFoldNode));
-
- var button = new Button(OnGenerateClick) { text = "Generate Keyframe" };
- mainContainer.Add(button);
- }
-
- GameObject GetTargetObject()
- {
- var edge = gameObjectPort.connections.FirstOrDefault();
- if (edge?.output?.node is IValueNode<GameObject> goProvider)
- return goProvider.GetValue();
- return null;
- }
-
- IFoldNode GetFoldInput()
- {
- var edge = foldPort.connections.FirstOrDefault();
- return edge?.output?.node as IFoldNode;
+ extensionContainer.Add(new Button(OnGenerateClick) { text = "Generate Keyframe" });
+ RefreshExpandedState();
}
void OnGenerateClick()
{
- validationMessages.Clear();
-
- var go = GetTargetObject();
- if (go == null)
- {
- validationMessages.Add("GameObject input missing or empty.");
- ShowValidation();
- return;
- }
+ var goEdge = gameObjectPort.connections.FirstOrDefault();
+ var go = (goEdge?.output?.node as ValueNodeView)?.GetTypedValue<GameObject>();
+ if (go == null) { graphView.ShowNotification("GameObject input missing or empty."); return; }
var renderer = go.GetComponent<MeshRenderer>();
- if (renderer == null)
- {
- validationMessages.Add("GameObject must have a MeshRenderer component.");
- ShowValidation();
- return;
- }
+ if (renderer == null) { graphView.ShowNotification("GameObject must have a MeshRenderer component."); return; }
- var foldNode = GetFoldInput();
- if (foldNode == null)
- {
- validationMessages.Add("Fold input not connected.");
- ShowValidation();
- return;
- }
+ var foldNode = foldPort.connections.FirstOrDefault()?.output?.node as IFoldNode;
+ if (foldNode == null) { graphView.ShowNotification("Fold input not connected."); return; }
var stack = new Stack<IFoldNode>();
- var current = foldNode;
- while (current != null)
- {
- stack.Push(current);
- current = current.GetPreviousFold();
- }
+ for (var cur = foldNode; cur != null; cur = cur.GetPreviousFold())
+ stack.Push(cur);
Undo.RecordObject(renderer, "Generate Keyframe");
var mat = renderer.material;
- const int kNumSlots = 16;
- for (int i = 0; i < kNumSlots; i++)
+ for (int i = 0; i < 16; i++)
{
- string slotPrefix = $"_Vertex_Deformation_Slot_{i}_";
- IFoldNode node = stack.Count > 0 ? stack.Pop() : null;
- FoldNodeSerialized data = node?.Serialize();
-
- bool active = data != null;
- mat.SetFloat(slotPrefix + "Enabled", active ? 1.0f : 0.0f);
- mat.SetInt(slotPrefix + "Opcode", active ? data.opcode : 0);
-
- mat.SetFloat(slotPrefix + "Float_0", active ? data.float0 : 0.0f);
- mat.SetFloat(slotPrefix + "Float_1", active ? data.float1 : 0.0f);
- mat.SetFloat(slotPrefix + "Float_2", active ? data.float2 : 0.0f);
- mat.SetFloat(slotPrefix + "Float_3", active ? data.float3 : 0.0f);
-
- mat.SetVector(slotPrefix + "Vector_0", active ? data.vec0 : Vector4.zero);
- mat.SetVector(slotPrefix + "Vector_1", active ? data.vec1 : Vector4.zero);
- mat.SetVector(slotPrefix + "Vector_2", active ? data.vec2 : Vector4.zero);
- mat.SetVector(slotPrefix + "Vector_3", active ? data.vec3 : Vector4.zero);
+ var prefix = $"_Vertex_Deformation_Slot_{i}_";
+ var node = stack.Count > 0 ? stack.Pop() : null;
+ var s = node?.Serialize();
+ bool on = s != null;
+
+ mat.SetFloat(prefix + "Enabled", on ? 1f : 0f);
+ mat.SetInt(prefix + "Opcode", on ? s.opcode : 0);
+ mat.SetFloat(prefix + "Float_0", on ? s.float0 : 0f);
+ mat.SetFloat(prefix + "Float_1", on ? s.float1 : 0f);
+ mat.SetFloat(prefix + "Float_2", on ? s.float2 : 0f);
+ mat.SetFloat(prefix + "Float_3", on ? s.float3 : 0f);
+ mat.SetVector(prefix + "Vector_0", on ? s.vec0 : Vector4.zero);
+ mat.SetVector(prefix + "Vector_1", on ? s.vec1 : Vector4.zero);
+ mat.SetVector(prefix + "Vector_2", on ? s.vec2 : Vector4.zero);
+ mat.SetVector(prefix + "Vector_3", on ? s.vec3 : Vector4.zero);
}
-
EditorUtility.SetDirty(renderer);
}
-
- void ShowValidation()
- {
- graphView.ShowNotification(string.Join("\n", validationMessages));
- }
}
public class FoldGraphView : GraphView
{
+ static readonly (string label, Type dataType)[] AllNodeOptions =
+ {
+ ("Fold/Axis Align", typeof(AxisAlignNodeData)),
+ ("Fold/Plane to Tube", typeof(PlaneToTubeNodeData)),
+ ("Fold/Point Align", typeof(PointAlignNodeData)),
+ ("Fold/Tube to Plane", typeof(TubeToPlaneNodeData)),
+ ("Value/Float", typeof(FloatValueNodeData)),
+ ("Value/Vector", typeof(VectorValueNodeData)),
+ ("Value/GameObject", typeof(GameObjectNodeData)),
+ ("Output/Keyframe", typeof(KeyframeNodeData)),
+ };
+
readonly FoldWindow window;
public readonly FoldGraph graphAsset;
readonly Dictionary<string, FoldNodeView> nodeLookup = new();
- bool suppressGraphChanges;
readonly FoldEdgeConnectorListener edgeConnectorListener;
+ bool suppressGraphChanges;
public FoldGraphView(FoldWindow owner, FoldGraph graph)
{
@@ -362,76 +223,30 @@ public class FoldGraphView : GraphView
this.AddManipulator(new SelectionDragger());
this.AddManipulator(new RectangleSelector());
- gridBackground = new GridBackground();
- Insert(0, gridBackground);
- gridBackground.StretchToParentSize();
+ var grid = new GridBackground();
+ Insert(0, grid);
+ grid.StretchToParentSize();
graphViewChanged = OnGraphViewChanged;
+ nodeCreationRequest = ctx => ShowSearchWindow(null, ctx.screenMousePosition);
RegisterCallback<KeyDownEvent>(OnKeyDown);
+ RegisterCallback<ContextualMenuPopulateEvent>(evt => { evt.menu.ClearItems(); evt.StopImmediatePropagation(); evt.PreventDefault(); ShowSearchWindow(null, evt.mousePosition); });
Undo.undoRedoPerformed += OnUndoRedo;
Reload();
}
- GridBackground gridBackground;
-
- public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
- {
- return ports.ToList().Where(p =>
- p.node != startPort.node &&
- p.direction != startPort.direction &&
- IsTypeCompatible(startPort.portType, p.portType)).ToList();
- }
-
- bool IsTypeCompatible(Type a, Type b)
- {
- if (a == null || b == null)
- return true;
- return a.IsAssignableFrom(b) || b.IsAssignableFrom(a);
- }
-
- internal void BindEdgeConnector(Port port)
- {
- var field = typeof(Port).GetField("m_EdgeConnector", BindingFlags.Instance | BindingFlags.NonPublic);
- if (field != null)
- {
- if (field.GetValue(port) is IManipulator oldManipulator)
- port.RemoveManipulator(oldManipulator);
- }
+ public override List<Port> GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter) =>
+ ports.Where(p => p.node != startPort.node && p.direction != startPort.direction &&
+ (startPort.portType == null || p.portType == null || startPort.portType.IsAssignableFrom(p.portType) || p.portType.IsAssignableFrom(startPort.portType))).ToList();
- var connector = new EdgeConnector<Edge>(edgeConnectorListener);
- field?.SetValue(port, connector);
- port.AddManipulator(connector);
- }
+ internal void BindEdgeConnector(Port port) => port.AddManipulator(new EdgeConnector<Edge>(edgeConnectorListener));
GraphViewChange OnGraphViewChanged(GraphViewChange change)
{
- if (suppressGraphChanges)
- return change;
-
- if (change.elementsToRemove != null)
- {
- foreach (var element in change.elementsToRemove)
- {
- if (element is Edge e)
- {
- RemoveEdgeData(e);
- }
- else if (element is FoldNodeView node)
- {
- RemoveNode(node);
- }
- }
- }
-
- if (change.edgesToCreate != null)
- {
- foreach (var edge in change.edgesToCreate)
- {
- AddEdgeData(edge);
- }
- }
-
+ if (suppressGraphChanges) return change;
+ change.elementsToRemove?.ForEach(e => { if (e is Edge edge) RemoveEdgeData(edge); else if (e is FoldNodeView node) RemoveNode(node); });
+ change.edgesToCreate?.ForEach(AddEdgeData);
return change;
}
@@ -446,33 +261,17 @@ public class FoldGraphView : GraphView
internal void AddEdgeData(Edge edge)
{
- if (edge.input?.node is not FoldNodeView inputNode ||
- edge.output?.node is not FoldNodeView outputNode)
- return;
-
+ if (edge.input?.node is not FoldNodeView inp || edge.output?.node is not FoldNodeView outp) return;
RecordUndo("Add Edge");
- graphAsset.edges.Add(new FoldEdge
- {
- inputNodeGuid = inputNode.Data.guid,
- inputPortName = edge.input.portName,
- outputNodeGuid = outputNode.Data.guid,
- outputPortName = edge.output.portName
- });
+ graphAsset.edges.Add(new FoldEdge { inputNodeGuid = inp.Data.guid, inputPortName = edge.input.portName, outputNodeGuid = outp.Data.guid, outputPortName = edge.output.portName });
MarkDirty();
}
void RemoveEdgeData(Edge edge)
{
- if (edge.input?.node is not FoldNodeView inputNode ||
- edge.output?.node is not FoldNodeView outputNode)
- return;
-
+ if (edge.input?.node is not FoldNodeView inp || edge.output?.node is not FoldNodeView outp) return;
RecordUndo("Remove Edge");
- graphAsset.edges.RemoveAll(e =>
- e.inputNodeGuid == inputNode.Data.guid &&
- e.outputNodeGuid == outputNode.Data.guid &&
- e.inputPortName == edge.input.portName &&
- e.outputPortName == edge.output.portName);
+ graphAsset.edges.RemoveAll(e => e.inputNodeGuid == inp.Data.guid && e.outputNodeGuid == outp.Data.guid && e.inputPortName == edge.input.portName && e.outputPortName == edge.output.portName);
MarkDirty();
}
@@ -480,350 +279,144 @@ public class FoldGraphView : GraphView
{
suppressGraphChanges = true;
nodeLookup.Clear();
- var removable = graphElements.Where(e => e is Node || e is Edge).ToList();
- DeleteElements(removable);
+ DeleteElements(graphElements.Where(e => e is Node or Edge).ToList());
- foreach (var nodeData in graphAsset.nodes)
+ foreach (var d in graphAsset.nodes) { var n = CreateNode(d); nodeLookup[d.guid] = n; AddElement(n); }
+ foreach (var e in graphAsset.edges)
{
- var node = CreateNode(nodeData);
- nodeLookup[nodeData.guid] = node;
- AddElement(node);
+ if (nodeLookup.TryGetValue(e.outputNodeGuid, out var o) && nodeLookup.TryGetValue(e.inputNodeGuid, out var i))
+ if (o.GetPort(e.outputPortName) is { } op && i.GetPort(e.inputPortName) is { } ip)
+ AddElement(op.ConnectTo(ip));
}
-
- foreach (var edgeData in graphAsset.edges)
- {
- if (!nodeLookup.TryGetValue(edgeData.outputNodeGuid, out var outputNode) ||
- !nodeLookup.TryGetValue(edgeData.inputNodeGuid, out var inputNode))
- continue;
-
- var outputPort = outputNode.GetPort(edgeData.outputPortName);
- var inputPort = inputNode.GetPort(edgeData.inputPortName);
- if (outputPort == null || inputPort == null)
- continue;
-
- var edge = new Edge
- {
- output = outputPort,
- input = inputPort
- };
- edge.input.Connect(edge);
- edge.output.Connect(edge);
- AddElement(edge);
- }
-
suppressGraphChanges = false;
}
void OnKeyDown(KeyDownEvent evt)
{
- bool isDuplicate = (evt.keyCode == KeyCode.D) && (evt.commandKey || evt.ctrlKey);
- if (!isDuplicate)
- return;
-
- evt.StopImmediatePropagation();
- DuplicateSelectedNodes();
+ if (evt.keyCode == KeyCode.D && (evt.commandKey || evt.ctrlKey)) { evt.StopImmediatePropagation(); DuplicateSelectedNodes(); }
}
void DuplicateSelectedNodes()
{
- var selectedNodes = selection.OfType<FoldNodeView>().ToList();
- if (selectedNodes.Count == 0)
- return;
+ var sel = selection.OfType<FoldNodeView>().ToList();
+ if (sel.Count == 0) return;
suppressGraphChanges = true;
RecordUndo("Duplicate Nodes");
- var cloneMap = new Dictionary<string, FoldNodeView>();
- foreach (var original in selectedNodes)
+ var map = new Dictionary<string, FoldNodeView>();
+ foreach (var orig in sel)
{
- var cloneData = CloneNodeData(original.Data);
- cloneData.position += new Vector2(30f, 30f);
- graphAsset.nodes.Add(cloneData);
-
- var cloneView = CreateNode(cloneData);
- cloneMap[original.Data.guid] = cloneView;
- nodeLookup[cloneData.guid] = cloneView;
- AddElement(cloneView);
+ var clone = JsonUtility.FromJson<FoldNodeData>(JsonUtility.ToJson(orig.Data));
+ clone = (FoldNodeData)Activator.CreateInstance(orig.Data.GetType());
+ JsonUtility.FromJsonOverwrite(JsonUtility.ToJson(orig.Data), clone);
+ clone.guid = Guid.NewGuid().ToString();
+ clone.position += new Vector2(30f, 30f);
+ graphAsset.nodes.Add(clone);
+ var v = CreateNode(clone);
+ map[orig.Data.guid] = v;
+ nodeLookup[clone.guid] = v;
+ AddElement(v);
}
- var edgesToClone = graphAsset.edges
- .Where(e => cloneMap.ContainsKey(e.outputNodeGuid) && cloneMap.ContainsKey(e.inputNodeGuid))
- .ToList();
-
- foreach (var edgeData in edgesToClone)
- {
- var outputNode = cloneMap[edgeData.outputNodeGuid];
- var inputNode = cloneMap[edgeData.inputNodeGuid];
- var outputPort = outputNode.GetPort(edgeData.outputPortName);
- var inputPort = inputNode.GetPort(edgeData.inputPortName);
- if (outputPort == null || inputPort == null)
- continue;
-
- var edge = new Edge
- {
- output = outputPort,
- input = inputPort
- };
- edge.input.Connect(edge);
- edge.output.Connect(edge);
- AddElement(edge);
- AddEdgeData(edge);
- }
+ foreach (var e in graphAsset.edges.Where(e => map.ContainsKey(e.outputNodeGuid) && map.ContainsKey(e.inputNodeGuid)))
+ if (map[e.outputNodeGuid].GetPort(e.outputPortName) is { } op && map[e.inputNodeGuid].GetPort(e.inputPortName) is { } ip)
+ { var edge = op.ConnectTo(ip); AddElement(edge); AddEdgeData(edge); }
suppressGraphChanges = false;
-
ClearSelection();
- foreach (var newNode in cloneMap.Values)
- AddToSelection(newNode);
-
+ foreach (var n in map.Values) AddToSelection(n);
MarkDirty();
}
- FoldNodeData CloneNodeData(FoldNodeData data)
- {
- var json = JsonUtility.ToJson(data);
- var clone = (FoldNodeData)Activator.CreateInstance(data.GetType());
- JsonUtility.FromJsonOverwrite(json, clone);
- clone.guid = Guid.NewGuid().ToString();
- return clone;
- }
-
FoldNodeView CreateNode(FoldNodeData data)
{
FoldNodeView view = data switch
{
- FloatValueNodeData => new FloatValueNodeView(),
- VectorValueNodeData => new VectorValueNodeView(),
- GameObjectNodeData => new GameObjectNodeView(),
- AxisAlignNodeData => new AxisAlignNodeView(),
- PlaneToTubeNodeData => new PlaneToTubeNodeView(),
- PointAlignNodeData => new PointAlignNodeView(),
- TubeToPlaneNodeData => new TubeToPlaneNodeView(),
+ FloatValueNodeData or VectorValueNodeData or GameObjectNodeData => new ValueNodeView(),
+ FoldOperationData => new OperationNodeView(),
KeyframeNodeData => new KeyframeNodeView(),
- _ => throw new ArgumentOutOfRangeException(nameof(data), $"Unknown node data type {data.GetType().Name}")
+ _ => throw new ArgumentOutOfRangeException(nameof(data))
};
-
view.Initialize(this, data);
return view;
}
- public T CreateNode<T>(Vector2 position) where T : FoldNodeData, new()
- {
- RecordUndo("Create Node");
- var data = new T { position = position };
- graphAsset.nodes.Add(data);
- var view = CreateNode(data);
- nodeLookup[data.guid] = view;
- AddElement(view);
- MarkDirty();
- return data;
- }
-
internal FoldNodeView CreateNode(Type dataType, Vector2 position)
{
- RecordUndo("Create Node");
+ if (!suppressGraphChanges) RecordUndo("Create Node");
var data = (FoldNodeData)Activator.CreateInstance(dataType);
data.position = position;
graphAsset.nodes.Add(data);
var view = CreateNode(data);
nodeLookup[data.guid] = view;
AddElement(view);
+ view.RefreshExpandedState();
+ view.RefreshPorts();
+ MarkDirty();
return view;
}
- public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
- {
- ShowSearchWindow(null, evt.mousePosition);
- evt.StopPropagation();
- }
-
- public void MarkDirty()
- {
- EditorUtility.SetDirty(graphAsset);
- AssetDatabase.SaveAssets();
- }
-
- public void ShowNodeCreationMenuFromPort(Port startPort, Vector2 worldPosition) => ShowSearchWindow(startPort, worldPosition);
+ public void MarkDirty() => EditorUtility.SetDirty(graphAsset);
+ public void ShowNotification(string msg) => window.ShowNotification(msg);
+ public void ShowNodeCreationMenuFromPort(Port port, Vector2 pos) => ShowSearchWindow(port, pos);
+ public void Dispose() => Undo.undoRedoPerformed -= OnUndoRedo;
+ void OnUndoRedo() => Reload();
+ internal void RecordUndo(string label) { if (!suppressGraphChanges) Undo.RegisterCompleteObjectUndo(graphAsset, label); }
- public void CreateNodeAndConnect(Port startPort, Type dataType, Vector2 graphPosition)
+ public FoldNodeView CreateNodeAndConnect(Port startPort, Type dataType, Vector2 pos)
{
suppressGraphChanges = true;
-
- var view = CreateNode(dataType, graphPosition);
-
- var compatible = GetCompatiblePorts(startPort, null).FirstOrDefault(p => p.node == view);
- if (compatible != null)
+ var view = CreateNode(dataType, pos);
+ var compat = GetCompatiblePorts(startPort, null).FirstOrDefault(p => p.node == view);
+ if (compat != null)
{
- var output = startPort.direction == Direction.Output ? startPort : compatible;
- var input = startPort.direction == Direction.Output ? compatible : startPort;
-
- var edge = new Edge
- {
- output = output,
- input = input
- };
- edge.input.Connect(edge);
- edge.output.Connect(edge);
+ var (o, i) = startPort.direction == Direction.Output ? (startPort, compat) : (compat, startPort);
+ var edge = o.ConnectTo(i);
AddElement(edge);
AddEdgeData(edge);
}
-
suppressGraphChanges = false;
MarkDirty();
+ return view;
}
- IEnumerable<(string label, Type dataType)> GetCompatibleNodeOptions(Port startPort)
- {
- Type type = startPort.portType;
- bool fromOutput = startPort.direction == Direction.Output;
-
- if (type == typeof(float))
- {
- if (fromOutput)
- {
- yield return ("Fold/Axis Align", typeof(AxisAlignNodeData));
- yield return ("Fold/Plane to Tube", typeof(PlaneToTubeNodeData));
- yield return ("Fold/Point Align", typeof(PointAlignNodeData));
- yield return ("Fold/Tube to Plane", typeof(TubeToPlaneNodeData));
- }
- else
- {
- yield return ("Value/Float", typeof(FloatValueNodeData));
- }
- }
- else if (type == typeof(Vector4))
- {
- if (fromOutput)
- {
- yield return ("Fold/Axis Align", typeof(AxisAlignNodeData));
- yield return ("Fold/Plane to Tube", typeof(PlaneToTubeNodeData));
- yield return ("Fold/Point Align", typeof(PointAlignNodeData));
- yield return ("Fold/Tube to Plane", typeof(TubeToPlaneNodeData));
- }
- else
- {
- yield return ("Value/Vector", typeof(VectorValueNodeData));
- }
- }
- else if (type == typeof(GameObject))
- {
- if (fromOutput)
- yield return ("Output/Keyframe", typeof(KeyframeNodeData));
- else
- yield return ("Value/GameObject", typeof(GameObjectNodeData));
- }
- else if (typeof(IFoldNode).IsAssignableFrom(type))
- {
- if (fromOutput)
- {
- yield return ("Fold/Axis Align", typeof(AxisAlignNodeData));
- yield return ("Fold/Plane to Tube", typeof(PlaneToTubeNodeData));
- yield return ("Fold/Point Align", typeof(PointAlignNodeData));
- yield return ("Fold/Tube to Plane", typeof(TubeToPlaneNodeData));
- yield return ("Output/Keyframe", typeof(KeyframeNodeData));
- }
- else
- {
- yield return ("Fold/Axis Align", typeof(AxisAlignNodeData));
- yield return ("Fold/Plane to Tube", typeof(PlaneToTubeNodeData));
- yield return ("Fold/Point Align", typeof(PointAlignNodeData));
- yield return ("Fold/Tube to Plane", typeof(TubeToPlaneNodeData));
- }
- }
- }
-
- IEnumerable<(string label, Type dataType)> GetAllNodeOptions()
- {
- yield return ("Fold/Axis Align", typeof(AxisAlignNodeData));
- yield return ("Fold/Plane to Tube", typeof(PlaneToTubeNodeData));
- yield return ("Fold/Point Align", typeof(PointAlignNodeData));
- yield return ("Fold/Tube to Plane", typeof(TubeToPlaneNodeData));
- yield return ("Value/Float", typeof(FloatValueNodeData));
- yield return ("Value/Vector", typeof(VectorValueNodeData));
- yield return ("Value/GameObject", typeof(GameObjectNodeData));
- yield return ("Output/Keyframe", typeof(KeyframeNodeData));
- }
-
- internal IEnumerable<(string label, Type dataType)> GetNodeOptions(Port startPort)
- {
- return startPort == null ? GetAllNodeOptions() : GetCompatibleNodeOptions(startPort);
- }
-
- public T GetInputValue<T>(string nodeGuid, string portName, T fallback)
- {
- var node = nodeLookup.TryGetValue(nodeGuid, out var found) ? found : null;
- if (node == null) return fallback;
-
- var port = node.GetPort(portName);
- var edge = port?.connections.FirstOrDefault();
- if (edge?.output?.node is IValueNode<T> provider)
- return provider.GetValue();
-
- return fallback;
- }
-
- public void ShowNotification(string message)
- {
- window.ShowNotification(message);
- }
-
- void OnUndoRedo()
- {
- Reload();
- }
-
- public void Dispose()
+ internal IEnumerable<(string label, Type dataType)> GetNodeOptions(Port port)
{
- Undo.undoRedoPerformed -= OnUndoRedo;
+ if (port == null) return AllNodeOptions;
+ var t = port.portType;
+ bool @out = port.direction == Direction.Output;
+ return AllNodeOptions.Where(o =>
+ (t == typeof(float) || t == typeof(Vector4)) ? (@out ? typeof(FoldOperationData).IsAssignableFrom(o.dataType) : (t == typeof(float) ? o.dataType == typeof(FloatValueNodeData) : o.dataType == typeof(VectorValueNodeData))) :
+ t == typeof(GameObject) ? (@out ? o.dataType == typeof(KeyframeNodeData) : o.dataType == typeof(GameObjectNodeData)) :
+ typeof(IFoldNode).IsAssignableFrom(t) && (typeof(FoldOperationData).IsAssignableFrom(o.dataType) || (@out && o.dataType == typeof(KeyframeNodeData))));
}
- internal void RecordUndo(string label)
- {
- if (suppressGraphChanges)
- return;
- Undo.RegisterCompleteObjectUndo(graphAsset, label);
- }
+ public T GetInputValue<T>(string guid, string port, T fallback) =>
+ nodeLookup.TryGetValue(guid, out var n) && n.GetPort(port)?.connections.FirstOrDefault()?.output?.node is ValueNodeView v ? v.GetTypedValue<T>() : fallback;
- void ShowSearchWindow(Port startPort, Vector2 worldPosition)
+ void ShowSearchWindow(Port port, Vector2 pos)
{
- var provider = ScriptableObject.CreateInstance<FoldSearchProvider>();
- provider.Initialize(this, startPort, worldPosition);
- if (!provider.HasEntries())
- return;
-
- SearchWindow.Open(new SearchWindowContext(GUIUtility.GUIToScreenPoint(worldPosition)), provider);
+ var p = ScriptableObject.CreateInstance<FoldSearchProvider>();
+ p.Initialize(this, port, pos);
+ if (p.HasEntries()) SearchWindow.Open(new SearchWindowContext(GUIUtility.GUIToScreenPoint(pos)), p);
}
}
class FoldEdgeConnectorListener : IEdgeConnectorListener
{
readonly FoldGraphView graphView;
+ public FoldEdgeConnectorListener(FoldGraphView gv) => graphView = gv;
- public FoldEdgeConnectorListener(FoldGraphView graphView)
- {
- this.graphView = graphView;
- }
-
- public void OnDropOutsidePort(Edge edge, Vector2 position)
- {
- var startPort = edge.output ?? edge.input;
- if (startPort == null)
- return;
-
- graphView.ShowNodeCreationMenuFromPort(startPort, position);
- }
+ public void OnDropOutsidePort(Edge edge, Vector2 pos) => graphView.ShowNodeCreationMenuFromPort(edge.output ?? edge.input, pos);
public void OnDrop(GraphView gv, Edge edge)
{
- if (edge.input == null || edge.output == null)
- return;
-
+ if (edge.input == null || edge.output == null) return;
edge.input.Connect(edge);
edge.output.Connect(edge);
gv.AddElement(edge);
-
- if (graphView != null)
- graphView.AddEdgeData(edge);
+ graphView?.AddEdgeData(edge);
}
}
@@ -832,50 +425,30 @@ class FoldSearchProvider : ScriptableObject, ISearchWindowProvider
FoldGraphView graphView;
Port startPort;
Vector2 worldPosition;
- List<(string label, Type dataType)> cachedOptions;
-
- public void Initialize(FoldGraphView graphView, Port startPort, Vector2 worldPosition)
- {
- this.graphView = graphView;
- this.startPort = startPort;
- this.worldPosition = worldPosition;
- cachedOptions = graphView.GetNodeOptions(startPort).ToList();
- }
+ List<(string label, Type dataType)> options;
- public bool HasEntries() => cachedOptions != null && cachedOptions.Count > 0;
+ public void Initialize(FoldGraphView gv, Port port, Vector2 pos) { graphView = gv; startPort = port; worldPosition = pos; options = gv.GetNodeOptions(port).ToList(); }
+ public bool HasEntries() => options?.Count > 0;
- public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext context)
+ public List<SearchTreeEntry> CreateSearchTree(SearchWindowContext ctx)
{
- var entries = new List<SearchTreeEntry>
- {
- new SearchTreeGroupEntry(new GUIContent("Create Node"), 0)
- };
-
- foreach (var option in cachedOptions)
+ var entries = new List<SearchTreeEntry> { new SearchTreeGroupEntry(new GUIContent("Create Node"), 0) };
+ foreach (var grp in options.GroupBy(o => o.label.Split('/')[0]).OrderBy(g => g.Key))
{
- entries.Add(new SearchTreeEntry(new GUIContent(option.label))
- {
- level = 1,
- userData = option.dataType
- });
+ entries.Add(new SearchTreeGroupEntry(new GUIContent(grp.Key), 1));
+ foreach (var opt in grp)
+ entries.Add(new SearchTreeEntry(new GUIContent(opt.label.Split('/')[1])) { level = 2, userData = opt.dataType });
}
-
return entries;
}
- public bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context)
+ public bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext ctx)
{
- var dataType = entry.userData as Type;
- if (dataType == null)
- return false;
-
- Vector2 graphPosition = graphView.contentViewContainer.WorldToLocal(worldPosition);
-
- if (startPort != null)
- graphView.CreateNodeAndConnect(startPort, dataType, graphPosition);
- else
- graphView.CreateNode(dataType, graphPosition);
-
+ if (entry.userData is not Type dataType) return false;
+ var pos = graphView.contentViewContainer.WorldToLocal(worldPosition);
+ if (startPort != null) graphView.CreateNodeAndConnect(startPort, dataType, pos);
+ else graphView.CreateNode(dataType, pos);
return true;
}
}
+
diff --git a/Scripts/Fold/Editor/FoldWindow.cs b/Scripts/Fold/Editor/FoldWindow.cs
index 3ba556c..f0f9b42 100644
--- a/Scripts/Fold/Editor/FoldWindow.cs
+++ b/Scripts/Fold/Editor/FoldWindow.cs
@@ -21,7 +21,6 @@ public class FoldWindow : EditorWindow
{
graphAsset = LoadOrCreateGraph();
ConstructGraphView();
- Undo.undoRedoPerformed += OnUndoRedo;
}
void OnDisable()
@@ -34,7 +33,6 @@ public class FoldWindow : EditorWindow
}
AssetDatabase.SaveAssets();
- Undo.undoRedoPerformed -= OnUndoRedo;
}
void OnFocus()
@@ -77,11 +75,6 @@ public class FoldWindow : EditorWindow
rootVisualElement.Add(graphView);
}
- void OnUndoRedo()
- {
- graphView?.Reload();
- }
-
public void ShowNotification(string message)
{
if (string.IsNullOrEmpty(message))