Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 111 additions & 46 deletions app.py
Original file line number Diff line number Diff line change
@@ -1,77 +1,142 @@
import datetime
import json
from io import BytesIO
import os
import tempfile
from pathlib import Path
import compute_rhino3d.Grasshopper as gh
import compute_rhino3d.Util
import rhino3dm

from viktor import ViktorController, File
from viktor.external.generic import GenericAnalysis
from viktor.parametrization import NumberField
from viktor.parametrization import Text
from viktor.parametrization import ViktorParametrization, DateField
from viktor.parametrization import ViktorParametrization, DateField, NumberField, ToggleButton, Text, Lookup
from viktor.utils import memoize
from viktor.views import GeometryAndDataView, GeometryAndDataResult, DataGroup, DataItem


class Parametrization(ViktorParametrization):
intro = Text(
"## Grasshopper Analysis app \n This app parametrically generates and analyses a "
"# Sunlight Hours Analysis with Grasshopper & Ladybug 🐞 🦗 🦏 \n This app parametrically generates and analyses a "
"3D model of a tower using a Grasshopper script. "
"The sun hour analysis is carried out using the Ladybug plugin for Grasshopper. "
"Geometry and resulting values are sent back and forth to the Grasshopper script in real-time."
"\n\n Please fill in the following parameters:"
"The sun hours analysis is carried out using the Ladybug plugin for Grasshopper. \n "
"\n Please fill in the following parameters:"
)

# Input fields
floorplan_width = NumberField(
"Floorplan width", default=15, min=10, max=18, suffix="m", flex=100, variant='slider', step=1
)
twist_top = NumberField(
"Twist top", default=0.65, min=0.20, max=1.00, variant='slider', flex=100, step=0.01
)
floor_height = NumberField(
"Floor height", default=3.5, min=2.5, max=5.0, suffix="m", variant='slider', flex=100, step=0.1
)
tower_height = NumberField(
"Tower height", default=75, min=20, max=100, suffix="m", flex=100, variant='slider', step=1
)
rotation = NumberField(
"Rotation", default=60, min=0, max=90, suffix="°", flex=100, variant='slider', step=1
)
date = DateField(
'Date for the sun hour analysis', default=datetime.date.today(), flex=100
)
floorplan_width = NumberField("Floorplan width", default=15, min=10, max=18, suffix="m", flex=100, variant='slider', step=1)
twist_top = NumberField("Twist top", default=0.65, min=0.20, max=1.00, variant='slider', flex=100, step=0.01)
floor_height = NumberField("Floor height", default=3.5, min=2.5, max=5.0, suffix="m", variant='slider', flex=100, step=0.1)
tower_height = NumberField("Tower height", default=75, min=20, max=100, suffix="m", flex=100, variant='slider', step=1)
rotation = NumberField("Rotation", default=60, min=0, max=90, suffix="°", flex=100, variant='slider', step=1)
run_solar_analysis = ToggleButton("Sun Hours Analysis")
date = DateField("Date for the sun hour analysis", default=datetime.date.today(), flex=100, visible=Lookup('run_solar_analysis'))

outro = Text(" ## Start building Grasshopper cloud apps [here](https://community.viktor.ai/t/sunlight-hours-analysis-with-grasshopper-and-ladybug/1250?u=mostafa) 🚀 ")
ps = Text("PS: If the app starts after an hour of inactivity, it takes an extra 30 seconds to get the Rhino Compute server up and running.")


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

@GeometryAndDataView("Geometry", duration_guess=0, update_label='Run Grasshopper')
@GeometryAndDataView("Output", duration_guess=0, update_label='Run Grasshopper')
def run_grasshopper(self, params, **kwargs):
# Credentials for Rhino Compute api
compute_rhino3d.Util.url = os.getenv("RHINO_COMPUTE_URL")
compute_rhino3d.Util.apiKey = os.getenv("RHINO_COMPUTE_API_KEY")

# Replace datetime object with month and day
date: datetime.date = params["date"]
params["month"] = date.month
params["day"] = date.day
params.pop("date")

# Create a JSON file from the input parameters
input_json = json.dumps(params)
# Run Grasshopper file with input parameters
output = self.evaluate_grasshopper(str(Path(__file__).parent / 'files' / 'script.gh'), params)

# Create a new rhino3dm file and save resulting geometry to file
file = rhino3dm.File3dm()
output_geometry = self.get_value_from_tree(output, "Geometry", index=0)
output_geometry2 = self.get_value_from_tree(output, "Geometry2")

