Editor/Core/Wizard.cs
#nullable enable annotations
namespace HumanoidRigger;
using Vector3 = System.Numerics.Vector3;
// Preserve previous values so editor hot reload does not reinterpret an open session.
public enum WizardStep { Import=0,Body=1,LeftHand=2,RightHand=3,Generating=4,Validation=5,Finish=6,Centerline=7 }
public sealed class Wizard
{
public WizardStep Step {get;private set;}=WizardStep.Import;
public ImportedCharacter? Character {get;private set;}
public Anatomy? Anatomy {get;private set;}
public GeneratedRig? Rig {get;private set;}
public CharacterPose RequestedPose {get;private set;}=CharacterPose.Auto;
public RigProfile Profile {get;private set;}=Profiles.BuiltIn.First(p=>p.Id=="sbox-human");
public IHandRefiner? HandRefiner {get;set;}
bool centerlineChanged;
public long Revision {get;private set;}
public int? LeftFingerCount {get;private set;}
public int? RightFingerCount {get;private set;}
public bool FingerCountsConfirmed {get;private set;}
public void SetFingerCounts(int? left,int? right)
{
if(left is <0 or >5||right is <0 or >5)throw new ArgumentOutOfRangeException("Finger counts must be between zero and five.");
if(left==LeftFingerCount&&right==RightFingerCount){FingerCountsConfirmed=true;return;}
LeftFingerCount=left;RightFingerCount=right;FingerCountsConfirmed=true;Invalidate();
}
long preparedFromRevision=-1;
WizardStep preparedFromStep;
/// <summary>Background analysis owns a copy of mutable landmarks; the live viewport stays stable.</summary>
public Wizard CopyForContinuation()=>new()
{
Character=Character,Anatomy=Anatomy?.Copy(),Rig=Rig,RequestedPose=RequestedPose,Profile=Profile,HandRefiner=HandRefiner,
Step=Step,centerlineChanged=centerlineChanged,Revision=Revision,preparedFromRevision=Revision,preparedFromStep=Step,
LeftFingerCount=LeftFingerCount,RightFingerCount=RightFingerCount,FingerCountsConfirmed=FingerCountsConfirmed
};
public bool CanAccept(Wizard prepared)=>prepared.preparedFromRevision==Revision&&prepared.preparedFromStep==Step&&prepared.Character==Character&&prepared.Profile==Profile;
public void AcceptContinuation(Wizard prepared)
{
if(!CanAccept(prepared)||prepared.Revision!=prepared.preparedFromRevision+1)throw new InvalidOperationException("Prepared rigging results are out of date.");
Anatomy=prepared.Anatomy;Rig=prepared.Rig;Step=prepared.Step;centerlineChanged=prepared.centerlineChanged;Revision++;
}
public void Import(string path)=>Load(ModelImporter.Import(path));
public void Load(ImportedCharacter model)
{
// Detection succeeds before replacing the previous session.
var anatomy=BodyDetector.Detect(model);Character=model;Anatomy=anatomy;RequestedPose=CharacterPose.Auto;Rig=null;centerlineChanged=false;Step=WizardStep.Centerline;LeftFingerCount=null;RightFingerCount=null;FingerCountsConfirmed=false;Revision++;
}
public void SetProfile(RigProfile profile){profile.Validate();Profile=profile;Invalidate();}
public void Reset(CharacterPose pose=CharacterPose.Auto)
{
if(Character is null)throw new InvalidOperationException("No character loaded.");
Anatomy=BodyDetector.Detect(Character,pose);RequestedPose=pose;Rig=null;centerlineChanged=false;Step=WizardStep.Centerline;Revision++;
}
public void Correct(string role,Vector3 position){Anatomy!.Correct(role,position);Invalidate();}
public void CorrectCenterline(float x)
{
if(Step!=WizardStep.Centerline||Anatomy is null)throw new InvalidOperationException("Open the centerline step first.");
if(!float.IsFinite(x))throw new ArgumentException("Invalid centerline position.");
Anatomy.SymmetryPlaneX=x;Anatomy.CenterlineCorrected=true;centerlineChanged=true;Rig=null;Revision++;
}
void Invalidate(){Rig=null;if(Step is WizardStep.Generating or WizardStep.Validation or WizardStep.Finish)Step=WizardStep.Body;Revision++;}
public void Continue()
{
if(Character is null||Anatomy is null)throw new InvalidOperationException("Import a character first.");
switch(Step)
{
case WizardStep.Centerline:
if(centerlineChanged)
{
var fitted=BodyDetector.Detect(Character,RequestedPose,Anatomy.SymmetryPlaneX);fitted.CenterlineCorrected=true;
foreach(var point in Anatomy.Points.Values.Where(p=>p.Corrected))fitted.Points[point.Role]=point;
Anatomy=fitted;centerlineChanged=false;
}
Step=WizardStep.Body;break;
case WizardStep.Body:DetectHand("L");Step=WizardStep.LeftHand;break;
case WizardStep.LeftHand:DetectHand("R");Step=WizardStep.RightHand;break;
case WizardStep.RightHand:
Step=WizardStep.Generating;
try{Rig=SkeletonSolver.Fit(Character,Anatomy,Profile);Anatomy=Rig.Anatomy??Anatomy;Step=Rig.Report.Passed ? WizardStep.Finish : WizardStep.Validation;}
catch{Step=WizardStep.RightHand;throw;}
break;
default:throw new InvalidOperationException("Cannot continue from this step.");
}
Revision++;
}
void DetectHand(string side)
{
foreach(var role in Anatomy!.GeometricHandPoints.Keys.Where(r=>r.EndsWith("."+side)).ToArray())Anatomy.GeometricHandPoints.Remove(role);
HandDetector.Detect(Character!,Anatomy!,side,expectedCount:side=="L"?LeftFingerCount:RightFingerCount);
if(HandRefiner is null)return;
var geometry=Anatomy.Points.Values.Where(p=>p.Role.EndsWith("."+side)&&Profiles.Fingers.Any(f=>p.Role.StartsWith(f))).ToArray();
Anatomy.HandRefinements[side]=HandRefiner.Refine(Character!,Anatomy,side);
foreach(var point in geometry)
if(!point.Corrected&&Anatomy.Points.TryGetValue(point.Role,out var refined)&&!refined.Corrected&&refined.Position!=point.Position)
Anatomy.GeometricHandPoints[point.Role]=point;
}
public void Back()
{
Step=Step switch {WizardStep.Body=>WizardStep.Centerline,WizardStep.LeftHand=>WizardStep.Body,WizardStep.RightHand=>WizardStep.LeftHand,WizardStep.Validation or WizardStep.Finish=>WizardStep.RightHand,_=>throw new InvalidOperationException("Cannot go back.")};
Rig=null;Revision++;
}
public void Restart(){Character=null;Anatomy=null;Rig=null;centerlineChanged=false;RequestedPose=CharacterPose.Auto;Step=WizardStep.Import;LeftFingerCount=null;RightFingerCount=null;FingerCountsConfirmed=false;Revision++;}
}