3dm File Selection in Viktor interface

Hello everyone,

We would like to create a Viktor App for the optimization of string connections and the arrangement on PV facades. We are trying to upload a 3D model via the inputs and then select the area which should be calculated and optimized. Furthermore, the original area is split into PV panels, in this step you should have the possibility to select which panels should be included in the calculation. In other words, the user must be able to select the specific areas via the Viktor interface. Is this possible with Viktor and have you a specific suggestion?
Picture 1: our current Viktor interface,
Picture 2: initial surface to select,
Picture 3: select multiple panels suggestion
cheers Samuel & Marco



Hi Samuel and Marco,

Nice to see you posting here and working with Grasshopper and VIKTOR!
Welcome to the forum and best wishes!

It is not possible to interact directly with the view. The view is only a display of the Rhino model. However, we can tweak the workflow to meet your needs. Some suggestions:

  • Have 2 upload fields. One with all your breps / surfaces etc, the other one with only your selected surface (the upload manager does remember uploaded surfaces, so you could easily switch between them)
  • Upload 1 file and fill a OptionField with all the surfaces from that model. This will probably require 2 views; one where you highlight the selected surface(s), and one where you perform the calculation.

A quick snippet on how to access surfaces of each object in a Rhino file.

# Get RhinoFile from upload (fix this location)
rhino_file = params.rhino_upload.file

# Make the VIKTOR file into a 3dm file
file = rhino3dm.File3dm().FromByteArray(rhino_file.getvalue_binary())

# Get a iterable list of the objects
objects = file.Objects
surfaces = [o for o in objects if o.Geometry.ObjectType == rhino3dm.ObjectType.Surface]

See here for more info.

Hopefully this gives some insights, feel free to ask further questions!

Rick

Thanks alot! we will try it like you described

cheers Samuel & Marco

Hi Rick, we tried to upload the 3dm file, that we can use the geometry in our grasshopper script. We tried it with this App code:

from io import BytesIO
import json
import rhino3dm
import os
from viktor import ViktorController, File
from viktor.parametrization import NumberField, FileField, Text, ViktorParametrization, OptionField, ActionButton
from viktor.external.generic import GenericAnalysis
from viktor.views import GeometryAndDataView, GeometryAndDataResult, DataGroup, DataItem

class Parametrization(ViktorParametrization):
    intro = Text("## PV Optimierung Level 1 \n Das Script Level 1 berechnet den Ertrag der jeweiligen Input Fassaden FlÀche \n\n Laden sie Ihre FassadenflÀche und Kontextgeometrien hoch und geben sie die vorgegebenen Parameter an:")

    # Input fields
    #WetterFile = FileField('Wetter File', file_types=['.epw'], max_size=10_000_000)
    Modell = FileField('Modell', file_types=['.3dm'], max_size=100_000_000)
    #Flaechenausrichtung = ActionButton('FlÀchenausrichtung Àndern', method='perform_action')
    #KontextGeometrien = FileField('Kontext Geometrien', file_types=['.3dm'], max_size=100_000_000)
    #Modultyp = OptionField('Modultyp', options=['0', '1', '2', '3'], default=0)
    #ScriptStarten = ActionButton('Script Starten', method='perform_action')
    Eigenverbrauch = NumberField("Eigenverbrauch in % des Gesamtertrages", default=50)
    Investitionskosten = NumberField("Investitionskosten", default=10000)
    Instandhaltungskosten = NumberField("Instandhaltungskosten in % der Investitionskosten", default=5.0)
    Foerderbeitrag = NumberField("Förderbeitrag %", default=50)
    Strompreis = NumberField("Strompreis(Fr./kWh)", default=0.3)
    Einspeiseverguetung = NumberField("Einspeiseverguetung(Fr./kWh)", default=20)
    Zeitraum = NumberField("Zeitraum(Jahre)", default=20)


