Update geometry with the action button

Hello everyone,

I am relatively new to Viktor. I’m developing an app where the geometry should be updated upon the user clicking an action button. The function triggered by the action button initially performs a computation to determine certain parameters, which are then utilized to update the geometry. However, when I invoke the geometry visualization function (decorated with GeometryView) within the action button function (after computing the necessary parameters), I encounter “no attribute available” errors. Is there a solution for this in Viktor, or is this implementation not feasible? Thanks in advance.

Hi there, and welcome to the community forum, thanks for posting!!

Just so I get a clear idea of what you’re trying to do: am I correct in assuming that you want the user to be able to change some parameters without the geometry changing, and then, once the user is happy with their parameter configuration, let them hit the ActionButton after which the geometry gets refreshed?

If that’s the case, I wonder why using the Update button on the bottom right of a “Slow view” is not a viable alternative to the ActionButton?

Hello Daniel,

Thank you very much for the reply. You are almost right about the idea but I would like to add a few more details. In the app, the user gives the input (yes the geometry should not change while the user is giving the input) and clicks on the ActionButton which is when a function runs a computation and returns some parameters. These parameters will then be used to generate/update the geometry. So I wanted to write the app in such as way that as soon as the ActionButton is clicked, the computation should be run and the updated geometry has to be displayed. I tried it with the Slow View approach but the way the update option is shown in the geometry view is not natural for the user to click on it. I want more like a single-click option to run the computation and render the updated geometry immediately after that. Is there a way to update the geometry view through some function that can be linked to the ActionButton or do the same thing through some sort of maybe modified ActionButton? I hope this gives more context to the question. Thanks in advance.

Hi Kubair,

Excuse me if I still do not understand correctly, but wouldn’t your problem be fixed by structuring your app thusly?

class Controller(ViktorController):
    label = "My Controller"
    parametrization = Parametrization

    @GeometryView("Geometry", duration_guess=5)
    def render_geometry(self, params: Munch, **kwargs: dict) -> GeometryResult:
        """update geometry"""
        calculation_result = self.run_calculation(params)
        
        geom = geometry.based_on_calculation(calculation_result)
        
        return GeometryResult(geom)

    def run_calculation(self, params: Munch, **kwargs: dict):
        """some calculation"""
        b = 2 * params.a
        calculation_result = run_some_calculation

        return calculation_result

Or is your main problem that the update button in this case is on the bottom right of the view instead of in the parametrization?

Hi Daniel,

Thanks for the reply. The main issue with our idea is that the input field in our interface is a long text-type input (TextAreaField). So the user can give a very long input. I think your implementation above would run the run_calculation method whenever the user is typing the input and takes a small break in between (even when he is not fully done with writing the input) right? So we wanted to add an ActionButton so that the user clicks on it when he/she is done with the full input. Also, we can’t run the computation every time when they stop because it is an expensive computation. I hope that gives a bit more clarity.

No in the setup I presented the view is a slow one (duration_guess=5). So it will only be triggered when the user clicks the Update button, not when the parametrization is triggered by the user start/stopping the typing.

Ok, that’s great. But is there a way to link the functionality of the Update button to an ActionButton or some other option that can be triggered by the user in the main UI?

Same need and problem for me. This is the order of calls:

2024-11-10 16:02:00.160 INFO : Job (uid: 1055982) received - EntityType: Controller - call: parametrization
2024-11-10 16:02:02.636 INFO : Job (uid: 1055982) completed in 0ms
2024-11-10 16:02:05.459 INFO : Job (uid: 1055983) received - EntityType: Controller - call: get_geometry
Nothing to draw
2024-11-10 16:02:08.424 INFO : Job (uid: 1055983) completed in 583ms
2024-11-10 16:02:12.193 INFO : Job (uid: 1055984) received - EntityType: Controller - call: loadModel

In particular, I want that get_geometry is called AFTER loadModel, not before it, because I have to refresh the view.

I noticed also that I cannot call self.get_geometry(params, kwargs) from inside the action button func loadModel.
Then, how to proceed?

Hi!

It’s difficult to understand what some of your questions mean without looking at your code, what get_geometry is and what loadModel is, however: I think what’s missing here is some fundamental understanding of how VIKTOR executes your code. It’s good to be aware of this difference compared to scripting: in VIKTOR it’s not that you as a developer writes some functions down in a main and that’s executed top to bottom, instead you determine logic that is executed by the platform when the user triggers it!

So, what triggers actions?:

  • Modifying input
  • Clicking an action button
  • Clicking a next step button
  • Preprocessing an uploaded file

That’s it, with 1 important nuance for your case:

  • Visualisations (so my guess is get_geometry is a geometryview) are updated any time a trigger occurs, unless the view decorator specifies duration_guess>3. In that case: updating the params leads to showing the “update view” button in the bottom right corner of the view.

For anything else, you can determine the sequence of functions by calling them out in the order you want for all the triggers, i.e. you could do something like:

