Editor/Effigy/Variables.cs
using System;
using System.Collections.Generic;
namespace Effigy;
/// <summary>
/// A named value on the document, evaluated before the first feature rebuilds.
///
/// THIS IS WHAT MAKES A PARAMETRIC MODELLER PARAMETRIC IN MORE THAN ONE PLACE. A feature's own
/// parameter is one number; a variable is a number several features can refer to. "The wall is 2
/// units thick" said once, used by the shell, the rib pitch and the hole, and changed once.
///
/// The expression is the source of truth. <see cref="Value"/> is the last successful evaluation,
/// held when the text is mid-keystroke or a cycle, the same rule numeric fields already use.
/// </summary>
public sealed class StudioVariable
{
public string Name;
public string Expr;
public float Value;
public StudioVariable() { }
public StudioVariable( string name, string expr, float value = 0f )
{
Name = name;
Expr = expr;
Value = value;
}
}
/// <summary>
/// Resolve <c>#name</c> against a list of variables, with cycle detection.
///
/// A CYCLE IS A PARSE FAILURE, not a stack overflow: <c>a = #b + 1</c>, <c>b = #a + 1</c> names
/// both variables and keeps the last good values. Evaluation is recursive through expressions so
/// order in the table does not matter.
/// </summary>
public static class VariableResolver
{
public static bool IsLegalName( string name )
{
if ( string.IsNullOrWhiteSpace( name ) )
return false;
if ( !( char.IsLetter( name[0] ) || name[0] == '_' ) )
return false;
for ( var i = 1; i < name.Length; i++ )
{
if ( !( char.IsLetterOrDigit( name[i] ) || name[i] == '_' ) )
return false;
}
return true;
}
/// <summary>
/// Evaluate every variable in the table, writing each successful result back onto
/// <see cref="StudioVariable.Value"/>. Failures leave the previous value.
/// </summary>
public static void EvaluateAll( IReadOnlyList<StudioVariable> variables )
{
if ( variables is null || variables.Count == 0 )
return;
foreach ( var variable in variables )
{
if ( variable is null || string.IsNullOrWhiteSpace( variable.Name ) )
continue;
var visiting = new HashSet<string>( StringComparer.OrdinalIgnoreCase );
if ( TryResolve( variables, variable.Name, visiting, out var value ) )
variable.Value = value;
}
}
public static bool TryResolve( IReadOnlyList<StudioVariable> variables, string name, out float value ) =>
TryResolve( variables, name, new HashSet<string>( StringComparer.OrdinalIgnoreCase ), out value );
public static bool TryResolve( IReadOnlyList<StudioVariable> variables, string name,
HashSet<string> visiting, out float value )
{
value = 0f;
if ( variables is null || string.IsNullOrWhiteSpace( name ) )
return false;
StudioVariable match = null;
foreach ( var variable in variables )
{
if ( variable?.Name is null )
continue;
if ( string.Equals( variable.Name, name, StringComparison.OrdinalIgnoreCase ) )
{
match = variable;
break;
}
}
if ( match is null )
return false;
if ( !visiting.Add( match.Name ) )
return false;
if ( string.IsNullOrWhiteSpace( match.Expr ) )
{
value = match.Value;
visiting.Remove( match.Name );
return true;
}
float? Resolve( string asked )
{
if ( !TryResolve( variables, asked, visiting, out var found ) )
return null;
return found;
}
var ok = Expression.TryEvaluate( match.Expr, null, Resolve, out value );
visiting.Remove( match.Name );
return ok;
}
/// <summary>A resolver a numeric field or a rebuild can hand to <see cref="Expression"/>.</summary>
public static Func<string, float?> Bind( IReadOnlyList<StudioVariable> variables ) =>
name => TryResolve( variables, name, out var value ) ? value : null;
}