class Controller(ViktorController):
    label = 'My Entity Type'
    parametrization = Parametrization(width=30)

    @GeometryAndDataView("Geometry", duration_guess=0, update_label='Run Grasshopper')
    def run_grasshopper(self, params, **kwargs):
        # Get RhinoFile from upload (fix this location)
        rhino_file = params.Modell.file

        # Create a JSON file from the input parameters
        input_json = json.dumps(params)

        # Generate the input files
        files = [('input.json', BytesIO(bytes(input_json, 'utf8'))), ('Modell.3dm', rhino_file)]

        # Run the Grasshopper analysis and obtain the output files
        generic_analysis = GenericAnalysis(files=files, executable_key="run_grasshopper", output_filenames=[
            "geometry.3dm", "output.json"
        ])
        generic_analysis.execute(timeout=60)
        rhino_3dm_file = generic_analysis.get_output_file("geometry.3dm", as_file=True)
        output_values: File = generic_analysis.get_output_file("output.json", as_file=True)

        # Create a DataGroup object to display output data
        output_dict = json.loads(output_values.getvalue())
        print(output_dict)
        data_group = DataGroup(
            *[DataItem(key.replace("_", " "), val) for key, val in output_dict.items()]
        )

        return GeometryAndDataResult(geometry=rhino_3dm_file, geometry_type="3dm", data=data_group)

How do we connect the 3dm file with the grasshopper file ? Do we have to modify our run_grasshopper script as welll?

run grasshopper script:

# Pip install required packages
import os
import json
import compute_rhino3d.Grasshopper as gh
import compute_rhino3d.Util
import rhino3dm

# Set the compute_rhino3d.Util.url, default URL is http://localhost:6500/
compute_rhino3d.Util.url = 'http://localhost:6500/'

# Define path to local working directory
workdir = os.getcwd() + '\\'

# Read input parameters from JSON file
with open(workdir + 'input.json') as f:
    input_params = json.load(f)

# Create the input DataTree
input_trees = []
for key, value in input_params.items():
    tree = gh.DataTree(key)
    tree.Append([{0}], [str(value)])
    input_trees.append(tree)

# Evaluate the Grasshopper definition
output = gh.EvaluateDefinition(
    workdir + 'sample_box_grasshopper.gh',
    input_trees
)


def get_value_from_tree(datatree: dict, param_name: str):
    """Get first value in datatree that matches given param_name"""
    for val in datatree['values']:
        if val["ParamName"] == param_name:
            return val['InnerTree']['{0}'][0]['data']


# Create a new rhino3dm file and save resulting geometry to file
file = rhino3dm.File3dm()
output_geometry = get_value_from_tree(output, "Geometry")
obj = rhino3dm.CommonObject.Decode(json.loads(output_geometry))
file.Objects.AddMesh(obj)

# Save Rhino file to working directory
file.Write(workdir + 'geometry.3dm', 7)

# Parse output data
output_values = {}
for key in ["Einspeiseverguetung", "EinnahmeJahr", "Stromkosteneinsparung", "Einnahmen", "Unterhaltskosten", "KostenproJahr"]:
    val = get_value_from_tree(output, key)
    #val = val.replace("\"", "")
    output_values[key] = val

# Save json file with output data to working directory
with open(workdir + 'output.json', 'w') as f:
    json.dump(output_values, f)

cheers Samuel & Marco

1 Like

Hi Samuel, Marco,

If I’m understanding this right, you want to add the generated Grasshopper geometry to your Model.3dm.

You can achieve that by reading the file into the File3dm object.

# Create a new rhino3dm file
file = rhino3dm.File3dm()

# Read rhino file 
file = rhino3dm.File3dm.Read("Path/To/Modell.3dm")

That should work fine.

Some notes on the optimal solution

Please note that you are now sending over the 3dm to the worker, and sending a larger 3dm file back to the Viktor app. If the Modell.3dm is big, this results in longer waiting times (as you are uploading / downloading twice).

You could also do the file.Objects.Add(obj) logic in your Viktor app, so that the data you send over is small. Our new tutorial with our new worker is a bit better at that.