obj = rhino3dm.CommonObject.Decode(json.loads(output_geometry))
file.Objects.AddMesh(obj)

for data in output_geometry2:
obj2 = rhino3dm.CommonObject.Decode(json.loads(data))
file.Objects.Add(obj2)

# Add solar analysis legend values
point_x = self.get_value_from_tree(output, "pointsx")
point_y = self.get_value_from_tree(output, "pointsy")
solar_vals = self.get_value_from_tree(output, "solar_values")
for i in range(len(point_y)):
pointyvalue = float(point_y[i].replace('"', ""))
pointxvalue = float(point_x[i].replace('"', ""))
solarv = str(solar_vals[i])
file.Objects.AddTextDot(text=solarv, location=rhino3dm._rhino3dm.Point3d(pointxvalue, pointyvalue, 0))

# Add compass orientation
compass__x = self.get_value_from_tree(output, "compass_x")
compass__y = self.get_value_from_tree(output, "compass_y")
comapass__text = self.get_value_from_tree(output, "compass_text")
for i in range(len(compass__x)):
compass_xpoint = float(compass__x[i].replace('"', ""))
compass_ypoint = float(compass__y[i].replace('"', ""))
compassv= str(comapass__text[i])
file.Objects.AddTextDot(text=compassv, location=rhino3dm._rhino3dm.Point3d(compass_xpoint,compass_ypoint, 0))

# Save Rhino file to a temporary file
temp_file = tempfile.NamedTemporaryFile(suffix=".3dm", delete=False, mode="wb")
temp_file.close()
file.Write(temp_file.name, 7)
rhino_3dm_file = File.from_path(Path(temp_file.name))

# Parse output data
output_params = ["floor_area", "gross_area", "facade_area"]
if params.run_solar_analysis:
output_params.extend(["avg_sun_hours_context", "avg_sun_hours_tower"])

output_values = {}
for key in output_params:
val = self.get_value_from_tree(output, key, index=0)
output_values[key] = float(val.replace("\"", ""))

data_items = [
DataItem('Floor area', output_values["floor_area"], suffix='m²', number_of_decimals=0),
DataItem('Gross area', output_values["gross_area"], suffix='m²', number_of_decimals=0),
DataItem('Facade area', output_values["facade_area"], suffix='m²', number_of_decimals=0),
]

if params.run_solar_analysis:
data_items.extend([
DataItem('Avg sun hours context', output_values["avg_sun_hours_context"], suffix='h', number_of_decimals=2),
DataItem('Avg sun hours tower', output_values["avg_sun_hours_tower"], suffix='h', number_of_decimals=2),
])

# Generate the input files
files = [('input.json', BytesIO(bytes(input_json, 'utf8')))]
return GeometryAndDataResult(geometry=rhino_3dm_file, geometry_type="3dm", data=DataGroup(*data_items))

# 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)
@staticmethod
@memoize
def evaluate_grasshopper(file_path, params):
# Create the input DataTree
input_trees = []
for key, value in params.items():
tree = gh.DataTree(key)
tree.Append([{0}], [str(value).lower()])
input_trees.append(tree)

# 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 gh.EvaluateDefinition(file_path, input_trees)

return GeometryAndDataResult(geometry=rhino_3dm_file, geometry_type="3dm", data=data_group)
@staticmethod
def get_value_from_tree(datatree: dict, param_name: str, index=None):
"""Get first value in datatree that matches given param_name"""
for val in datatree['values']:
if val["ParamName"] == param_name:
try:
if index is not None:
return val['InnerTree']['{0}'][index]['data']
return [v['data'] for v in val['InnerTree']['{0}']]
except:
if index is not None:
return val['InnerTree']['{0}'][index]['data']
return [v['data'] for v in val['InnerTree']['{0;0}']]
9 changes: 0 additions & 9 deletions files/input.json

This file was deleted.

57 changes: 0 additions & 57 deletions files/run_grasshopper.py

This file was deleted.

Binary file modified files/script.gh
Binary file not shown.
4 changes: 3 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,3 @@
viktor==14.4.0
viktor==14.4.0
compute-rhino3d==0.12.2
rhino3dm==7.15.0
2 changes: 1 addition & 1 deletion viktor.config.toml
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
app_type = 'editor'
python_version = '3.11' # '3.8' | '3.9' | '3.10' | '3.11'
python_version = '3.10' # '3.8' | '3.9' | '3.10' | '3.11'