using System; using System.Collections.Generic; using System.Reflection; using UnityEditor; using UnityEngine; public class FoldEditorWindow : EditorWindow { [SerializeField] Material targetMaterial; [SerializeField] Vector2 scrollPos; [SerializeReference] List operations = new(); [SerializeField] List expandedOps = new(); [SerializeField] bool liveUpdate = true; [SerializeField] GameObject targetObject; int frameStep; bool wasInAnimationMode; const string FrameStepPrefKey = "Fold_FrameStep"; [MenuItem("Tools/yum_food/Fold")] static void ShowWindow() { var window = GetWindow("Fold"); window.minSize = new Vector2(400, 300); } void OnEnable() => frameStep = EditorPrefs.GetInt(FrameStepPrefKey, 1); void OnDisable() { EditorPrefs.SetInt(FrameStepPrefKey, frameStep); ClearPropertyBlock(); } void OnGUI() { bool inAnimMode = AnimationMode.InAnimationMode(); if (wasInAnimationMode && !inAnimMode) ClearPropertyBlock(); wasInAnimationMode = inAnimMode; EditorGUILayout.Space(5); DrawHeader(); EditorGUILayout.Space(10); if (targetMaterial == null) { EditorGUILayout.HelpBox("Select a material to build deformation pipelines", MessageType.Info); return; } DrawToolbar(); EditorGUILayout.Space(5); EditorGUI.BeginChangeCheck(); scrollPos = EditorGUILayout.BeginScrollView(scrollPos); DrawOperationsList(); EditorGUILayout.EndScrollView(); if (EditorGUI.EndChangeCheck() && liveUpdate && targetMaterial != null) ApplyToMaterial(); EditorGUILayout.Space(5); DrawFooter(); } void DrawHeader() { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Target Material", EditorStyles.boldLabel, GUILayout.Width(100)); var newMat = EditorGUILayout.ObjectField(targetMaterial, typeof(Material), false) as Material; if (newMat != targetMaterial) { targetMaterial = newMat; if (targetMaterial != null) LoadFromMaterial(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Target Object", EditorStyles.boldLabel, GUILayout.Width(100)); targetObject = EditorGUILayout.ObjectField(targetObject, typeof(GameObject), true) as GameObject; EditorGUILayout.EndHorizontal(); } void DrawToolbar() { EditorGUILayout.BeginHorizontal(EditorStyles.toolbar); bool pipelineFull = operations.Count >= 16; using (new EditorGUI.DisabledScope(pipelineFull)) { var content = pipelineFull ? new GUIContent("Add Operation", "Pipeline full (16/16)") : new GUIContent("Add Operation"); if (GUILayout.Button(content, EditorStyles.toolbarDropDown, GUILayout.Width(120))) ShowAddOperationMenu(); } GUILayout.FlexibleSpace(); liveUpdate = GUILayout.Toggle(liveUpdate, "Live", EditorStyles.toolbarButton, GUILayout.Width(40)); EditorGUILayout.EndHorizontal(); } void DrawOperationsList() { if (operations.Count == 0) { EditorGUILayout.HelpBox("No operations. Click 'Add Operation' to begin.", MessageType.Info); return; } for (int i = 0; i < operations.Count; i++) { DrawOperation(i); EditorGUILayout.Space(3); } } void DrawOperation(int index) { var op = operations[index]; bool isExpanded = expandedOps.Contains(index); var bgColor = isExpanded ? new Color(0.3f, 0.5f, 0.8f, 0.3f) : new Color(0.2f, 0.2f, 0.2f, 0.3f); EditorGUILayout.BeginVertical(GUI.skin.box); GUI.backgroundColor = bgColor; // Header with drag handle and delete button EditorGUILayout.BeginHorizontal(); // Drag handle GUILayout.Label($"#{index}", GUILayout.Width(30)); // Operation name (clickable to expand/collapse) if (GUILayout.Button(op.GetDisplayName(), EditorStyles.boldLabel)) { if (isExpanded) expandedOps.Remove(index); else expandedOps.Add(index); } GUILayout.FlexibleSpace(); // Move up/down GUI.enabled = index > 0; if (GUILayout.Button("▲", GUILayout.Width(25))) { operations.RemoveAt(index); operations.Insert(index - 1, op); if (expandedOps.Contains(index)) { expandedOps.Remove(index); expandedOps.Add(index - 1); } } GUI.enabled = index < operations.Count - 1; if (GUILayout.Button("▼", GUILayout.Width(25))) { operations.RemoveAt(index); operations.Insert(index + 1, op); if (expandedOps.Contains(index)) { expandedOps.Remove(index); expandedOps.Add(index + 1); } } GUI.enabled = true; // Delete button if (GUILayout.Button("×", GUILayout.Width(25))) { operations.RemoveAt(index); expandedOps.Remove(index); // Adjust indices in expandedOps for (int i = 0; i < expandedOps.Count; i++) { if (expandedOps[i] > index) expandedOps[i]--; } } EditorGUILayout.EndHorizontal(); // Draw parameters when expanded if (isExpanded) { EditorGUILayout.Space(5); EditorGUI.indentLevel++; op.DrawParameters(); EditorGUI.indentLevel--; } GUI.backgroundColor = Color.white; EditorGUILayout.EndVertical(); } void DrawFooter() { EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Create Keyframe", GUILayout.Height(30))) ApplyToMaterial(recordKeyframes: true); if (GUILayout.Button("Delete Keyframe", GUILayout.Height(30), GUILayout.Width(120))) DeleteKeyframeAtCurrentTime(); if (GUILayout.Button("Clear All", GUILayout.Height(30), GUILayout.Width(80))) { if (EditorUtility.DisplayDialog("Clear All Operations", "Remove all operations from the pipeline?", "Clear", "Cancel")) { operations.Clear(); expandedOps.Clear(); } } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(3); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("<", GUILayout.Width(30), GUILayout.Height(20))) AdvancePlayhead(-frameStep); frameStep = EditorGUILayout.IntField(frameStep, GUILayout.Width(40)); if (frameStep < 1) frameStep = 1; if (GUILayout.Button(">", GUILayout.Width(30), GUILayout.Height(20))) AdvancePlayhead(frameStep); GUILayout.FlexibleSpace(); EditorGUILayout.LabelField($"Operations: {operations.Count}/16", EditorStyles.miniLabel); EditorGUILayout.EndHorizontal(); } void ShowAddOperationMenu() { var menu = new GenericMenu(); menu.AddItem(new GUIContent("Tube to Plane"), false, () => AddOperation(new TubeToPlaneOp())); menu.AddItem(new GUIContent("Plane to Tube"), false, () => AddOperation(new PlaneToTubeOp())); menu.AddItem(new GUIContent("Plane to Hemi-Octahedron"), false, () => AddOperation(new PlaneToHemiOctahedronOp())); menu.AddItem(new GUIContent("Hemi-Octahedron to Plane"), false, () => AddOperation(new HemiOctahedronToPlaneOp())); menu.AddItem(new GUIContent("Plane to Octahedron"), false, () => AddOperation(new PlaneToOctahedronOp())); menu.AddItem(new GUIContent("Octahedron to Plane"), false, () => AddOperation(new OctahedronToPlaneOp())); menu.AddSeparator(""); menu.AddItem(new GUIContent("Point Align"), false, () => AddOperation(new PointAlignOp())); menu.AddItem(new GUIContent("Axis Align"), false, () => AddOperation(new AxisAlignOp())); menu.AddSeparator(""); menu.AddItem(new GUIContent("Scale"), false, () => AddOperation(new ScaleOp())); menu.AddItem(new GUIContent("Translate"), false, () => AddOperation(new TranslateOp())); menu.AddItem(new GUIContent("Rotate"), false, () => AddOperation(new RotateOp())); menu.AddItem(new GUIContent("Norm Conversion"), false, () => AddOperation(new NormConversionOp())); menu.AddItem(new GUIContent("Seal"), false, () => AddOperation(new SealOp())); menu.AddSeparator(""); menu.AddItem(new GUIContent("Sine Waves"), false, () => AddOperation(new SineWavesOp())); menu.AddItem(new GUIContent("FBM"), false, () => AddOperation(new FBMOp())); menu.ShowAsContext(); } void AddOperation(DeformOperation op) { if (operations.Count >= 16) return; operations.Add(op); expandedOps.Add(operations.Count - 1); } void ApplyToMaterial(bool recordKeyframes = false) { var builder = FoldPipelineBuilder.Create().For(targetMaterial); foreach (var op in operations) op.ApplyTo(builder); builder.Apply(); if (AnimationMode.InAnimationMode()) ApplyPropertyBlock(builder); if (recordKeyframes) RecordAnimationKeyframes(builder); } void ApplyPropertyBlock(FoldPipelineBuilder builder) { if (targetObject == null) return; var renderer = targetObject.GetComponent(); if (renderer == null) return; var mpb = new MaterialPropertyBlock(); renderer.GetPropertyBlock(mpb); mpb.SetFloat("_Vertex_Deformation_Enabled", 1f); for (int i = 0; i < 16; i++) { var slot = builder.GetSlot(i); var prefix = $"_Vertex_Deformation_Slot_{i}_"; mpb.SetFloat(prefix + "Enabled", slot != null ? 1f : 0f); mpb.SetInteger(prefix + "Opcode", slot?.opcode ?? 0); mpb.SetFloat(prefix + "Float_0", slot?.float0 ?? 0f); mpb.SetFloat(prefix + "Float_1", slot?.float1 ?? 0f); mpb.SetFloat(prefix + "Float_2", slot?.float2 ?? 0f); mpb.SetFloat(prefix + "Float_3", slot?.float3 ?? 0f); mpb.SetVector(prefix + "Vector_0", slot?.vec0 ?? Vector4.zero); mpb.SetVector(prefix + "Vector_1", slot?.vec1 ?? Vector4.zero); mpb.SetVector(prefix + "Vector_2", slot?.vec2 ?? Vector4.zero); mpb.SetVector(prefix + "Vector_3", slot?.vec3 ?? Vector4.zero); } renderer.SetPropertyBlock(mpb); } void ClearPropertyBlock() { if (targetObject == null) return; var renderer = targetObject.GetComponent(); if (renderer != null) renderer.SetPropertyBlock(null); } #region Animation Recording void RecordAnimationKeyframes(FoldPipelineBuilder builder) { if (targetObject == null || !AnimationMode.InAnimationMode()) return; var renderer = targetObject.GetComponent(); if (renderer == null) return; if (!TryGetAnimationWindowState(out var clip, out float time)) return; var animator = targetObject.GetComponentInParent(); string path = animator != null ? AnimationUtility.CalculateTransformPath(renderer.transform, animator.transform) : ""; SetFloatKey(clip, path, "material._Vertex_Deformation_Enabled", 1f, time); for (int i = 0; i < 16; i++) { var slot = builder.GetSlot(i); var prefix = $"material._Vertex_Deformation_Slot_{i}_"; SetFloatKey(clip, path, prefix + "Enabled", slot != null ? 1f : 0f, time); SetDiscreteKey(clip, path, prefix + "Opcode", slot?.opcode ?? 0, time); SetFloatKey(clip, path, prefix + "Float_0", slot?.float0 ?? 0f, time); SetFloatKey(clip, path, prefix + "Float_1", slot?.float1 ?? 0f, time); SetFloatKey(clip, path, prefix + "Float_2", slot?.float2 ?? 0f, time); SetFloatKey(clip, path, prefix + "Float_3", slot?.float3 ?? 0f, time); SetVectorKey(clip, path, prefix + "Vector_0", slot?.vec0 ?? Vector4.zero, time); SetVectorKey(clip, path, prefix + "Vector_1", slot?.vec1 ?? Vector4.zero, time); SetVectorKey(clip, path, prefix + "Vector_2", slot?.vec2 ?? Vector4.zero, time); SetVectorKey(clip, path, prefix + "Vector_3", slot?.vec3 ?? Vector4.zero, time); } } void DeleteKeyframeAtCurrentTime() { if (targetObject == null || !AnimationMode.InAnimationMode()) { Debug.LogWarning("Fold: No target object set or Animation window is not recording."); return; } var renderer = targetObject.GetComponent(); if (renderer == null) return; if (!TryGetAnimationWindowState(out var clip, out float time)) return; var animator = targetObject.GetComponentInParent(); string path = animator != null ? AnimationUtility.CalculateTransformPath(renderer.transform, animator.transform) : ""; Undo.RecordObject(clip, "Delete Fold Keyframe"); foreach (var binding in AnimationUtility.GetCurveBindings(clip)) { if (binding.type != typeof(Renderer)) continue; if (binding.path != path) continue; if (!binding.propertyName.StartsWith("material._Vertex_Deformation_")) continue; var curve = AnimationUtility.GetEditorCurve(clip, binding); if (curve == null) continue; bool removed = false; for (int i = curve.length - 1; i >= 0; i--) { if (Mathf.Approximately(curve.keys[i].time, time)) { curve.RemoveKey(i); removed = true; } } if (removed) { if (curve.length == 0) AnimationUtility.SetEditorCurve(clip, binding, null); else AnimationUtility.SetEditorCurve(clip, binding, curve); } } } static void SetFloatKey(AnimationClip clip, string path, string prop, float value, float time) { var binding = EditorCurveBinding.FloatCurve(path, typeof(Renderer), prop); var curve = AnimationUtility.GetEditorCurve(clip, binding) ?? new AnimationCurve(); AddOrReplaceKey(curve, time, value); AnimationUtility.SetEditorCurve(clip, binding, curve); } static void SetDiscreteKey(AnimationClip clip, string path, string prop, int value, float time) { var binding = EditorCurveBinding.DiscreteCurve(path, typeof(Renderer), prop); var curve = AnimationUtility.GetEditorCurve(clip, binding) ?? new AnimationCurve(); AddOrReplaceKey(curve, time, value); AnimationUtility.SetEditorCurve(clip, binding, curve); } static void SetVectorKey(AnimationClip clip, string path, string prop, Vector4 v, float time) { SetFloatKey(clip, path, prop + ".x", v.x, time); SetFloatKey(clip, path, prop + ".y", v.y, time); SetFloatKey(clip, path, prop + ".z", v.z, time); SetFloatKey(clip, path, prop + ".w", v.w, time); } static void AddOrReplaceKey(AnimationCurve curve, float time, float value) { for (int i = curve.length - 1; i >= 0; i--) if (Mathf.Approximately(curve.keys[i].time, time)) curve.RemoveKey(i); curve.AddKey(new Keyframe(time, value)); } void AdvancePlayhead(int frames) { if (!TryGetAnimationWindowState(out var clip, out float time)) return; float frameDuration = 1f / clip.frameRate; SetAnimationWindowTime(time + frames * frameDuration); } static void SetAnimationWindowTime(float time) { var windowType = typeof(EditorWindow).Assembly.GetType("UnityEditor.AnimationWindow"); if (windowType == null) return; var windows = Resources.FindObjectsOfTypeAll(windowType); if (windows.Length == 0) return; var timeProp = windowType.GetProperty("time", BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); timeProp?.SetValue(windows[0], time); } static bool TryGetAnimationWindowState(out AnimationClip clip, out float time) { clip = null; time = 0f; var windowType = typeof(EditorWindow).Assembly.GetType("UnityEditor.AnimationWindow"); if (windowType == null) return false; var windows = Resources.FindObjectsOfTypeAll(windowType); if (windows.Length == 0) return false; var window = windows[0]; const BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance; var clipProp = windowType.GetProperty("animationClip", flags); var timeProp = windowType.GetProperty("time", flags); if (clipProp == null || timeProp == null) return false; clip = clipProp.GetValue(window) as AnimationClip; time = (float)timeProp.GetValue(window); return clip != null; } #endregion void LoadFromMaterial() { operations.Clear(); expandedOps.Clear(); for (int i = 0; i < 16; i++) { var prefix = $"_Vertex_Deformation_Slot_{i}_"; if (targetMaterial.GetFloat(prefix + "Enabled") < 0.5f) break; int opcode = targetMaterial.GetInteger(prefix + "Opcode"); if (opcode == FoldPipelineBuilder.Opcodes.None) break; var slot = new FoldSlot { opcode = opcode, float0 = targetMaterial.GetFloat(prefix + "Float_0"), float1 = targetMaterial.GetFloat(prefix + "Float_1"), float2 = targetMaterial.GetFloat(prefix + "Float_2"), float3 = targetMaterial.GetFloat(prefix + "Float_3"), vec0 = targetMaterial.GetVector(prefix + "Vector_0"), vec1 = targetMaterial.GetVector(prefix + "Vector_1"), vec2 = targetMaterial.GetVector(prefix + "Vector_2"), vec3 = targetMaterial.GetVector(prefix + "Vector_3"), }; DeformOperation op = opcode switch { FoldPipelineBuilder.Opcodes.TubeToPlane => new TubeToPlaneOp(slot), FoldPipelineBuilder.Opcodes.PlaneToTube => new PlaneToTubeOp(slot), FoldPipelineBuilder.Opcodes.PlaneToHemiOctahedron => new PlaneToHemiOctahedronOp(slot), FoldPipelineBuilder.Opcodes.HemiOctahedronToPlane => new HemiOctahedronToPlaneOp(slot), FoldPipelineBuilder.Opcodes.Scale => new ScaleOp(slot), FoldPipelineBuilder.Opcodes.Translate => new TranslateOp(slot), FoldPipelineBuilder.Opcodes.PlaneToOctahedron => new PlaneToOctahedronOp(slot), FoldPipelineBuilder.Opcodes.OctahedronToPlane => new OctahedronToPlaneOp(slot), FoldPipelineBuilder.Opcodes.Rotate => new RotateOp(slot), FoldPipelineBuilder.Opcodes.PointAlign => new PointAlignOp(slot), FoldPipelineBuilder.Opcodes.AxisAlign => new AxisAlignOp(slot), FoldPipelineBuilder.Opcodes.NormConversion => new NormConversionOp(slot), FoldPipelineBuilder.Opcodes.Seal => new SealOp(slot), FoldPipelineBuilder.Opcodes.SineWaves => new SineWavesOp(), FoldPipelineBuilder.Opcodes.FBM => new FBMOp(), _ => null }; if (op == null) break; operations.Add(op); } } } // Base class for deformation operations [System.Serializable] public abstract class DeformOperation { public abstract string GetDisplayName(); public abstract void DrawParameters(); public abstract void ApplyTo(FoldPipelineBuilder builder); } [System.Serializable] public class TubeToPlaneOp : DeformOperation { public Vector3 p = Vector3.zero; public Vector3 r = Vector3.right; public Vector3 s = Vector3.forward; public float t = 1f; public TubeToPlaneOp() { } public TubeToPlaneOp(FoldSlot slot) { p = slot.vec0; r = slot.vec1; s = slot.vec2; t = slot.float0; } public override string GetDisplayName() => "Tube to Plane"; public override void DrawParameters() { p = EditorGUILayout.Vector3Field("Origin (p)", p); r = EditorGUILayout.Vector3Field("R Axis", r); s = EditorGUILayout.Vector3Field("S Axis", s); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.TubeToPlane(p, r, s, t); } [System.Serializable] public class PlaneToTubeOp : DeformOperation { public Vector3 p = Vector3.zero; public Vector3 r = Vector3.right; public Vector3 s = Vector3.forward; public float t = 1f; public PlaneToTubeOp() { } public PlaneToTubeOp(FoldSlot slot) { p = slot.vec0; r = slot.vec1; s = slot.vec2; t = slot.float0; } public override string GetDisplayName() => "Plane to Tube"; public override void DrawParameters() { p = EditorGUILayout.Vector3Field("Origin (p)", p); r = EditorGUILayout.Vector3Field("R Axis", r); s = EditorGUILayout.Vector3Field("S Axis", s); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.PlaneToTube(p, r, s, t); } [System.Serializable] public class PlaneToHemiOctahedronOp : DeformOperation { public Vector3 p = Vector3.zero; public Vector3 r = Vector3.right; public Vector3 s = Vector3.forward; public float t = 1f; public PlaneToHemiOctahedronOp() { } public PlaneToHemiOctahedronOp(FoldSlot slot) { p = slot.vec0; r = slot.vec1; s = slot.vec2; t = slot.float0; } public override string GetDisplayName() => "Plane to Hemi-Octahedron"; public override void DrawParameters() { p = EditorGUILayout.Vector3Field("Origin (p)", p); r = EditorGUILayout.Vector3Field("R Axis", r); s = EditorGUILayout.Vector3Field("S Axis", s); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.PlaneToHemiOctahedron(p, r, s, t); } [System.Serializable] public class ScaleOp : DeformOperation { public Vector3 k = Vector3.one; public float t = 1f; public ScaleOp() { } public ScaleOp(FoldSlot slot) { k = slot.vec0; t = slot.float0; } public override string GetDisplayName() => "Scale"; public override void DrawParameters() { k = EditorGUILayout.Vector3Field("Scale", k); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.Scale(k, t); } [System.Serializable] public class TranslateOp : DeformOperation { public Vector3 offset = Vector3.zero; public float t = 1f; public TranslateOp() { } public TranslateOp(FoldSlot slot) { offset = slot.vec0; t = slot.float0; } public override string GetDisplayName() => "Translate"; public override void DrawParameters() { offset = EditorGUILayout.Vector3Field("Offset", offset); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.Translate(offset, t); } [System.Serializable] public class RotateOp : DeformOperation { public Vector3 center = Vector3.zero; public Vector3 axis = Vector3.up; public float angleDeg = 90f; public float t = 1f; public RotateOp() { } public RotateOp(FoldSlot slot) { center = slot.vec0; axis = slot.vec1; angleDeg = slot.float0 * Mathf.Rad2Deg; t = slot.float1; } public override string GetDisplayName() => "Rotate"; public override void DrawParameters() { center = EditorGUILayout.Vector3Field("Center", center); axis = EditorGUILayout.Vector3Field("Axis", axis); angleDeg = EditorGUILayout.Slider("Angle", angleDeg, 0f, 360f); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.Rotate(center, axis, angleDeg * Mathf.Deg2Rad, t); } [System.Serializable] public class HemiOctahedronToPlaneOp : DeformOperation { public Vector3 p = Vector3.zero; public Vector3 r = Vector3.right; public Vector3 s = Vector3.forward; public float t = 1f; public HemiOctahedronToPlaneOp() { } public HemiOctahedronToPlaneOp(FoldSlot slot) { p = slot.vec0; r = slot.vec1; s = slot.vec2; t = slot.float0; } public override string GetDisplayName() => "Hemi-Octahedron to Plane"; public override void DrawParameters() { p = EditorGUILayout.Vector3Field("Origin (p)", p); r = EditorGUILayout.Vector3Field("R Axis", r); s = EditorGUILayout.Vector3Field("S Axis", s); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.HemiOctahedronToPlane(p, r, s, t); } [System.Serializable] public class PlaneToOctahedronOp : DeformOperation { public Vector3 p = Vector3.zero; public Vector3 r = Vector3.right; public Vector3 s = Vector3.forward; public float t = 1f; public PlaneToOctahedronOp() { } public PlaneToOctahedronOp(FoldSlot slot) { p = slot.vec0; r = slot.vec1; s = slot.vec2; t = slot.float0; } public override string GetDisplayName() => "Plane to Octahedron"; public override void DrawParameters() { p = EditorGUILayout.Vector3Field("Origin (p)", p); r = EditorGUILayout.Vector3Field("R Axis", r); s = EditorGUILayout.Vector3Field("S Axis", s); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.PlaneToOctahedron(p, r, s, t); } [System.Serializable] public class OctahedronToPlaneOp : DeformOperation { public Vector3 p = Vector3.zero; public Vector3 r = Vector3.right; public Vector3 s = Vector3.forward; public float t = 1f; public OctahedronToPlaneOp() { } public OctahedronToPlaneOp(FoldSlot slot) { p = slot.vec0; r = slot.vec1; s = slot.vec2; t = slot.float0; } public override string GetDisplayName() => "Octahedron to Plane"; public override void DrawParameters() { p = EditorGUILayout.Vector3Field("Origin (p)", p); r = EditorGUILayout.Vector3Field("R Axis", r); s = EditorGUILayout.Vector3Field("S Axis", s); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.OctahedronToPlane(p, r, s, t); } [System.Serializable] public class PointAlignOp : DeformOperation { public Vector3 po = Vector3.zero; public Vector3 pp = Vector3.up; public Vector3 r = Vector3.right; public float t = 1f; public PointAlignOp() { } public PointAlignOp(FoldSlot slot) { po = slot.vec0; pp = slot.vec1; r = slot.vec2; t = slot.float0; } public override string GetDisplayName() => "Point Align"; public override void DrawParameters() { po = EditorGUILayout.Vector3Field("Origin Point (po)", po); pp = EditorGUILayout.Vector3Field("Target Point (pp)", pp); r = EditorGUILayout.Vector3Field("Rotation Axis (r)", r); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.PointAlign(po, pp, r, t); } [System.Serializable] public class AxisAlignOp : DeformOperation { public Vector3 po = Vector3.zero; public Vector3 pp = Vector3.up; public Vector3 r = Vector3.right; public float t = 1f; public AxisAlignOp() { } public AxisAlignOp(FoldSlot slot) { po = slot.vec0; pp = slot.vec1; r = slot.vec2; t = slot.float0; } public override string GetDisplayName() => "Axis Align"; public override void DrawParameters() { po = EditorGUILayout.Vector3Field("Origin Point (po)", po); pp = EditorGUILayout.Vector3Field("Target Point (pp)", pp); r = EditorGUILayout.Vector3Field("Rotation Axis (r)", r); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.AxisAlign(po, pp, r, t); } [System.Serializable] public class NormConversionOp : DeformOperation { public float inputK = 2f; public float outputK = 1f; public float t = 1f; public NormConversionOp() { } public NormConversionOp(FoldSlot slot) { inputK = slot.float0; outputK = slot.float1; t = slot.float2; } public override string GetDisplayName() => $"Norm Conversion (L{FormatNorm(inputK)}→L{FormatNorm(outputK)})"; string FormatNorm(float k) { if (float.IsPositiveInfinity(k)) return "∞"; return k.ToString("F1"); } public override void DrawParameters() { EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Input Norm (k)", GUILayout.Width(120)); inputK = EditorGUILayout.FloatField(inputK); if (GUILayout.Button("L∞", GUILayout.Width(30))) inputK = float.PositiveInfinity; EditorGUILayout.EndHorizontal(); EditorGUILayout.BeginHorizontal(); EditorGUILayout.LabelField("Output Norm (k)", GUILayout.Width(120)); outputK = EditorGUILayout.FloatField(outputK); if (GUILayout.Button("L∞", GUILayout.Width(30))) outputK = float.PositiveInfinity; EditorGUILayout.EndHorizontal(); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); EditorGUILayout.HelpBox( "Common: L1=diamond, L2=sphere, L∞=cube", MessageType.Info); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.NormConversion(inputK, outputK, t); } [System.Serializable] public class SealOp : DeformOperation { public float A = 0.1f; public float k = 2f; public float st = 0.8f; public float t = 1f; public SealOp() { } public SealOp(FoldSlot slot) { A = slot.float0; k = slot.float1; st = slot.float2; t = slot.float3; } public override string GetDisplayName() => "Seal"; public override void DrawParameters() { A = EditorGUILayout.FloatField("Amplitude (A)", A); k = EditorGUILayout.FloatField("Smoothness (k)", k); st = EditorGUILayout.FloatField("Scale (st)", st); t = EditorGUILayout.Slider("Interpolation (t)", t, 0f, 1f); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.Seal(A, k, st, t); } [System.Serializable] public class SineWavesOp : DeformOperation { public override string GetDisplayName() => "Sine Waves"; public override void DrawParameters() { EditorGUILayout.HelpBox("Sine waves use shader-side parameters", MessageType.Info); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.SineWaves(); } [System.Serializable] public class FBMOp : DeformOperation { public override string GetDisplayName() => "FBM (Fractal Brownian Motion)"; public override void DrawParameters() { EditorGUILayout.HelpBox("FBM uses shader-side parameters", MessageType.Info); } public override void ApplyTo(FoldPipelineBuilder builder) => builder.FBM(); }