Using GeometrySelect with 3dm files

Hey there!

We would like to implement your newest feature of selecting a geometry in the 3d view and further process this in grasshopper. Because the grasshopper calculation can only be made when the “facade” is selected, we thougt of splitting the input (geometry and data) and selection and the output in two pages. But we are stuck at this stage. How do we define the pages in our controller and how do we make a geometryview that is only for selecting the facade without processing it? (processing should be done in the second page)

This is our state of the script:

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

class Parametrization(ViktorParametrization):
    intro = Text("## PV Machbarkeitsanalyse \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
    step_1 = Step("Stufe 1: Modellupload", ['run_grasshopper'])
    step_1.selected_geometry = GeometrySelectField("Wähle eine Fassadenfläche aus")
    step_1.selected_geometries = GeometryMultiSelectField("Wähle die Umgebungsgeometrie aus")
    step_1.Modell = FileField('Modell', file_types=['.3dm'], max_size=100_000_000)

    step_2 = Step("Stufe 2: Berechnung", views=['run_grasshopper'])
    step_2.Modultyp = OptionField('Modultyp', options=[OptionListElement('0', 'L 1300x875'),
                                                OptionListElement('1', 'Q 1300x720'),
                                                OptionListElement('2', 'M 985x875'),
                                                OptionListElement('3', 'S 985x720')], default=0)
    step_2.Eigenverbrauch = NumberField("Eigenverbrauch in % des Gesamtertrages", default=50)
    step_2.Investitionskosten = NumberField("Investitionskosten", default=10000)
    step_2.Instandhaltungskosten = NumberField("Instandhaltungskosten in % der Investitionskosten", default=2.0)
    step_2.Foerderbeitrag = NumberField("Förderbeitrag %", default=6)
    step_2.Strompreis = NumberField("Strompreis(Fr./kWh)", default=1.0)
    step_2.Einspeiseverguetung = NumberField("Einspeiseverguetung(Fr./kWh)", default=1)
    step_2.Zeitraum = NumberField("Zeitraum(Jahre)", default=5)


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
        selected_geometry = params.selected_geometry
        selected_geometries = params.selected_geometries

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

        facade = selected_geometry
        context = selected_geometries

        encoded_facade = []
        encoded_context = []

        # Durchlaufen aller Objekte und Kodieren der zum gewünschten Layer gehörenden
        for obj in gh_file.Objects:
            if facade:
                geom = obj.Geometry
                # Kodieren des Geometrieobjekts
                encoded_data = json.dumps(geom.Encode())
                encoded_facade.append(encoded_data)

        for obj in gh_file.Objects:
            if context:
                geom = obj.Geometry
                # Kodieren des Geometrieobjekts
                encoded_data2 = json.dumps(geom.Encode())
                encoded_context.append(encoded_data2)



        # Erstellen einer JSON-Datei aus den Eingabeparametern
        #new dictinary
        params.pop("Modell")
        params['encoded_facade'] = encoded_facade
        params['encoded_context'] = encoded_context
        input_json = json.dumps(params)  # Konvertieren aller Parameter inklusive der Geometrie in JSON
        print(params)
        # 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)

Thanks for your help!

Hi @Broesmeli,

To use the GeometrySelect with 3dm-files, you will have to fill in the Name of a mesh:

Your GeometrySelectField will then return ‘This is the identifier’.


To retrieve only this mesh from a model you are on the right track; you indeed loop over all objects and compare the name in attributes:

# Read the rhino file from your upload
rh_file = rhino3dm.File3dm.FromByteArray(
    params.step_1.rhino_upload.getvalue_binary()
)

# Prepare output list
encoded_facade = []

# Iterate over all objects
for rhino_object in rh_file.Objects:
    if params.step_1.selected_geometry == rhino_object.Attributes.Name:
        # Encode and add to list
        encoded_data = json.dumps(geom.Encode())
        encoded_facade.append(encoded_data)

Extra: Generating 3dm geometries compatible for geometry select

This also works the other way around; if you don’t have a 3dm file to start with. You can create meshes yourself, and create ObjectAttributes and add a identifier to the Name value. The GeometrySelectField will then return that identifier when you select it.


Regarding your question on how to build this into multiple steps; I’d suggest creating 2 views, and assigning the choose_geom view only to the first step, and your Grasshopper definition in the second one. Your view can look like this:

@GeometryView("Choose Geometry", duration_guess=0, x_axis_to_right=True)
def choose_geom(self, params, **kwargs):
    return GeometryResult(geometry=params.step_1.Modell.file, geometry_type="3dm")

Hopefully this comment helps you out!

2 Likes

Hey @rvandijk

Perfect, now we are a step further :slight_smile: The geometry selection works and we got 2 separate steps now, yeah!

Now that we splitted up the import and selection (GeometryView) from the data input (GeometryAndDataView) we got a problem with the “rhino_upload”. We think that we mixed up the steps a little bit.

Can you maybe help us out again …?

Most recent error:

Traceback (most recent call last):
  File "viktor_connector\\connector.pyx", line 302, in connector.Job.execute
  File "viktor\core.pyx", line 1940, in viktor.core._handle_job
  File "viktor\core.pyx", line 1925, in viktor.core._handle_job._handle_view
  File "viktor\views.pyx", line 2053, in viktor.views.View._wrapper
  File "D:\Programme\Viktor\viktor-apps\pv-app\app.py", line 59, in run_grasshopper
    if params.step_2.selected_geometry == rhino_upload.Attributes.Name:
  File "D:\Programme\Viktor\viktor-apps\pv-app\venv\lib\site-packages\munch\__init__.py", line 121, in __getattr__
    raise AttributeError(k)
AttributeError: selected_geometry

Complete app.py - code:

import json
import rhino3dm
from viktor import ViktorController, File
from viktor.parametrization import NumberField, FileField, Text, ViktorParametrization, Step, OptionField, \
    OptionListElement, GeometrySelectField, GeometryMultiSelectField
from viktor.external.generic import GenericAnalysis
from viktor.views import GeometryAndDataView, GeometryAndDataResult, DataGroup, DataItem, GeometryView, GeometryResult


class Parametrization(ViktorParametrization):
    step_1 = Step('Modell_Modultyp', views='choose_geom')
    step_1.intro = Text(
        "## PV Machbarkeitsanalyse \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

    step_1.selected_geometry = GeometrySelectField("Wähle eine Fassadenfläche aus")
    step_1.selected_geometries = GeometryMultiSelectField("Wähle die Umgebungsgeometrie aus")
    step_1.Modell = FileField('Modell', file_types=['.3dm'], max_size=100_000_000)

    step_2 = Step('Angaben', views='run_grasshopper')

    step_2.Modultyp = OptionField('Modultyp', options=[OptionListElement('0', 'L 1300x875'),
                                                       OptionListElement('1', 'Q 1300x720'),
                                                       OptionListElement('2', 'M 985x875'),
                                                       OptionListElement('3', 'S 985x720')], default=0)
    step_2.Eigenverbrauch = NumberField("Eigenverbrauch in % des Gesamtertrages", default=50)
    step_2.Investitionskosten = NumberField("Investitionskosten", default=10000)
    step_2.Instandhaltungskosten = NumberField("Instandhaltungskosten in % der Investitionskosten", default=2.0)
    step_2.Foerderbeitrag = NumberField("Förderbeitrag %", default=6)
    step_2.Strompreis = NumberField("Strompreis(Fr./kWh)", default=1.0)
    step_2.Einspeiseverguetung = NumberField("Einspeiseverguetung(Fr./kWh)", default=1)
    step_2.Zeitraum = NumberField("Zeitraum(Jahre)", default=5)


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

    @GeometryView("Choose Geometry", duration_guess=0, x_axis_to_right=True)
    def choose_geom(self, params, **kwargs):
        return GeometryResult(geometry=params.step_1.Modell.file, geometry_type="3dm")

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


        rhino_upload = params.step_1['Modell'].file

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

        encoded_facade = []
        encoded_context = []

        # Durchlaufen aller Objekte und Kodieren der zum gewünschten Layer gehörenden
        for obj in gh_file.Objects:
            if params.step_2.selected_geometry == rhino_upload.Attributes.Name:
                geom = obj.Geometry
                # Kodieren des Geometrieobjekts
                encoded_data = json.dumps(geom.Encode())
                encoded_facade.append(encoded_data)

        for obj in gh_file.Objects:
            if params.step_2.selected_geometries == rhino_upload.Attributes.Name:
                geom = obj.Geometry
                # Kodieren des Geometrieobjekts
                encoded_data2 = json.dumps(geom.Encode())
                encoded_context.append(encoded_data2)

        # Erstellen einer JSON-Datei aus den Eingabeparametern
        # new dictinary
        params.step_2.pop("Modell")
        params['encoded_facade'] = encoded_facade
        params['encoded_context'] = encoded_context
        input_json = json.dumps(params)  # Konvertieren aller Parameter inklusive der Geometrie in JSON
        print(params)
        # 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=params.step_2.rhino_3dm_file, geometry_type="3dm", data=data_group)

Step_1:

Step_2:

Thank you so much, really appreciate your support!

Hi @Broesmeli ,

selected_geometry and selected_geometries are part of step_1, so while looping through the gh_file.Objects, you will have to reference step_1 in stead of step_2.

Hopefully that helps out!

1 Like

That should be it. Thank you!