Hello,
This is more of a programming question than a Revit API question. I was creating a program that allows me to quantify elements. This is the code.
public double GetFormworkArea(Element el)
{
string elementDefinition = el.LookupParameter("element definition").AsString();
switch (elementDefinition)
{
case "exterior Column":
return getArea(el) / 2;
break;
case "interior Column":
return getArea(el) * 3 / 2;
break;
}
return 0;
}
I have two cases inner column and outer column. But during the modeling process the user wants to create a new definition called "central column" with a formula get area()
How could I do for the user to add this functionality
You probably need a database to save/edit all your elementDefinition cases. (Simple file should work, or you could save on the file using Extensible Storage)
And you need some kind of Expression Evaluator, similar to Revit Formula works.
Like this, the user can create any formula for each elementDefinition case.
Something like this for the file:
exterior Column = AREA / 2
interior Column = AREA * 3 / 2
central Column = AREA
my Column = AREA * AREA
Each line is one case and split by = separate the case with the formula.
That's an idea,
See yaa!
Not sure if it would work for you solution or not, but for these types of things I often use xml files and readers to provide scalability as well as project / owner specific needs.
I have attached a PDF example of something similar I had shared previously on LinkedIn.
Also a working example on my Git.
Thank you very much for your answers.
I will try it and comment my results.
Since the Revit API is .NET based, and .NET provides powerful scripting support, why not make use of that?
For instance, search the Internet for '.net script':
https://duckduckgo.com/?q=.net+script
Here is an example script engine:
https://www.codeproject.com/articles/30999/scriptengine-user-defined-calculations-in-c-vb-jsc
I always use JSON to save data, really simple to serialize and deserialize a c# object.
Revit already uses the Newtonsoft.Json package by default.
Here is an example: https://github.com/ricaun/RevitAddin.JsonExample.
@jeremy_tammik wrote:Since the Revit API is .NET based, and .NET provides powerful scripting support, why not make use of that?
For instance, search the Internet for '.net script':
Interesting but I don't think is a good idea to give the user the ability to run any C# code, looks dangerous.
This ExpressionParser looks that gonna work.
public class ExpressionParser
{
/// <summary>
/// eval
/// Source: https://stackoverflow.com/questions/3972854/parse-math-expression
/// </summary>
/// <param name="exp"></param>
/// <param name="vars"></param>
/// <returns></returns>
public double eval(string exp, Dictionary<string, double> vars)
{
int bracketCounter = 0;
int operatorIndex = -1;
exp = exp.Trim();
for (int i = 0; i < exp.Length; i++)
{
char c = exp[i];
if (c == '(') bracketCounter++;
else if (c == ')') bracketCounter--;
else if ((c == '+' || c == '-') && bracketCounter == 0)
{
operatorIndex = i;
break;
}
else if ((c == '*' || c == '/') && bracketCounter == 0 && operatorIndex < 0)
{
operatorIndex = i;
}
}
if (operatorIndex < 0)
{
exp = exp.Trim();
if (exp[0] == '(' && exp[exp.Length - 1] == ')')
return eval(exp.Substring(1, exp.Length - 1), vars);
else if (vars.ContainsKey(exp))
return vars[exp];
else
return double.Parse(exp);
}
else
{
switch (exp[operatorIndex])
{
case '+':
return eval(exp.Substring(0, operatorIndex), vars) + eval(exp.Substring(operatorIndex + 1), vars);
case '-':
return eval(exp.Substring(0, operatorIndex), vars) - eval(exp.Substring(operatorIndex + 1), vars);
case '*':
return eval(exp.Substring(0, operatorIndex), vars) * eval(exp.Substring(operatorIndex + 1), vars);
case '/':
return eval(exp.Substring(0, operatorIndex), vars) / eval(exp.Substring(operatorIndex + 1), vars);
}
}
return 0;
}
}
With this simple code to test!
var expressionParser = new ExpressionParser();
var vars = new Dictionary<string, double>();
vars.Add("AREA", 10.0);
var area = expressionParser.eval("AREA * AREA", vars);
System.Windows.MessageBox.Show($"AREA {area}");
See yaa!
You may find FormulaManager.Evaluate offers a more Revit centric approach.
However with the above parameterId as ElementId seems to imply a parameter element is required. Likely to establish the parameter type of the result.
"It evaluates formula using list of global or family parameters depends on document type. "
That isn't that clear: probably however means you have to be in a family document to evaluate a family parameter and a project to evaluate a global one. I guess you could make it work via adding what you need in a temporary way if it is requiring a parameter of some kind i.e. a global one in project (although you wouldn't be able to reference other parameter names in the formula string).
A simple example that works:
Public Function Obj_220118a(commandData As ExternalCommandData, ByRef message As String, elements As ElementSet) As Result
Dim app = commandData.Application
Dim uidoc = commandData.Application.ActiveUIDocument
Dim IntDoc = uidoc.Document
Dim Formula As String = "(10*10)^0.5"
Dim Formula0 As String = "Pi()"
Dim Out As String = ""
Using Tx As New Transaction(IntDoc, "XX")
If Tx.Start Then
Dim G As String = Guid.NewGuid.ToString
Dim GP = GlobalParameter.Create(IntDoc, "RPT_" & G, SpecTypeId.Number)
Out = FormulaManager.Evaluate(GP.Id, IntDoc, Formula0)
Tx.RollBack()
End If
End Using
TaskDialog.Show("Result", Out)
Return Result.Succeeded
End Function
I thought that I could use as the quantification tables, but it didn't work and I had an error with the following code.
public Result Execute(ExternalCommandData commandData, ref string message, ElementSet elements) { UIApplication uiApp = commandData.Application; UIDocument uiDoc = uiApp.ActiveUIDocument; Document doc = uiDoc.Document; ElementId elementId = uiDoc.Selection.PickObject(ObjectType.Element, "Select element").ElementId; //IList<string> functions = FormulaManager.GetFunctions(); try { string result = FormulaManager.Evaluate(elementId, doc, "Volume * 2"); TaskDialog.Show("Lambda Ingenieria e Innovacion", result); } catch (Exception ex) { TaskDialog.Show("Lambda Ingenieria e Innovacion", ex.Message); return Result.Cancelled; } return Result.Succeeded; }
No you need to provide the id to a FamilyParameter or GlobalParameter (GlobalParameter in Project or FamilyParameter in Family) and not an Element or parameter you get from Element.Parameter.
A further alternative would be if you had a known empty family that is added to a project. The user could add formulas to that and you could then change type parameter value inputs for those formulas in the project family type to get an output that way.
So in your situation you would add a type parameter to the empty family named 'Area'. The user would add separate type parameters to same family with formulas:
Parameter name = ExtColumn, Formula = Area / 2
Parameter name = InteriorColumn, Formula = Area * 1.5
...
If you then edit the type and populate the 'Area' parameter then you can read off the appropriate named parameter value (ExtColumn or InteriorColumn...). 'Area' could be a shared parameter that is not user modifiable and you should find it by GUID not it's name 'Area'.
Although the approach I outlined initially above seems to work and is likely the most straightforward option. The loaded family option has the benefit of not having to store the formulas in some separate settings file nor create a UI for editing such i.e. it is all inherently Revit.