What are Cross-Function Overrides?
Typically, overrides in a function are "attached" to elements produced by the same function. A function produces some elements, and via the overrides associated with those elements, a user can modify the properties of those elements.
In some situations, it is desirable to attach overrides to elements from a different function β one that your function depends on.
For instance, a function for creating a Facade might depend on Envelopes from another function. That function might want to permit setting different facade settings per-envelope. To achieve this, the Facade function could specify a cross-function override β one with a dependency on
Envelope
β so that the user will be able to select an envelope and modify the associated facade settings.For the complete code for the following example, take a look at this repository:github.com/hypar-io/CrossFunctionOverrideFacadeExample. Note that some details have been simplified in the example code below for clarity β for a full, working example refer to the code in the repository.
Configuring the Override
An override for facade settings might look like this:
json"overrides": { "Facade Grid Settings": { "context": "[*discriminator=Elements.Envelope]", "identity": { "Profile": { "type": "object", "properties": { "Perimeter": { "$ref": "https://hypar.io/Schemas/Geometry/Polygon.json" } } } }, "schema": { "X Grid Size": { "type": "number", "$hyparUnitType": "length", "default": 3 }, "Y Grid Size": { "type": "number", "$hyparUnitType": "length", "default": 3 } } } },
This configuration would be fine if we were adding the override to the Envelope function itself. But in this case, we want to add behavior to any function producing envelopes, and we only need that information in the facade function we're writing. So instead, we'll set up the override in the Facade function, with a
dependency
on Envelope
, in order to "attach" this information to Envelope
elements from an upstream function:json// You can only add a dependency to an override if your function has that // dependency among its model_dependencies. "model_dependencies": [ { "name": "Envelope" } ], "overrides": { "Facade Grid Settings": { "context": "[*discriminator=Elements.Envelope]", "dependency": "Envelope", "identity": { "Profile": { "type": "object", "properties": { "Perimeter": { "$ref": "https://hypar.io/Schemas/Geometry/Polygon.json" } } } }, "schema": { "X Grid Size": { "type": "number", "$hyparUnitType": "length", "default": 3 }, "Y Grid Size": { "type": "number", "$hyparUnitType": "length", "default": 3 } } } },
In short, the configuration for cross-function overrides is just like any other override, except:
- it has a
dependency
property specified, which names the dependency containing the elements this override should be "attached" to
- the function also includes that
dependency
among itsmodel_dependencies
.
Implementing a cross-function override in code
Once your configuration is set up, you generate code for your overrides with
hypar init
, just like any other override. There are some additional differences in how you write the code to handle these overrides, which arise from the need to attach information to elements that our function consumes, but does not produce. In other words, our function makes a facade, and depends on envelopes from another function β how do we set values on those envelopes, since we're not passing them out with the output model? Proxy elements
A Proxy Element can be thought of as a "pointer" to an element produced by another function. In our Facade example, we'll need to create Proxy Elements from the Envelopes we depend on, so we can attach information about the facade settings to those Envelopes.
A proxy is created like this:
c#var envelopeProxy = envelope.Proxy("Facade Grid Settings");
The argument to the
.Proxy
method is the override name β this must match the name of the override as it is defined in hypar.json
. Integrating into the function
We have a very simple function, which creates a grid on the vertical faces of an envelope:
c#public static CrossFunctionOverrideFacadeExampleOutputs Execute(Dictionary<string, Model> inputModels, CrossFunctionOverrideFacadeExampleInputs input) { var output = new CrossFunctionOverrideFacadeExampleOutputs(); var envelopes = inputModels["Envelope"].AllElementsOfType<Envelope>(); foreach (var envelope in envelopes) { // the FacadeGridSettingsValue type is already // generated for us β it is useful to use it here, // even though we're creating it ourselves, to start. var gridSettings = new FacadeGridSettingsValue(3, 3); // create grid elements for an envelope and grid settings var grid = CreateGridForEnvelope(envelope, gridSettings); output.Model.AddElements(grid); } return output; }
We want to check if there were any overrides that matched each envelope, just like we would with any other override:
c#public static CrossFunctionOverrideFacadeExampleOutputs Execute(Dictionary<string, Model> inputModels, CrossFunctionOverrideFacadeExampleInputs input) { var output = new CrossFunctionOverrideFacadeExampleOutputs(); var envelopes = inputModels["Envelope"].AllElementsOfType<Envelope>(); foreach (var envelope in envelopes) { // the FacadeGridSettingsValue type is already // generated for us β it is useful to use it here, // even though we're creating it ourselves, to start. var gridSettings = new FacadeGridSettingsValue(3, 3); if (input.Overrides?.FacadeGridSettings != null) { // find a matching override by comparing the override's // identity with the envelope's properties var match = input.Overrides.FacadeGridSettings .FirstOrDefault(o => o.Identity.Profile.Perimeter.Centroid().DistanceTo( envelope.Profile.Perimeter.Centroid()) < 0.01); // if we found a match, use it to set different grid settings. if (match != null) { gridSettings = match.Value; } } // create grid elements for an envelope and grid settings var grid = CreateGridForEnvelope(envelope, gridSettings); output.Model.AddElements(grid); } return output; }
Next, we have to make sure we attach the modified value to the envelope element, using a Proxy Element and
Identity.AddOverrideValue
c#public static CrossFunctionOverrideFacadeExampleOutputs Execute(Dictionary<string, Model> inputModels, CrossFunctionOverrideFacadeExampleInputs input) { var output = new CrossFunctionOverrideFacadeExampleOutputs(); var envelopes = inputModels["Envelope"].AllElementsOfType<Envelope>(); foreach (var envelope in envelopes) { // create proxy var envelopeProxy = envelope.Proxy(FacadeGridSettingsOverride.Name); var gridSettings = new FacadeGridSettingsValue(3, 3); if (input.Overrides?.FacadeGridSettings != null) { var match = input.Overrides.FacadeGridSettings.FirstOrDefault(o => o.Identity.Profile.Perimeter.Centroid().DistanceTo(envelope.Profile.Perimeter.Centroid()) < 0.01); if (match != null) { gridSettings = match.Value; } } // attach grid settings values to proxy element Identity.AddOverrideValue(envelopeProxy, FacadeGridSettingsOverride.Name, gridSettings); // add proxy to output model output.Model.AddElement(envelopeProxy); // create grid elements for an envelope and grid settings var grid = CreateGridForEnvelope(envelope, gridSettings); output.Model.AddElements(grid); } return output; }
Lastly, we need to attach the override's identity to the element proxy, if there was a matching override. This will allow us to individually revert this override for this element:
c#public static CrossFunctionOverrideFacadeExampleOutputs Execute(Dictionary<string, Model> inputModels, CrossFunctionOverrideFacadeExampleInputs input) { var output = new CrossFunctionOverrideFacadeExampleOutputs(); var envelopes = inputModels["Envelope"].AllElementsOfType<Envelope>(); foreach (var envelope in envelopes) { // create proxy var envelopeProxy = envelope.Proxy(FacadeGridSettingsOverride.Name); // the FacadeGridSettingsValue type is already // generated for us β it is useful to use it here, // even though we're creating it ourselves, to start. var gridSettings = new FacadeGridSettingsValue(3, 3); if (input.Overrides?.FacadeGridSettings != null) { // find a matching override by comparing the override's identity with the envelope's properties var match = input.Overrides.FacadeGridSettings.FirstOrDefault(o => o.Identity.Profile.Perimeter.Centroid().DistanceTo(envelope.Profile.Perimeter.Centroid()) < 0.01); // if we found a match, use it to set different grid settings. if (match != null) { gridSettings = match.Value; // attach the override's identity to the proxy Identity.AddOverrideIdentity(envelopeProxy, match); } } // attach grid settings values to proxy element Identity.AddOverrideValue(envelopeProxy, FacadeGridSettingsOverride.Name, gridSettings); // add proxy to output model output.Model.AddElement(envelopeProxy); // create grid elements for an envelope and grid settings var grid = CreateGridForEnvelope(envelope, gridSettings); output.Model.AddElements(grid); } return output; }
Behavior
Now in the UI, we should be able to set facade grid settings per-envelope:
Summary
- Create cross-function overrides by adding a
dependency
property to your override configuration inhypar.json
.
- Make sure this
dependency
is already among yourmodel_dependencies
.
- In your function code, make sure to:
- Create
ProxyElement
s from the elements to which the override should be attached (the ones specified in the override'scontext
) - Handle the information from the code-generated override like you would any other override, by accessing
input.Overrides.YourOverrideName
. - use
Identity.AddOverrideIdentity
on the proxy you create to associate any matching override with the targeted element. - use
Identity.AddOverrideValue
to attach the overridden value to the proxy β almost as though you were attaching new properties to the target element itself. - Pass the
ProxyElement
into the output model.