Hi Rick

Thank you for your reply!

Our plan was that a user can upload his 3dm-file and we work with his file in our grasshopper script. We are trying to do this with specific layers that must be defined in the model from the user himself, so that our script knows what surface it should take as an input. The question now is how do we download the uploaded model from the user so that we can store / work with it?

cheers

Hi,

I understand you though process here! Although working with Grasshopper, through Rhino Compute, always has the big disadvantage that there is no Active Rhino Document. Therefore it is not possible to:

  • In your script reference Rhino Geometry
  • In your script retrieve layer data, or read data to it
  • From your script directly bake geometry (thats why we use rhino3dm)

For your workflow that means we need to preprocess the uploaded 3dm file in python (using rhino3dm), and then send the encoded geometry to a Get Geometry node.

Luckily, encoding is very easy, we can just call Encode() on any valid rhino3dm geometry object.

Filtering out the input of the user is a bit more complex. This is a conceptual snippet for getting the geometry from a layer called ‘chosen_layer’:

# Grasshopper parameters (to be sent with the Analysis)
gh_params = {...}

# Get uploaded rhino file
rhino_upload = params.rhino_upload.file

# Read the rhino file
gh_file = rhino3dm.File3dm.FromBytesArray(rhino_upload.getvalue_binary())

layer_name = 'chosen_layer'
layer_index = -1
# Get the chosen layer
for lay in gh_file.Layers:
   if lay.Name == layer_name:
      layer_index = lay.Index
      break

