You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Add Dynamo-style custom nodes via a single registered engine function (script.customScript) that executes embedded per-node script source. The recommended v1 backend is Kotlin Script (KTS) for zero translation layer and shared JVM types. Python remains optional later with an explicit JSON marshalling layer.
Motivation
OpenBimRL functions today are compile-time @OpenBIMRLFunction classes discovered by FunctionFactory at startup. Extending the engine requires rebuilding the JAR and redeploying. Revit Dynamo and similar tools allow practitioners to author custom nodes in script (historically Python).
Goals for custom script support:
One palette node type; different code per graph instance
Rich geometry/math on data passed through the graph
No direct IFC/native access from scripts — IFC data flows through existing ifc.* nodes upstream
Minimal changes to FunctionFactory and function registration
Core design: one function, many embedded scripts
One registered function, many embedded scripts — analogous to TextInput, which holds its value on the node rather than on the classpath:
Register exactly one class: script.customScript
Every custom node in the graph uses function="script.customScript"
Per-node data carries: script source, language (v1: kotlin), user-defined input/output ports
FunctionFactory stays unchanged — no dynamic registry, no hacked getFunction lookup
flowchart LR
Palette["Palette: one Custom Script item"]
NodeA["Node A: script.customScript + KTS body A"]
NodeB["Node B: script.customScript + KTS body B"]
FF[FunctionFactory fixed map]
CSF[CustomScriptFunction]
Palette --> NodeA
Palette --> NodeB
NodeA --> FF
NodeB --> FF
FF --> CSF
CSF -->|"read script from node XML"| Eval[KTS evaluator]
Loading
This matches Dynamo: one script node type, different code per instance.
Alternatives considered
Dynamic script registry (script.myHeatmap, …)
Register many script keys in FunctionFactory at startup or per check. Rejected: requires extending getFunction lookup and REST catalog churn. The single customScript function avoids hacking the provider model.
Python via subprocess (CPython)
Requires a translation layer. Kotlin/JVM objects cannot cross a process boundary natively:
Direction
Work
Engine → Python
Serialize inputs to JSON: Point3d → {x,y,z}, collections → arrays
Python → Engine
Deserialize JSON back to JVM types; validate shapes
Rich types
Ship openbimrl Python helpers (Point, Matrix, …)
Failure modes
Type mismatches at runtime, not compile time
Non-trivial and duplicates types already in the engine (javax.vecmath, Geometry.kt).
Deferred as optional Phase 5; same script.customScript node with language="python" in XML.
Jython / JPython
Python 2.7 only, unmaintained. Not recommended.
GraalVM polyglot (in-process Python)
Issue
Detail
JDK swap
Polyglot Python needs GraalVM JDK, not current OpenJDK/devcontainer setup
Deploy footprint
Larger runtime, more RAM; Spring Boot tuning
Graal Python ≠ CPython
graalpy lacks full numpy/scipy — weak for Dynamo-style matrix math
Ops complexity
Nix/Docker/CI must pin GraalVM
Sandbox
In-process; policy easy to misconfigure
GraalVM removes the Python translation layer in-process but trades deploy cost and ecosystem gaps. KTS in-process is simpler today; CPython subprocess is simpler than GraalVM if real Python/numpy is required later.
KTS / KtsRunner (recommended v1)
Aspect
KTS (in-process)
Python (subprocess)
Translation layer
None — bind inputs[0] as Collection<Point3d> directly
Required
Type safety
Compile-time script errors
Runtime only
IFC/native isolation
Classpath whitelist — omit IfcPointer, JNA
Process boundary
Dynamo familiarity
Lower (Kotlin)
Higher (Python)
Dependencies
Kotlin 2.3.10 already on classpath
python3, optional numpy on server
Performance
No IPC
Spawn + JSON per node
Use kotlin-scripting-jvm-host (BasicJvmScriptingHost) aligned with Kotlin 2.x rather than legacy JSR 223 KtsRunner. Evaluate script text from node XML at check time; cache compilation by script hash.
Type restrictions (KTS)
Treated as a feature, not a limitation:
Input bindings are typed — e.g. val points: Collection<Point3d> = inputs[0]. Mismatches fail at script compile.
Classpath whitelist — ScriptCompilationConfiguration exposes only allowed packages. No IfcPointer, no FunctionsLibrary.
Dynamic ports — unlike @FunctionInput on normal functions, port count/names come from node XML (Creator Tool). CustomScriptFunction reads ports from the node and builds bindings manually (same idea as TextInput reading its value from XML).
Outputs — script assigns to outputs: MutableMap<Int, Any?> or returns Map<Int, Any?>; engine validates before setResult.
New package: de.rub.bi.inf.openbimrl.functions.script
@OpenBIMRLFunction(
packageName ="script",
name ="customScript",
description ="Runs embedded Kotlin script with user-defined inputs/outputs.",
type ="scriptType",
)
classCustomScriptFunction(nodeProxy:NodeProxy) : AbstractFunction(nodeProxy) {
overridefunexecute() {
val source = readScriptFromNode(nodeProxy.node)
val inputCount = nodeProxy.node.inputs?.input?.size ?:0val inputs = (0 until inputCount).map { getInputAsCollection(it) }
val outputs =KtsScriptEvaluator.eval(source, inputs, allowedClasspath)
outputs.forEach { (pos, value) -> setResult(pos, value) }
}
}
No @FunctionInput fields — ports defined per node in XML. /functions may advertise default template ports (e.g. one input, one output) editable per instance.
Register de.rub.bi.inf.openbimrl.functions.script in FunctionFactory.functionPackages.
RFC-1: Custom Script Functions
Summary
Add Dynamo-style custom nodes via a single registered engine function (
script.customScript) that executes embedded per-node script source. The recommended v1 backend is Kotlin Script (KTS) for zero translation layer and shared JVM types. Python remains optional later with an explicit JSON marshalling layer.Motivation
OpenBimRL functions today are compile-time
@OpenBIMRLFunctionclasses discovered byFunctionFactoryat startup. Extending the engine requires rebuilding the JAR and redeploying. Revit Dynamo and similar tools allow practitioners to author custom nodes in script (historically Python).Goals for custom script support:
ifc.*nodes upstreamFunctionFactoryand function registrationCore design: one function, many embedded scripts
One registered function, many embedded scripts — analogous to
TextInput, which holds its value on the node rather than on the classpath:script.customScriptfunction="script.customScript"kotlin), user-defined input/output portsFunctionFactorystays unchanged — no dynamic registry, no hackedgetFunctionlookupflowchart LR Palette["Palette: one Custom Script item"] NodeA["Node A: script.customScript + KTS body A"] NodeB["Node B: script.customScript + KTS body B"] FF[FunctionFactory fixed map] CSF[CustomScriptFunction] Palette --> NodeA Palette --> NodeB NodeA --> FF NodeB --> FF FF --> CSF CSF -->|"read script from node XML"| Eval[KTS evaluator]This matches Dynamo: one script node type, different code per instance.
Alternatives considered
Dynamic script registry (
script.myHeatmap, …)Register many script keys in
FunctionFactoryat startup or per check. Rejected: requires extendinggetFunctionlookup and REST catalog churn. The singlecustomScriptfunction avoids hacking the provider model.Python via subprocess (CPython)
Requires a translation layer. Kotlin/JVM objects cannot cross a process boundary natively:
Point3d→{x,y,z}, collections → arraysopenbimrlPython helpers (Point,Matrix, …)Non-trivial and duplicates types already in the engine (
javax.vecmath,Geometry.kt).Deferred as optional Phase 5; same
script.customScriptnode withlanguage="python"in XML.Jython / JPython
Python 2.7 only, unmaintained. Not recommended.
GraalVM polyglot (in-process Python)
graalpylacks full numpy/scipy — weak for Dynamo-style matrix mathGraalVM removes the Python translation layer in-process but trades deploy cost and ecosystem gaps. KTS in-process is simpler today; CPython subprocess is simpler than GraalVM if real Python/numpy is required later.
KTS / KtsRunner (recommended v1)
inputs[0]asCollection<Point3d>directlyIfcPointer, JNApython3, optional numpy on serverUse
kotlin-scripting-jvm-host(BasicJvmScriptingHost) aligned with Kotlin 2.x rather than legacy JSR 223 KtsRunner. Evaluate script text from node XML at check time; cache compilation by script hash.Type restrictions (KTS)
Treated as a feature, not a limitation:
val points: Collection<Point3d> = inputs[0]. Mismatches fail at script compile.ScriptCompilationConfigurationexposes only allowed packages. NoIfcPointer, noFunctionsLibrary.@FunctionInputon normal functions, port count/names come from node XML (Creator Tool).CustomScriptFunctionreads ports from the node and builds bindings manually (same idea asTextInputreading its value from XML).outputs: MutableMap<Int, Any?>or returnsMap<Int, Any?>; engine validates beforesetResult.Example embedded script:
Target implementation
1. Engine:
CustomScriptFunctionNew package:
de.rub.bi.inf.openbimrl.functions.script@OpenBIMRLFunction( packageName = "script", name = "customScript", description = "Runs embedded Kotlin script with user-defined inputs/outputs.", type = "scriptType", ) class CustomScriptFunction(nodeProxy: NodeProxy) : AbstractFunction(nodeProxy) { override fun execute() { val source = readScriptFromNode(nodeProxy.node) val inputCount = nodeProxy.node.inputs?.input?.size ?: 0 val inputs = (0 until inputCount).map { getInputAsCollection(it) } val outputs = KtsScriptEvaluator.eval(source, inputs, allowedClasspath) outputs.forEach { (pos, value) -> setResult(pos, value) } } }No
@FunctionInputfields — ports defined per node in XML./functionsmay advertise default template ports (e.g. one input, one output) editable per instance.Register
de.rub.bi.inf.openbimrl.functions.scriptinFunctionFactory.functionPackages.2. Script storage (OpenBIMRL XML)
Creator Tool
ParserOpenBIMRL.ts: round-trip<Script>intonode.data.scriptSource.3. Script-exposed API (no native IFC)
Whitelist in compilation config:
javax.vecmath.*javax.media.j3d.BoundingBox(as data from upstream nodes)openbimrl.script.helpers.*(matrix ops, lerp, transforms — wrapGeometry.kt)Excluded:
IfcPointer,NativeFunction,FunctionsLibrary, file/network APIs.4. REST and Creator Tool
FunctionFactoryCustomScriptFunctionAvailableFunctionServicescript.customScript,type: scriptTypeScriptNode.vue: editable ports + KTS editorGET /functionsPhased delivery
Phase 1 — Engine PoC (KTS)
CustomScriptFunction+KtsScriptEvaluator(whitelist, timeout, compile cache)List<Double>Phase 2 — Schema + parser
<Script language="kotlin">in OpenBIMRL XMLPhase 3 — Creator Tool
scriptTypenode, port editor, KTS code panel/functionsPhase 4 — Script helper library
openbimrl.script.helpersfor matrix/geometry (Kotlin-native Dynamo-like surface)Phase 5 (optional) — Python backend
Out of scope (v1)
FunctionFactoryregistration / per-script registry keysIfcPointeror native IFC access from scriptsAbstractFunctionsubclasses on classpathReferences
FunctionFactory.kt— current function discoveryTextInput.java— per-node data patternAvailableFunctionService.kt— Creator Tool paletteSynced from
rfc/rfc-1-custom-script-functions.md(2026-07-05 00:35 UTC).