def get_geometry(self, params, **kwargs):
    # First load model
    updated_model = self.loadModel(params)

    <do stuff>
    return GeometryResult()

So: either the view is short and it’s updated whenever the params update and you can extend the method that is called when the actionbutton is clicked to also update the view, OR it is long and these things are separated. You cannot move the Update view button to another location (but you could of course add that as a feature request or look into using the interactive webview to build something

Hello!
thanks for your detailed reply. However, I see that ActionButton does not trigger a refresh in view.
My log when ActionButton has been pressed reports:

2024-11-11 12:24:44.435 INFO    : Job (uid: 1056963) received - EntityType: Controller - call: loadModel
2024-11-11 12:24:44.939 INFO    : Job (uid: 1056963) completed in 0ms

Am I misunderstanding something?

So sorry, I think I probably could have worded it a bit better, but these triggers trigger different things:

  • Visualisations are triggered:
    • whenever the params are updated for short views
    • whenever the update button is clicked for long views
  • Non-visualisation/Other methods on the controller are triggered in different ways:
    • on buttons (both download and action) you specify the name of a method on the controller that should be run when the button is clicked
    • on the “next step” buttons you can specify a piece of code that should be run

So, if you want to modify that flow, you have to do it by cutting your logic into separate functions and calling it in the order you want, in any of the methods tied to views/buttons.

Does that clarify things a bit?

Thanks, I suppose it’s not clear enough for me
Is there a way to force a redraw by code? Maybe placing it in the method associated to the ActionButton? I tried also to change the state of an hidden BooleanField, but this does not refresh the view.

Your reload button (duration_guess>3) does not fit my needs: model is provided by a worker, button uses Storage to store the model, and after that I need to trigger a view refresh. The default reload button will appear also if I change any other param, and this is not what I want to achieve.

It’s not a matter of call sequence: refresh should happen after the ActionButton is pressed. How can I extend the method that is called when the actionbutton is clicked to also update the view?

In the default way to use VIKTOR, the answer to:

Is there a way to force a redraw by code?

is no. Methods belonging to views are triggered by params update for short views, and clicking a button for long views.

But you actually came up with a pretty good workaround yourself, updating the hidden booleanfield will work. I suspect in your test you tried to update it through an actionbutton, and that’s why it did not work. To actually update the params in the same call you can use a SetparamsButton/Method. Was that also what you were doing? Using the snippet below I did manage to trigger a redraw of the view:

import viktor as vkt


class Parametrization(vkt.Parametrization):
    floors = vkt.NumberField('Floors', min=5, max=40, step=1)
    invisible_bool = vkt.BooleanField('bool', visible=False)
    button = vkt.SetParamsButton('Perform setparams', method='setparams_button_method')


class Controller(vkt.Controller):
    parametrization = Parametrization

    def setparams_button_method(self, params, **kwargs):
        return vkt.SetParamsResult({'invisible_bool': True})

    @vkt.GeometryView("Geometry", x_axis_to_right=True)
    def get_geometry_view(self, params, **kwargs):

        # Define Materials
        glass = vkt.Material("Glass", color=vkt.Color(150, 150, 255))
        facade = vkt.Material("Facade", color=vkt.Color.white())

        # Create one floor
        width = 30
        length = 30
        number_of_floors = params.floors
        floor_glass = vkt.SquareBeam(width, length, 2, material=glass)
        floor_facade = vkt.SquareBeam(width + 1, length + 1, 1, material=facade)
        floor_facade.translate([0, 0, 1.5])
    
        # Pattern (duplicate) the floor to create a building
        floor = vkt.Group([floor_glass, floor_facade])
        building = vkt.LinearPattern(floor, direction=[0, 0, 1], number_of_elements=number_of_floors, spacing=3)
    
        return vkt.GeometryResult(building)

As an aside, this workaround does still leave you with the other problem, this will trigger a redraw whenever the params are updated. Using this workaround (or the long view option) I think in either case will force you to, inside the view method, do some checking and only draw something new when there is an update, and using a stored/cached result if not.

Does that help?

This is exactly what I was trying to do!
I placed a bool:

reloadModel = BooleanField("reloadModel",visible=False)
reloadModelButt = ActionButton("Reload model", "getmodel")

and then in the ActionButton function:

    def getmodel(self, params, **kwargs):
        ...
        return vkt.SetParamsResult({"reloadModel": True})

but redraw does not happen. Why?
I’m using app_type = “simple”, IDK if it matters.

It does not happen because you are using an ActionButton, but should be using a SetParamsButton.

Also, if you always want the redraw to happen you probably should do something like checking which value the bool currently has and then setting the opposite:

# Parametrization
reloadModel = vkt.BooleanField("reloadModel", visible=False)
reloadModelButt = vkt.SetParamsButton("Reload model", "getmodel")


# Controller
def getmodel(self, params, **kwargs):
    value = False if params.BooleanField else True
    return vkt.SetParamsResult({"reloadModel": value})

Sorry I didn’t notice the difference.
Wow, it works like a charm! Thanks a lot

1 Like