# Get the object in the layer
if layer_index != -1: 
   for obj in gh_file.Objects:
      if obj.Attributes.LayerIndex == layer_index:
         gh_params['gh_parameter_name' : json.dumps(obj.Encode())
         break
else:
   print('Layer was not found')

Hi Rick, thanks alot, we tried it like you described.
Our Code looks like that:

> from io import BytesIO
> import json
> import rhino3dm
> import os
> from viktor import ViktorController, File
> from viktor.parametrization import NumberField, FileField, Text, ViktorParametrization, OptionField, ActionButton
> from viktor.external.generic import GenericAnalysis
> from viktor.views import GeometryAndDataView, GeometryAndDataResult, DataGroup, DataItem
> 
> class Parametrization(ViktorParametrization):
>     intro = Text("## PV Optimierung Level 1 \n Das Script Level 1 berechnet den Ertrag der jeweiligen Input Fassaden FlÀche \n\n Laden sie Ihre FassadenflÀche und Kontextgeometrien hoch und geben sie die vorgegebenen Parameter an:")
> 
> 
>     Modell = FileField('Modell', file_types=['.3dm'], max_size=100_000_000)
>     #Flaechenausrichtung = ActionButton('FlÀchenausrichtung Àndern', method='perform_action')
>     #KontextGeometrien = FileField('Kontext Geometrien', file_types=['.3dm'], max_size=100_000_000)
>     #Modultyp = OptionField('Modultyp', options=['0', '1', '2', '3'], default=0)
>     #ScriptStarten = ActionButton('Script Starten', method='perform_action')
>     Eigenverbrauch = NumberField("Eigenverbrauch in % des Gesamtertrages", default=50)
>     Investitionskosten = NumberField("Investitionskosten", default=10000)
>     Instandhaltungskosten = NumberField("Instandhaltungskosten in % der Investitionskosten", default=5.0)
>     Foerderbeitrag = NumberField("Förderbeitrag %", default=50)
>     Strompreis = NumberField("Strompreis(Fr./kWh)", default=0.3)
>     Einspeiseverguetung = NumberField("Einspeiseverguetung(Fr./kWh)", default=20)
>     Zeitraum = NumberField("Zeitraum(Jahre)", default=20)
> 
> 
> class Controller(ViktorController):
>     label = 'My Entity Type'
>     parametrization = Parametrization(width=30)
> 
>     @GeometryAndDataView("Geometry", duration_guess=0, update_label='Run Grasshopper')
>     def run_grasshopper(self, params, **kwargs):
>         gh_params = {}
> 
>         # Get RhinoFile from upload (fix this location)
>         rhino_file = params.Modell.file
> 
>         # Read the rhino file
>         gh_file = rhino3dm.File3dm.FromBytesArray(rhino_file.getvalue_binary())
> 
>         layer_name = 'layer1'
>         layer_index = -1
>         # Get the chosen layer
>         for lay in gh_file.Layers:
>             if lay.Name == layer_name:
>                 layer_index = lay.Index
>                 break
>         print('Vor dem Test')
>         # Get the object in the layer
>         if layer_index != -1:
>             for obj in gh_file.Objects:
>                 if obj.Attributes.LayerIndex == layer_index:
>                     gh_params['Modell'] = json.dumps(obj.Encode())
>                     print('Layer was found')
>                     break
>         else:
>             print('Layer was not found')
> 
>         # Create a JSON file from the input parameters
>         input_json = json.dumps(params)
> 
>         # Generate the input files
>         files = [('input.json', BytesIO(bytes(input_json, 'utf8'))), ('Modell.3dm', rhino_file)]
> 
>         # Run the Grasshopper analysis and obtain the output files
>         generic_analysis = GenericAnalysis(files=files, executable_key="run_grasshopper", output_filenames=[
>             "geometry.3dm", "output.json"
>         ])
>         generic_analysis.execute(timeout=60)
>         rhino_3dm_file = generic_analysis.get_output_file("geometry.3dm", as_file=True)
>         output_values: File = generic_analysis.get_output_file("output.json", as_file=True)
> 
>         # Create a DataGroup object to display output data
>         output_dict = json.loads(output_values.getvalue())
>         print(output_dict)
>         data_group = DataGroup(
>             *[DataItem(key.replace("_", " "), val) for key, val in output_dict.items()]
>         )
> 
>         return GeometryAndDataResult(geometry=rhino_3dm_file, geometry_type="3dm", data=data_group)

but it didnÂŽt work, did we miss a part or do we have to change our run_grasshopper script that we can use the geometry?

run grasshopper script:

# Pip install required packages
import os
import json
import compute_rhino3d.Grasshopper as gh
import compute_rhino3d.Util
import rhino3dm

# Set the compute_rhino3d.Util.url, default URL is http://localhost:6500/
compute_rhino3d.Util.url = 'http://localhost:6500/'

# Define path to local working directory
workdir = os.getcwd() + '\\'

# Read input parameters from JSON file
with open(workdir + 'input.json') as f:
    input_params = json.load(f)

# Create the input DataTree
input_trees = []
for key, value in input_params.items():
    tree = gh.DataTree(key)
    tree.Append([{0}], [str(value)])
    input_trees.append(tree)

# Evaluate the Grasshopper definition
output = gh.EvaluateDefinition(
    workdir + 'sample_box_grasshopper.gh',
    input_trees
)


def get_value_from_tree(datatree: dict, param_name: str):
    """Get first value in datatree that matches given param_name"""
    for val in datatree['values']:
        if val["ParamName"] == param_name:
            return val['InnerTree']['{0}'][0]['data']


# Create a new rhino3dm file and save resulting geometry to file
file = rhino3dm.File3dm()
output_geometry = get_value_from_tree(output, "Geometry")
obj = rhino3dm.CommonObject.Decode(json.loads(output_geometry))
file.Objects.AddMesh(obj)

# Save Rhino file to working directory
file.Write(workdir + 'geometry.3dm', 7)

# Parse output data
output_values = {}
for key in ["Einspeiseverguetung", "EinnahmeJahr", "Stromkosteneinsparung", "Einnahmen", "Unterhaltskosten", "KostenproJahr"]:
    val = get_value_from_tree(output, key)
    #val = val.replace("\"", "")
    output_values[key] = val

# Save json file with output data to working directory
with open(workdir + 'output.json', 'w') as f:
    json.dump(output_values, f)

Hi;

Slight oversight with params <> gh_params. I meant my snippet to inspire you on how to do so :slight_smile:

Anyway; what we are sending to the worker is a big dict with paramname : value structure. As this is a dict, we can easily append new parameters to it. So it should be:

params['Modell'] = json.dumps(obj.Encode())

Hello Rick,
We have now done a local test with filtering by a specific area. This worked well, here is the code:

import rhino3dm

# Pfad zur ursprĂŒnglichen Rhino-Datei
original_file_path = r"C:\Program Files\Viktor_apps\viktor_grasshopper\Modell.3dm"
# Pfad, unter dem die neue Datei gespeichert werden soll
new_file_path = r"C:\Program Files\Viktor_apps\viktor_grasshopper\Test.3dm"

# Die ursprĂŒngliche Rhino-Datei laden
with open(original_file_path, "rb") as file:
    rhino_data = file.read()
    original_file = rhino3dm.File3dm.FromByteArray(rhino_data)

# Eine neue Rhino-Datei erstellen, in die die ausgewÀhlten Objekte kopiert werden
new_file = rhino3dm.File3dm()

# Der Name des Layers, dessen Objekte extrahiert werden sollen
desired_layer_name = 'TEST'

# Flag, um zu ĂŒberprĂŒfen, ob der Layer gefunden wurde
layer_found = False

# Alle Objekte durchgehen und die zum gewĂŒnschten Layer gehörenden in die neue Datei kopieren
for obj in original_file.Objects:
    layer = original_file.Layers[obj.Attributes.LayerIndex]
    if layer.Name == desired_layer_name:
        new_file.Objects.Add(obj.Geometry, obj.Attributes)
        layer_found = True

if not layer_found:
    print(f'Layer "{desired_layer_name}" wurde nicht gefunden')
else:
    # Die neue Datei mit den gefilterten Objekten speichern
    new_file.Write(new_file_path, 6)  # Der zweite Parameter ist die Version der Rhino-Datei

    print(f'Die neue Datei wurde unter {new_file_path} gespeichert.')`

Screenshot 2024-04-05 145208


We then tried applying this to our app.py code. Maybe we are too stupid but we don’t really understand the method. We have to encode the geometry so that it can be read by the Grasshopper Node get_geometry. But how do we pass this endcoded object. Is it also passed as bytes Io in the json file? We have tried to add our encoded object to the input json file. Or do we have to do it differently and save a separate file locally so that the get_geometry node can retrieve the filtered object?

here is our code:

from io import BytesIO
import json
import rhino3dm
import os
from viktor import ViktorController, File
from viktor.parametrization import NumberField, FileField, Text, ViktorParametrization, OptionField, ActionButton
from viktor.external.generic import GenericAnalysis
from viktor.views import GeometryAndDataView, GeometryAndDataResult, DataGroup, DataItem

class Parametrization(ViktorParametrization):
    intro = Text("## PV Optimierung Level 1 \n Das Script Level 1 berechnet den Ertrag der jeweiligen Input Fassaden FlÀche \n\n Laden sie Ihre FassadenflÀche und Kontextgeometrien hoch und geben sie die vorgegebenen Parameter an:")

    # Input fields
    #WetterFile = FileField('Wetter File', file_types=['.epw'], max_size=10_000_000)
    Modell = FileField('Modell', file_types=['.3dm'], max_size=100_000_000)
    #Flaechenausrichtung = ActionButton('FlÀchenausrichtung Àndern', method='perform_action')
    #KontextGeometrien = FileField('Kontext Geometrien', file_types=['.3dm'], max_size=100_000_000)
    #Modultyp = OptionField('Modultyp', options=['0', '1', '2', '3'], default=0)
    #ScriptStarten = ActionButton('Script Starten', method='perform_action')
    Eigenverbrauch = NumberField("Eigenverbrauch in % des Gesamtertrages", default=50)
    Investitionskosten = NumberField("Investitionskosten", default=10000)
    Instandhaltungskosten = NumberField("Instandhaltungskosten in % der Investitionskosten", default=5.0)
    Foerderbeitrag = NumberField("Förderbeitrag %", default=50)
    Strompreis = NumberField("Strompreis(Fr./kWh)", default=0.3)
    Einspeiseverguetung = NumberField("Einspeiseverguetung(Fr./kWh)", default=20)
    Zeitraum = NumberField("Zeitraum(Jahre)", default=20)


class Controller(ViktorController):
    label = 'My Entity Type'
    parametrization = Parametrization(width=30)

    @GeometryAndDataView("Geometry", duration_guess=0, update_label='Run Grasshopper')
    def run_grasshopper(self, params, **kwargs):

        rhino_upload = params['Modell'].file
        # Read the rhino file
        gh_file = rhino3dm.File3dm.FromByteArray(rhino_upload.getvalue_binary())

        layer_name = 'TEST'

        encoded_objects = []

        # Durchlaufen aller Objekte und Kodieren der zum gewĂŒnschten Layer gehörenden
        for obj in gh_file.Objects:
            layer = gh_file.Layers[obj.Attributes.LayerIndex]
            if layer.Name == layer_name:
                geom = obj.Geometry
                # Kodieren des Geometrieobjekts
                encoded_data = geom.Encode()
                encoded_objects.append(encoded_data)

        # Erstellen einer JSON-Datei aus den Eingabeparametern

        params['encoded_geometry'] = encoded_objects  # FĂŒgen Sie die kodierten Geometrien zu den Parametern hinzu
        input_json = json.dumps(params)  # Konvertieren aller Parameter inklusive der Geometrie in JSON

        # Erstellen der Eingabedateien fĂŒr die Analyse
        files = [
            ('input.json', BytesIO(bytes(input_json, 'utf8'))),
        ]

        # Run the Grasshopper analysis and obtain the output files
        generic_analysis = GenericAnalysis(files=files, executable_key="run_grasshopper", output_filenames=[
            "geometry.3dm", "output.json"
        ])
        generic_analysis.execute(timeout=60)
        rhino_3dm_file = generic_analysis.get_output_file("geometry.3dm", as_file=True)
        output_values: File = generic_analysis.get_output_file("output.json", as_file=True)

        # Create a DataGroup object to display output data
        output_dict = json.loads(output_values.getvalue())
        print(output_dict)
        data_group = DataGroup(
            *[DataItem(key.replace("_", " "), val) for key, val in output_dict.items()]
        )

        return GeometryAndDataResult(geometry=rhino_3dm_file, geometry_type="3dm", data=data_group)

we get this error in powershell: Traceback (most recent call last):
File “viktor_connector\connector.pyx”, line 295, in connector.Job.execute
File “viktor\core.pyx”, line 1926, in viktor.core._handle_job
File “viktor\core.pyx”, line 1911, in viktor.core._handle_job._handle_view
File “viktor\views.pyx”, line 2047, in viktor.views.View.wrapper
File “C:\Program Files\Viktor_apps\my-first-app\app.py”, line 57, in run_grasshopper
input_json = json.dumps(params) # Konvertieren aller Parameter inklusive der Geometrie in JSON
File "C:\Program Files\Python310\lib\json_init
.py", line 231, in dumps
return _default_encoder.encode(obj)

do you maybe have time for a short call? because we donÂŽt really now if itÂŽs possible with viktor what we would like to achieve

thanks alot

samuel & marco

After a call we have this issue working!

The idea above was pretty good; there were some issues however.

  1. The input_json = json.dumps(params) call threw some errors as params is a dict of the parameters, but the FileField doesn’t return a JSON serializable object. params.pop('Modell') fixed that.
  2. The encoded_data needed to be in a json string. So encoded_data = json.dumps(geom.Encode()).
  3. In the run_grasshopper.py script on the worker, we needed to add some code that if the input is already a list: feed the list.

@PVSyst : maybe this feature could also be valuable for you:

1 Like